pi-subagents 0.13.4 → 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
@@ -38,6 +38,11 @@ interface CachedSkillEntry {
38
38
  order: number;
39
39
  }
40
40
 
41
+ interface SkillSearchPath {
42
+ path: string;
43
+ source: SkillSource;
44
+ }
45
+
41
46
  const skillCache = new Map<string, SkillCacheEntry>();
42
47
  const MAX_CACHE_SIZE = 50;
43
48
 
@@ -74,22 +79,45 @@ function isWithinPath(filePath: string, dir: string): boolean {
74
79
  return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
75
80
  }
76
81
 
77
- function getPackageSkillPaths(packageRoot: string): string[] {
78
- const pkgJsonPath = path.join(packageRoot, "package.json");
82
+ function readOptionalJsonFile(filePath: string, label: string): unknown {
83
+ try {
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 {
79
98
  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));
99
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
87
100
  } catch {
88
- // Package scanning is opportunistic; ignore malformed/missing package metadata.
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 {
@@ -104,27 +132,25 @@ function getGlobalNpmRoot(): string | null {
104
132
  }
105
133
  }
106
134
 
107
- function collectPackageSkillPaths(cwd: string): string[] {
108
- const dirs = [
109
- path.join(cwd, CONFIG_DIR, "npm", "node_modules"),
110
- 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" },
111
139
  ];
112
-
113
- // Add global npm root if available (where pi installs global packages)
140
+
114
141
  const globalRoot = getGlobalNpmRoot();
115
142
  if (globalRoot) {
116
- dirs.push(globalRoot);
143
+ dirs.push({ path: globalRoot, source: "user-package" });
117
144
  }
118
-
119
- const results: string[] = [];
145
+
146
+ const results: SkillSearchPath[] = [];
120
147
 
121
148
  for (const dir of dirs) {
122
- if (!fs.existsSync(dir)) continue;
149
+ if (!fs.existsSync(dir.path)) continue;
123
150
  let entries: fs.Dirent[];
124
151
  try {
125
- entries = fs.readdirSync(dir, { withFileTypes: true });
152
+ entries = fs.readdirSync(dir.path, { withFileTypes: true });
126
153
  } catch {
127
- // Ignore unreadable package roots and continue scanning other roots.
128
154
  continue;
129
155
  }
130
156
 
@@ -133,75 +159,125 @@ function collectPackageSkillPaths(cwd: string): string[] {
133
159
  if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
134
160
 
135
161
  if (entry.name.startsWith("@")) {
136
- const scopeDir = path.join(dir, entry.name);
162
+ const scopeDir = path.join(dir.path, entry.name);
137
163
  let scopeEntries: fs.Dirent[];
138
164
  try {
139
165
  scopeEntries = fs.readdirSync(scopeDir, { withFileTypes: true });
140
166
  } catch {
141
- // Ignore unreadable scoped package directories and continue.
142
167
  continue;
143
168
  }
144
169
  for (const scopeEntry of scopeEntries) {
145
170
  if (scopeEntry.name.startsWith(".")) continue;
146
171
  if (!scopeEntry.isDirectory() && !scopeEntry.isSymbolicLink()) continue;
147
172
  const pkgRoot = path.join(scopeDir, scopeEntry.name);
148
- results.push(...getPackageSkillPaths(pkgRoot));
173
+ results.push(...extractSkillPathsFromPackageRoot(pkgRoot, dir.source, true));
149
174
  }
150
175
  continue;
151
176
  }
152
177
 
153
- const pkgRoot = path.join(dir, entry.name);
154
- results.push(...getPackageSkillPaths(pkgRoot));
178
+ const pkgRoot = path.join(dir.path, entry.name);
179
+ results.push(...extractSkillPathsFromPackageRoot(pkgRoot, dir.source, true));
155
180
  }
156
181
  }
157
182
 
158
183
  return results;
159
184
  }
160
185
 
161
- function collectSettingsSkillPaths(cwd: string): string[] {
162
- const results: string[] = [];
186
+ function collectSettingsSkillPaths(cwd: string): SkillSearchPath[] {
187
+ const results: SkillSearchPath[] = [];
163
188
  const settingsFiles = [
164
- { file: path.join(cwd, CONFIG_DIR, "settings.json"), base: path.join(cwd, CONFIG_DIR) },
165
- { 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 },
166
191
  ];
167
192
 
168
- for (const { file, base } of settingsFiles) {
169
- try {
170
- const content = fs.readFileSync(file, "utf-8");
171
- const settings = JSON.parse(content);
172
- const skills = settings?.skills;
173
- if (!Array.isArray(skills)) continue;
174
- for (const entry of skills) {
175
- if (typeof entry !== "string") continue;
176
- let resolved = entry;
177
- if (resolved.startsWith("~/")) {
178
- resolved = path.join(os.homedir(), resolved.slice(2));
179
- } else if (!path.isAbsolute(resolved)) {
180
- resolved = path.resolve(base, resolved);
181
- }
182
- 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);
183
205
  }
184
- } catch {
185
- // Settings-provided skills are optional; ignore malformed or missing settings files.
206
+ results.push({ path: resolved, source });
207
+ }
208
+ }
209
+
210
+ return results;
211
+ }
212
+
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 },
230
+ ];
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));
186
250
  }
187
251
  }
188
252
 
189
253
  return results;
190
254
  }
191
255
 
192
- function buildSkillPaths(cwd: string): string[] {
193
- const defaultSkillPaths = [
194
- path.join(cwd, CONFIG_DIR, "skills"),
195
- path.join(cwd, ".agents", "skills"),
196
- path.join(AGENT_DIR, "skills"),
197
- path.join(os.homedir(), ".agents", "skills"),
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),
198
266
  ];
199
- const packagePaths = collectPackageSkillPaths(cwd);
200
- const settingsPaths = collectSettingsSkillPaths(cwd);
201
- return [...new Set([...defaultSkillPaths, ...packagePaths, ...settingsPaths])];
267
+
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()];
202
276
  }
203
277
 
204
- function inferSkillSource(filePath: string, cwd: string): SkillSource {
278
+ function inferSkillSource(filePath: string, cwd: string, sourceHint?: SkillSource): SkillSource {
279
+ if (sourceHint) return sourceHint;
280
+
205
281
  const projectConfigRoot = path.resolve(cwd, CONFIG_DIR);
206
282
  const projectSkillsRoot = path.resolve(cwd, CONFIG_DIR, "skills");
207
283
  const projectPackagesRoot = path.resolve(cwd, CONFIG_DIR, "npm", "node_modules");
@@ -252,12 +328,12 @@ function maybeReadSkillDescription(filePath: string): string | undefined {
252
328
  }
253
329
  }
254
330
 
255
- function collectFilesystemSkills(cwd: string, skillPaths: string[]): CachedSkillEntry[] {
331
+ function collectFilesystemSkills(cwd: string, skillPaths: SkillSearchPath[]): CachedSkillEntry[] {
256
332
  const entries: CachedSkillEntry[] = [];
257
333
  const seen = new Set<string>();
258
334
  let order = 0;
259
335
 
260
- const pushEntry = (name: string, filePath: string) => {
336
+ const pushEntry = (name: string, filePath: string, sourceHint?: SkillSource) => {
261
337
  const resolvedFile = path.resolve(filePath);
262
338
  if (seen.has(resolvedFile)) return;
263
339
  if (!fs.existsSync(resolvedFile)) return;
@@ -265,60 +341,58 @@ function collectFilesystemSkills(cwd: string, skillPaths: string[]): CachedSkill
265
341
  entries.push({
266
342
  name,
267
343
  filePath: resolvedFile,
268
- source: inferSkillSource(resolvedFile, cwd),
344
+ source: inferSkillSource(resolvedFile, cwd, sourceHint),
269
345
  description: maybeReadSkillDescription(resolvedFile),
270
346
  order: order++,
271
347
  });
272
348
  };
273
349
 
274
350
  for (const skillPath of skillPaths) {
275
- if (!fs.existsSync(skillPath)) continue;
351
+ if (!fs.existsSync(skillPath.path)) continue;
276
352
 
277
353
  let stat: fs.Stats;
278
354
  try {
279
- stat = fs.statSync(skillPath);
355
+ stat = fs.statSync(skillPath.path);
280
356
  } catch {
281
- // Ignore paths that disappear or become unreadable during discovery.
282
357
  continue;
283
358
  }
284
359
 
285
360
  if (stat.isFile()) {
286
- const fileName = path.basename(skillPath);
361
+ const fileName = path.basename(skillPath.path);
287
362
  if (!fileName.toLowerCase().endsWith(".md")) continue;
288
363
  const skillName = fileName.toLowerCase() === "skill.md"
289
- ? path.basename(path.dirname(skillPath))
364
+ ? path.basename(path.dirname(skillPath.path))
290
365
  : path.basename(fileName, path.extname(fileName));
291
- pushEntry(skillName, skillPath);
366
+ pushEntry(skillName, skillPath.path, skillPath.source);
292
367
  continue;
293
368
  }
294
369
 
295
370
  if (!stat.isDirectory()) continue;
296
371
 
297
- const rootSkillFile = path.join(skillPath, "SKILL.md");
372
+ const rootSkillFile = path.join(skillPath.path, "SKILL.md");
298
373
  if (fs.existsSync(rootSkillFile)) {
299
- pushEntry(path.basename(skillPath), rootSkillFile);
374
+ pushEntry(path.basename(skillPath.path), rootSkillFile, skillPath.source);
300
375
  }
301
376
 
302
377
  let childEntries: fs.Dirent[];
303
378
  try {
304
- childEntries = fs.readdirSync(skillPath, { withFileTypes: true });
379
+ childEntries = fs.readdirSync(skillPath.path, { withFileTypes: true });
305
380
  } catch {
306
- // Ignore unreadable skill directories and continue scanning.
307
381
  continue;
308
382
  }
309
383
 
310
384
  for (const child of childEntries) {
311
385
  if (child.name.startsWith(".")) continue;
312
- const childPath = path.join(skillPath, child.name);
386
+ const childPath = path.join(skillPath.path, child.name);
313
387
  if (child.isDirectory() || child.isSymbolicLink()) {
314
388
  const nestedSkillPath = path.join(childPath, "SKILL.md");
315
389
  if (fs.existsSync(nestedSkillPath)) {
316
- pushEntry(child.name, nestedSkillPath);
390
+ pushEntry(child.name, nestedSkillPath, skillPath.source);
317
391
  }
318
392
  continue;
319
393
  }
320
394
  if (child.isFile() && child.name.toLowerCase().endsWith(".md")) {
321
- pushEntry(path.basename(child.name, path.extname(child.name)), childPath);
395
+ pushEntry(path.basename(child.name, path.extname(child.name)), childPath, skillPath.source);
322
396
  }
323
397
  }
324
398
  }
@@ -418,6 +492,22 @@ export function resolveSkills(
418
492
  return { resolved, missing };
419
493
  }
420
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
+
421
511
  export function buildSkillInjection(skills: ResolvedSkill[]): string {
422
512
  if (skills.length === 0) return "";
423
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 {
@@ -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);