pi-subagents 0.13.3 → 0.14.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/skills.ts CHANGED
@@ -6,7 +6,6 @@ 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 { loadSkills, type Skill } from "@mariozechner/pi-coding-agent";
10
9
 
11
10
  export type SkillSource =
12
11
  | "project"
@@ -39,6 +38,11 @@ interface CachedSkillEntry {
39
38
  order: number;
40
39
  }
41
40
 
41
+ interface SkillSearchPath {
42
+ path: string;
43
+ source: SkillSource;
44
+ }
45
+
42
46
  const skillCache = new Map<string, SkillCacheEntry>();
43
47
  const MAX_CACHE_SIZE = 50;
44
48
 
@@ -75,21 +79,45 @@ function isWithinPath(filePath: string, dir: string): boolean {
75
79
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
76
80
  }
77
81
 
78
- function getPackageSkillPaths(packageRoot: string): string[] {
79
- const pkgJsonPath = path.join(packageRoot, "package.json");
82
+ function readOptionalJsonFile(filePath: string, label: string): unknown {
80
83
  try {
81
- const content = fs.readFileSync(pkgJsonPath, "utf-8");
82
- const pkg = JSON.parse(content);
83
- const piSkills = pkg?.pi?.skills;
84
- if (!Array.isArray(piSkills)) return [];
85
- return piSkills
86
- .filter((s: unknown) => typeof s === "string")
87
- .map((s: string) => path.resolve(packageRoot, s));
84
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
85
+ } catch (error) {
86
+ const code = typeof error === "object" && error !== null && "code" in error
87
+ ? (error as { code?: unknown }).code
88
+ : undefined;
89
+ if (code === "ENOENT") return null;
90
+ const message = error instanceof Error ? error.message : String(error);
91
+ throw new Error(`Failed to read ${label} '${filePath}': ${message}`, {
92
+ cause: error instanceof Error ? error : undefined,
93
+ });
94
+ }
95
+ }
96
+
97
+ function readJsonFileBestEffort(filePath: string): unknown {
98
+ try {
99
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
88
100
  } catch {
89
- return [];
101
+ // Package scans over installed dependencies are opportunistic.
102
+ return null;
90
103
  }
91
104
  }
92
105
 
106
+ function extractSkillPathsFromPackageRoot(packageRoot: string, source: SkillSource, bestEffort = false): SkillSearchPath[] {
107
+ const packageJsonPath = path.join(packageRoot, "package.json");
108
+ const pkg = bestEffort
109
+ ? readJsonFileBestEffort(packageJsonPath)
110
+ : readOptionalJsonFile(packageJsonPath, "package manifest");
111
+ if (!pkg || typeof pkg !== "object" || Array.isArray(pkg)) return [];
112
+ const pi = (pkg as { pi?: unknown }).pi;
113
+ if (!pi || typeof pi !== "object" || Array.isArray(pi)) return [];
114
+ const skills = (pi as { skills?: unknown }).skills;
115
+ if (!Array.isArray(skills)) return [];
116
+ return skills
117
+ .filter((entry): entry is string => typeof entry === "string")
118
+ .map((entry) => ({ path: path.resolve(packageRoot, entry), source }));
119
+ }
120
+
93
121
  let cachedGlobalNpmRoot: string | null = null;
94
122
 
95
123
  function getGlobalNpmRoot(): string | null {
@@ -98,30 +126,30 @@ function getGlobalNpmRoot(): string | null {
98
126
  cachedGlobalNpmRoot = execSync("npm root -g", { encoding: "utf-8", timeout: 5000 }).trim();
99
127
  return cachedGlobalNpmRoot;
100
128
  } catch {
129
+ // Global npm root is optional in constrained environments.
101
130
  cachedGlobalNpmRoot = ""; // Empty string means "tried but failed"
102
131
  return null;
103
132
  }
104
133
  }
105
134
 
106
- function collectPackageSkillPaths(cwd: string): string[] {
107
- const dirs = [
108
- path.join(cwd, CONFIG_DIR, "npm", "node_modules"),
109
- path.join(AGENT_DIR, "npm", "node_modules"),
135
+ function collectInstalledPackageSkillPaths(cwd: string): SkillSearchPath[] {
136
+ const dirs: SkillSearchPath[] = [
137
+ { path: path.join(cwd, CONFIG_DIR, "npm", "node_modules"), source: "project-package" },
138
+ { path: path.join(AGENT_DIR, "npm", "node_modules"), source: "user-package" },
110
139
  ];
111
-
112
- // Add global npm root if available (where pi installs global packages)
140
+
113
141
  const globalRoot = getGlobalNpmRoot();
114
142
  if (globalRoot) {
115
- dirs.push(globalRoot);
143
+ dirs.push({ path: globalRoot, source: "user-package" });
116
144
  }
117
-
118
- const results: string[] = [];
145
+
146
+ const results: SkillSearchPath[] = [];
119
147
 
120
148
  for (const dir of dirs) {
121
- if (!fs.existsSync(dir)) continue;
149
+ if (!fs.existsSync(dir.path)) continue;
122
150
  let entries: fs.Dirent[];
123
151
  try {
124
- entries = fs.readdirSync(dir, { withFileTypes: true });
152
+ entries = fs.readdirSync(dir.path, { withFileTypes: true });
125
153
  } catch {
126
154
  continue;
127
155
  }
@@ -131,7 +159,7 @@ function collectPackageSkillPaths(cwd: string): string[] {
131
159
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
132
160
 
133
161
  if (entry.name.startsWith("@")) {
134
- const scopeDir = path.join(dir, entry.name);
162
+ const scopeDir = path.join(dir.path, entry.name);
135
163
  let scopeEntries: fs.Dirent[];
136
164
  try {
137
165
  scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
@@ -142,75 +170,129 @@ function collectPackageSkillPaths(cwd: string): string[] {
142
170
  if (scopeEntry.name.startsWith(".")) continue;
143
171
  if (!scopeEntry.isDirectory() && !scopeEntry.isSymbolicLink()) continue;
144
172
  const pkgRoot = path.join(scopeDir, scopeEntry.name);
145
- results.push(...getPackageSkillPaths(pkgRoot));
173
+ results.push(...extractSkillPathsFromPackageRoot(pkgRoot, dir.source, true));
146
174
  }
147
175
  continue;
148
176
  }
149
177
 
150
- const pkgRoot = path.join(dir, entry.name);
151
- results.push(...getPackageSkillPaths(pkgRoot));
178
+ const pkgRoot = path.join(dir.path, entry.name);
179
+ results.push(...extractSkillPathsFromPackageRoot(pkgRoot, dir.source, true));
152
180
  }
153
181
  }
154
182
 
155
183
  return results;
156
184
  }
157
185
 
158
- function collectSettingsSkillPaths(cwd: string): string[] {
159
- const results: string[] = [];
186
+ function collectSettingsSkillPaths(cwd: string): SkillSearchPath[] {
187
+ const results: SkillSearchPath[] = [];
160
188
  const settingsFiles = [
161
- { file: path.join(cwd, CONFIG_DIR, "settings.json"), base: path.join(cwd, CONFIG_DIR) },
162
- { file: path.join(AGENT_DIR, "settings.json"), base: AGENT_DIR },
189
+ { file: path.join(cwd, CONFIG_DIR, "settings.json"), base: path.join(cwd, CONFIG_DIR), source: "project-settings" as const },
190
+ { file: path.join(AGENT_DIR, "settings.json"), base: AGENT_DIR, source: "user-settings" as const },
163
191
  ];
164
192
 
165
- for (const { file, base } of settingsFiles) {
166
- try {
167
- const content = fs.readFileSync(file, "utf-8");
168
- const settings = JSON.parse(content);
169
- const skills = settings?.skills;
170
- if (!Array.isArray(skills)) continue;
171
- for (const entry of skills) {
172
- if (typeof entry !== "string") continue;
173
- let resolved = entry;
174
- if (resolved.startsWith("~/")) {
175
- resolved = path.join(os.homedir(), resolved.slice(2));
176
- } else if (!path.isAbsolute(resolved)) {
177
- resolved = path.resolve(base, resolved);
178
- }
179
- results.push(resolved);
193
+ for (const { file, base, source } of settingsFiles) {
194
+ const settings = readOptionalJsonFile(file, "skills settings file");
195
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) continue;
196
+ const skills = (settings as { skills?: unknown }).skills;
197
+ if (!Array.isArray(skills)) continue;
198
+ for (const entry of skills) {
199
+ if (typeof entry !== "string") continue;
200
+ let resolved = entry;
201
+ if (resolved.startsWith("~/")) {
202
+ resolved = path.join(os.homedir(), resolved.slice(2));
203
+ } else if (!path.isAbsolute(resolved)) {
204
+ resolved = path.resolve(base, resolved);
180
205
  }
181
- } catch {}
206
+ results.push({ path: resolved, source });
207
+ }
182
208
  }
183
209
 
184
210
  return results;
185
211
  }
186
212
 
187
- function buildSkillPaths(cwd: string): string[] {
188
- const defaultSkillPaths = [
189
- path.join(cwd, CONFIG_DIR, "skills"),
190
- path.join(cwd, ".agents", "skills"),
191
- path.join(AGENT_DIR, "skills"),
192
- path.join(os.homedir(), ".agents", "skills"),
213
+ function resolveSettingsPackageRoot(source: string, baseDir: string): string | undefined {
214
+ const trimmed = source.trim();
215
+ if (!trimmed) return undefined;
216
+ const normalized = trimmed.startsWith("file:") ? trimmed.slice(5) : trimmed;
217
+ if (normalized === "~") return os.homedir();
218
+ if (normalized.startsWith("~/")) return path.join(os.homedir(), normalized.slice(2));
219
+ if (path.isAbsolute(normalized)) return normalized;
220
+ if (normalized === "." || normalized === ".." || normalized.startsWith("./") || normalized.startsWith("../")) {
221
+ return path.resolve(baseDir, normalized);
222
+ }
223
+ return undefined;
224
+ }
225
+
226
+ function collectSettingsPackageSkillPaths(cwd: string): SkillSearchPath[] {
227
+ const settingsFiles = [
228
+ { file: path.join(cwd, CONFIG_DIR, "settings.json"), base: path.join(cwd, CONFIG_DIR), source: "project-package" as const },
229
+ { file: path.join(AGENT_DIR, "settings.json"), base: AGENT_DIR, source: "user-package" as const },
193
230
  ];
194
- const packagePaths = collectPackageSkillPaths(cwd);
195
- const settingsPaths = collectSettingsSkillPaths(cwd);
196
- return [...new Set([...defaultSkillPaths, ...packagePaths, ...settingsPaths])];
231
+ const results: SkillSearchPath[] = [];
232
+
233
+ for (const { file, base, source } of settingsFiles) {
234
+ const settings = readOptionalJsonFile(file, "skills settings file");
235
+ if (!settings || typeof settings !== "object" || Array.isArray(settings)) continue;
236
+ const packages = (settings as { packages?: unknown }).packages;
237
+ if (!Array.isArray(packages)) continue;
238
+
239
+ for (const entry of packages) {
240
+ const packageSource = typeof entry === "string"
241
+ ? entry
242
+ : typeof entry === "object" && entry !== null && typeof (entry as { source?: unknown }).source === "string"
243
+ ? (entry as { source: string }).source
244
+ : undefined;
245
+ if (!packageSource) continue;
246
+
247
+ const packageRoot = resolveSettingsPackageRoot(packageSource, base);
248
+ if (!packageRoot) continue;
249
+ results.push(...extractSkillPathsFromPackageRoot(packageRoot, source));
250
+ }
251
+ }
252
+
253
+ return results;
197
254
  }
198
255
 
199
- function inferSkillSource(sourceInfo: { source: string; scope: string }, filePath: string, cwd: string): SkillSource {
200
- const { scope, source } = sourceInfo;
256
+ function buildSkillPaths(cwd: string): SkillSearchPath[] {
257
+ const skillPaths: SkillSearchPath[] = [
258
+ { path: path.join(cwd, CONFIG_DIR, "skills"), source: "project" },
259
+ { path: path.join(cwd, ".agents", "skills"), source: "project" },
260
+ { path: path.join(AGENT_DIR, "skills"), source: "user" },
261
+ { path: path.join(os.homedir(), ".agents", "skills"), source: "user" },
262
+ ...collectInstalledPackageSkillPaths(cwd),
263
+ ...collectSettingsPackageSkillPaths(cwd),
264
+ ...extractSkillPathsFromPackageRoot(cwd, "project-package"),
265
+ ...collectSettingsSkillPaths(cwd),
266
+ ];
201
267
 
202
- if (scope === "project" && source === "local") return "project";
203
- if (scope === "user" && source === "local") return "user";
268
+ const deduped = new Map<string, SkillSearchPath>();
269
+ for (const entry of skillPaths) {
270
+ const resolvedPath = path.resolve(entry.path);
271
+ if (!deduped.has(resolvedPath)) {
272
+ deduped.set(resolvedPath, { path: resolvedPath, source: entry.source });
273
+ }
274
+ }
275
+ return [...deduped.values()];
276
+ }
277
+
278
+ function inferSkillSource(filePath: string, cwd: string, sourceHint?: SkillSource): SkillSource {
279
+ if (sourceHint) return sourceHint;
280
+
281
+ const projectConfigRoot = path.resolve(cwd, CONFIG_DIR);
282
+ const projectSkillsRoot = path.resolve(cwd, CONFIG_DIR, "skills");
283
+ const projectPackagesRoot = path.resolve(cwd, CONFIG_DIR, "npm", "node_modules");
284
+ const projectAgentsRoot = path.resolve(cwd, ".agents");
285
+ const userSkillsRoot = path.resolve(AGENT_DIR, "skills");
286
+ const userPackagesRoot = path.resolve(AGENT_DIR, "npm", "node_modules");
287
+ const userAgentsRoot = path.resolve(os.homedir(), ".agents");
204
288
 
205
- // Fallback: infer from file path when sourceInfo isn't specific enough
206
- // (e.g. scope === "temporary" for skills loaded via explicit skillPaths)
207
- const projectRoot = path.resolve(cwd, CONFIG_DIR);
208
- const altProjectRoot = path.resolve(cwd, ".agents");
209
- const isProjectScoped = isWithinPath(filePath, projectRoot) || isWithinPath(filePath, altProjectRoot);
210
- if (isProjectScoped) return "project";
289
+ if (isWithinPath(filePath, projectPackagesRoot)) return "project-package";
290
+ if (isWithinPath(filePath, projectSkillsRoot) || isWithinPath(filePath, projectAgentsRoot)) return "project";
291
+ if (isWithinPath(filePath, projectConfigRoot)) return "project-settings";
211
292
 
212
- const isUserScoped = isWithinPath(filePath, AGENT_DIR) || isWithinPath(filePath, path.join(os.homedir(), ".agents"));
213
- if (isUserScoped) return "user";
293
+ if (isWithinPath(filePath, userPackagesRoot)) return "user-package";
294
+ if (isWithinPath(filePath, userSkillsRoot) || isWithinPath(filePath, userAgentsRoot)) return "user";
295
+ if (isWithinPath(filePath, AGENT_DIR)) return "user-settings";
214
296
 
215
297
  const globalRoot = getGlobalNpmRoot();
216
298
  if (globalRoot && isWithinPath(filePath, globalRoot)) return "user-package";
@@ -227,6 +309,97 @@ function chooseHigherPrioritySkill(existing: CachedSkillEntry | undefined, candi
227
309
  return candidate.order < existing.order ? candidate : existing;
228
310
  }
229
311
 
312
+ function maybeReadSkillDescription(filePath: string): string | undefined {
313
+ try {
314
+ const content = fs.readFileSync(filePath, "utf-8");
315
+ const normalized = content.replace(/\r\n/g, "\n");
316
+ if (!normalized.startsWith("---")) return undefined;
317
+
318
+ const endIndex = normalized.indexOf("\n---", 3);
319
+ if (endIndex === -1) return undefined;
320
+
321
+ const frontmatter = normalized.slice(3, endIndex).trim();
322
+ const match = frontmatter.match(/^description:\s*(.+)$/m);
323
+ if (!match) return undefined;
324
+ return match[1]?.trim().replace(/^['\"]|['\"]$/g, "");
325
+ } catch {
326
+ // Description parsing is best-effort metadata extraction.
327
+ return undefined;
328
+ }
329
+ }
330
+
331
+ function collectFilesystemSkills(cwd: string, skillPaths: SkillSearchPath[]): CachedSkillEntry[] {
332
+ const entries: CachedSkillEntry[] = [];
333
+ const seen = new Set<string>();
334
+ let order = 0;
335
+
336
+ const pushEntry = (name: string, filePath: string, sourceHint?: SkillSource) => {
337
+ const resolvedFile = path.resolve(filePath);
338
+ if (seen.has(resolvedFile)) return;
339
+ if (!fs.existsSync(resolvedFile)) return;
340
+ seen.add(resolvedFile);
341
+ entries.push({
342
+ name,
343
+ filePath: resolvedFile,
344
+ source: inferSkillSource(resolvedFile, cwd, sourceHint),
345
+ description: maybeReadSkillDescription(resolvedFile),
346
+ order: order++,
347
+ });
348
+ };
349
+
350
+ for (const skillPath of skillPaths) {
351
+ if (!fs.existsSync(skillPath.path)) continue;
352
+
353
+ let stat: fs.Stats;
354
+ try {
355
+ stat = fs.statSync(skillPath.path);
356
+ } catch {
357
+ continue;
358
+ }
359
+
360
+ if (stat.isFile()) {
361
+ const fileName = path.basename(skillPath.path);
362
+ if (!fileName.toLowerCase().endsWith(".md")) continue;
363
+ const skillName = fileName.toLowerCase() === "skill.md"
364
+ ? path.basename(path.dirname(skillPath.path))
365
+ : path.basename(fileName, path.extname(fileName));
366
+ pushEntry(skillName, skillPath.path, skillPath.source);
367
+ continue;
368
+ }
369
+
370
+ if (!stat.isDirectory()) continue;
371
+
372
+ const rootSkillFile = path.join(skillPath.path, "SKILL.md");
373
+ if (fs.existsSync(rootSkillFile)) {
374
+ pushEntry(path.basename(skillPath.path), rootSkillFile, skillPath.source);
375
+ }
376
+
377
+ let childEntries: fs.Dirent[];
378
+ try {
379
+ childEntries = fs.readdirSync(skillPath.path, { withFileTypes: true });
380
+ } catch {
381
+ continue;
382
+ }
383
+
384
+ for (const child of childEntries) {
385
+ if (child.name.startsWith(".")) continue;
386
+ const childPath = path.join(skillPath.path, child.name);
387
+ if (child.isDirectory() || child.isSymbolicLink()) {
388
+ const nestedSkillPath = path.join(childPath, "SKILL.md");
389
+ if (fs.existsSync(nestedSkillPath)) {
390
+ pushEntry(child.name, nestedSkillPath, skillPath.source);
391
+ }
392
+ continue;
393
+ }
394
+ if (child.isFile() && child.name.toLowerCase().endsWith(".md")) {
395
+ pushEntry(path.basename(child.name, path.extname(child.name)), childPath, skillPath.source);
396
+ }
397
+ }
398
+ }
399
+
400
+ return entries;
401
+ }
402
+
230
403
  function getCachedSkills(cwd: string): CachedSkillEntry[] {
231
404
  const now = Date.now();
232
405
  if (loadSkillsCache && loadSkillsCache.cwd === cwd && now - loadSkillsCache.timestamp < LOAD_SKILLS_CACHE_TTL_MS) {
@@ -234,18 +407,10 @@ function getCachedSkills(cwd: string): CachedSkillEntry[] {
234
407
  }
235
408
 
236
409
  const skillPaths = buildSkillPaths(cwd);
237
- const loaded = loadSkills({ cwd, skillPaths, includeDefaults: false });
410
+ const loaded = collectFilesystemSkills(cwd, skillPaths);
238
411
  const dedupedByName = new Map<string, CachedSkillEntry>();
239
412
 
240
- for (let i = 0; i < loaded.skills.length; i++) {
241
- const skill = loaded.skills[i] as Skill;
242
- const entry: CachedSkillEntry = {
243
- name: skill.name,
244
- filePath: skill.filePath,
245
- source: inferSkillSource(skill.sourceInfo, skill.filePath, cwd),
246
- description: skill.description,
247
- order: i,
248
- };
413
+ for (const entry of loaded) {
249
414
  const current = dedupedByName.get(entry.name);
250
415
  dedupedByName.set(entry.name, chooseHigherPrioritySkill(current, entry));
251
416
  }
@@ -294,6 +459,7 @@ export function readSkill(
294
459
 
295
460
  return skill;
296
461
  } catch {
462
+ // Treat unreadable skill files as unresolved so callers can surface as missing.
297
463
  return undefined;
298
464
  }
299
465
  }
@@ -326,6 +492,22 @@ export function resolveSkills(
326
492
  return { resolved, missing };
327
493
  }
328
494
 
495
+ export function resolveSkillsWithFallback(
496
+ skillNames: string[],
497
+ primaryCwd: string,
498
+ fallbackCwd?: string,
499
+ ): { resolved: ResolvedSkill[]; missing: string[] } {
500
+ const primary = resolveSkills(skillNames, primaryCwd);
501
+ if (!fallbackCwd || primary.missing.length === 0) return primary;
502
+ if (path.resolve(primaryCwd) === path.resolve(fallbackCwd)) return primary;
503
+
504
+ const fallback = resolveSkills(primary.missing, fallbackCwd);
505
+ return {
506
+ resolved: [...primary.resolved, ...fallback.resolved],
507
+ missing: fallback.missing,
508
+ };
509
+ }
510
+
329
511
  export function buildSkillInjection(skills: ResolvedSkill[]): string {
330
512
  if (skills.length === 0) return "";
331
513
 
@@ -10,6 +10,7 @@ import { executeChain } from "./chain-execution.ts";
10
10
  import { resolveExecutionAgentScope } from "./agent-scope.ts";
11
11
  import { handleManagementAction } from "./agent-management.ts";
12
12
  import { runSync } from "./execution.ts";
13
+ import { resolveModelCandidate } from "./model-fallback.ts";
13
14
  import { aggregateParallelOutputs } from "./parallel-utils.ts";
14
15
  import { recordRun } from "./run-history.ts";
15
16
  import {
@@ -22,7 +23,7 @@ import {
22
23
  import { discoverAvailableSkills, normalizeSkillInput } from "./skills.ts";
23
24
  import { executeAsyncChain, executeAsyncSingle, isAsyncAvailable } from "./async-execution.ts";
24
25
  import { createForkContextResolver } from "./fork-context.ts";
25
- import { applyIntercomBridgeToAgent, resolveIntercomBridge } from "./intercom-bridge.ts";
26
+ import { applyIntercomBridgeToAgent, resolveIntercomBridge, resolveIntercomSessionTarget } from "./intercom-bridge.ts";
26
27
  import { finalizeSingleOutput, injectSingleOutputInstruction, resolveSingleOutputPath } from "./single-output.ts";
27
28
  import { getSingleResultOutput, mapConcurrent } from "./utils.ts";
28
29
  import {
@@ -364,7 +365,12 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
364
365
  };
365
366
  }
366
367
  const id = randomUUID();
367
- const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
368
+ const asyncCtx = {
369
+ pi: deps.pi,
370
+ cwd: ctx.cwd,
371
+ currentSessionId: deps.state.currentSessionId!,
372
+ currentModelProvider: ctx.model?.provider,
373
+ };
368
374
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
369
375
  provider: m.provider,
370
376
  id: m.id,
@@ -409,6 +415,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
409
415
  const normalizedSkills = normalizeSkillInput(params.skill);
410
416
  const skills = normalizedSkills === false ? [] : normalizedSkills;
411
417
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
418
+ const modelOverride = resolveModelCandidate((params.model as string | undefined) ?? a.model, availableModels, ctx.model?.provider);
412
419
  return executeAsyncSingle(id, {
413
420
  agent: params.agent!,
414
421
  task: params.context === "fork" ? wrapForkTask(params.task!) : params.task!,
@@ -424,7 +431,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
424
431
  sessionFile: sessionFileForIndex(0),
425
432
  skills,
426
433
  output: effectiveOutput,
427
- modelOverride: params.model as string | undefined,
434
+ modelOverride,
428
435
  maxSubagentDepth,
429
436
  worktreeSetupHook: deps.config.worktreeSetupHook,
430
437
  worktreeSetupHookTimeoutMs: deps.config.worktreeSetupHookTimeoutMs,
@@ -485,7 +492,12 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
485
492
  };
486
493
  }
487
494
  const id = randomUUID();
488
- const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
495
+ const asyncCtx = {
496
+ pi: deps.pi,
497
+ cwd: ctx.cwd,
498
+ currentSessionId: deps.state.currentSessionId!,
499
+ currentModelProvider: ctx.model?.provider,
500
+ };
489
501
  const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
490
502
  return executeAsyncChain(id, {
491
503
  chain: asyncChain,
@@ -632,6 +644,7 @@ async function runForegroundParallelTasks(input: ForegroundParallelRunInput): Pr
632
644
  maxSubagentDepth: input.maxSubagentDepths[index],
633
645
  modelOverride: input.modelOverrides[index],
634
646
  availableModels: input.availableModels,
647
+ preferredModelProvider: input.ctx.model?.provider,
635
648
  skills: effectiveSkills === false ? [] : effectiveSkills,
636
649
  onUpdate: input.onUpdate
637
650
  ? (progressUpdate) => {
@@ -707,13 +720,16 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
707
720
  if (worktreeTaskCwdError) return buildParallelModeError(worktreeTaskCwdError);
708
721
  }
709
722
 
723
+ const currentProvider = ctx.model?.provider;
710
724
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
711
725
  provider: m.provider,
712
726
  id: m.id,
713
727
  fullId: `${m.provider}/${m.id}`,
714
728
  }));
715
729
  let taskTexts = tasks.map((t) => t.task);
716
- const modelOverrides: (string | undefined)[] = tasks.map((t) => t.model);
730
+ const modelOverrides: (string | undefined)[] = tasks.map((t, i) =>
731
+ resolveModelCandidate(t.model ?? agentConfigs[i]?.model, availableModels, currentProvider),
732
+ );
717
733
  const skillOverrides: (string[] | false | undefined)[] = tasks.map((t) =>
718
734
  normalizeSkillInput(t.skill),
719
735
  );
@@ -722,7 +738,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
722
738
  const behaviors = agentConfigs.map((c, i) =>
723
739
  resolveStepBehavior(c, { skills: skillOverrides[i] }),
724
740
  );
725
- const availableSkills = discoverAvailableSkills(ctx.cwd);
741
+ const availableSkills = discoverAvailableSkills(params.cwd ?? ctx.cwd);
726
742
 
727
743
  const result = await ctx.ui.custom<ChainClarifyResult>(
728
744
  (tui, theme, _kb, done) =>
@@ -734,6 +750,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
734
750
  undefined,
735
751
  behaviors,
736
752
  availableModels,
753
+ currentProvider,
737
754
  availableSkills,
738
755
  done,
739
756
  "parallel",
@@ -761,7 +778,12 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
761
778
  };
762
779
  }
763
780
  const id = randomUUID();
764
- const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
781
+ const asyncCtx = {
782
+ pi: deps.pi,
783
+ cwd: ctx.cwd,
784
+ currentSessionId: deps.state.currentSessionId!,
785
+ currentModelProvider: ctx.model?.provider,
786
+ };
765
787
  const parallelTasks = tasks.map((t, i) => ({
766
788
  agent: t.agent,
767
789
  task: params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!,
@@ -901,13 +923,18 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
901
923
  };
902
924
  }
903
925
 
926
+ const currentProvider = ctx.model?.provider;
904
927
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map((m) => ({
905
928
  provider: m.provider,
906
929
  id: m.id,
907
930
  fullId: `${m.provider}/${m.id}`,
908
931
  }));
909
932
  let task = params.task!;
910
- let modelOverride: string | undefined = params.model as string | undefined;
933
+ let modelOverride: string | undefined = resolveModelCandidate(
934
+ (params.model as string | undefined) ?? agentConfig.model,
935
+ availableModels,
936
+ currentProvider,
937
+ );
911
938
  let skillOverride: string[] | false | undefined = normalizeSkillInput(params.skill);
912
939
  const rawOutput = params.output !== undefined ? params.output : agentConfig.output;
913
940
  let effectiveOutput: string | false | undefined = rawOutput === true ? agentConfig.output : (rawOutput as string | false | undefined);
@@ -916,7 +943,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
916
943
 
917
944
  if (params.clarify === true && ctx.hasUI) {
918
945
  const behavior = resolveStepBehavior(agentConfig, { output: effectiveOutput, skills: skillOverride });
919
- const availableSkills = discoverAvailableSkills(ctx.cwd);
946
+ const availableSkills = discoverAvailableSkills(params.cwd ?? ctx.cwd);
920
947
 
921
948
  const result = await ctx.ui.custom<ChainClarifyResult>(
922
949
  (tui, theme, _kb, done) =>
@@ -928,6 +955,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
928
955
  undefined,
929
956
  [behavior],
930
957
  availableModels,
958
+ currentProvider,
931
959
  availableSkills,
932
960
  done,
933
961
  "single",
@@ -954,7 +982,12 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
954
982
  };
955
983
  }
956
984
  const id = randomUUID();
957
- const asyncCtx = { pi: deps.pi, cwd: ctx.cwd, currentSessionId: deps.state.currentSessionId! };
985
+ const asyncCtx = {
986
+ pi: deps.pi,
987
+ cwd: ctx.cwd,
988
+ currentSessionId: deps.state.currentSessionId!,
989
+ currentModelProvider: ctx.model?.provider,
990
+ };
958
991
  return executeAsyncSingle(id, {
959
992
  agent: params.agent!,
960
993
  task: params.context === "fork" ? wrapForkTask(task) : task,
@@ -1009,6 +1042,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1009
1042
  onUpdate,
1010
1043
  modelOverride,
1011
1044
  availableModels,
1045
+ preferredModelProvider: currentProvider,
1012
1046
  skills: effectiveSkills,
1013
1047
  });
1014
1048
  recordRun(params.agent!, cleanTask, r.exitCode, r.progressSummary?.durationMs ?? 0);
@@ -1117,11 +1151,7 @@ export function createSubagentExecutor(deps: ExecutorDeps): {
1117
1151
  const parentSessionFile = ctx.sessionManager.getSessionFile() ?? null;
1118
1152
  deps.state.currentSessionId = parentSessionFile ?? `session-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1119
1153
  const discoveredAgents = deps.discoverAgents(ctx.cwd, scope).agents;
1120
- let sessionName = deps.pi.getSessionName()?.trim();
1121
- if (!sessionName) {
1122
- sessionName = `session-${ctx.sessionManager.getSessionId().slice(0, 8)}`;
1123
- deps.pi.setSessionName(sessionName);
1124
- }
1154
+ const sessionName = resolveIntercomSessionTarget(deps.pi.getSessionName(), ctx.sessionManager.getSessionId());
1125
1155
  const intercomBridge = resolveIntercomBridge({
1126
1156
  config: deps.config.intercomBridge,
1127
1157
  context: normalizedParams.context,