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/CHANGELOG.md +21 -0
- package/README.md +35 -12
- package/agent-management.ts +15 -6
- package/agent-manager-detail.ts +12 -2
- package/agent-manager-edit.ts +75 -23
- package/agent-manager-list.ts +9 -2
- package/agent-manager.ts +199 -11
- package/agents.ts +315 -20
- package/artifacts.ts +11 -5
- package/async-execution.ts +92 -71
- package/chain-clarify.ts +45 -156
- package/chain-execution.ts +23 -63
- package/execution.ts +54 -49
- package/index.ts +1 -1
- package/intercom-bridge.ts +8 -0
- package/model-fallback.ts +8 -2
- package/package.json +1 -1
- package/schemas.ts +1 -1
- package/settings.ts +6 -4
- package/skills.ts +259 -77
- package/subagent-executor.ts +45 -15
- package/subagent-runner.ts +176 -51
- package/types.ts +64 -13
- package/utils.ts +5 -10
- package/worktree.ts +27 -9
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
|
|
79
|
-
const pkgJsonPath = path.join(packageRoot, "package.json");
|
|
82
|
+
function readOptionalJsonFile(filePath: string, label: string): unknown {
|
|
80
83
|
try {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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(...
|
|
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(...
|
|
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):
|
|
159
|
-
const results:
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
206
|
+
results.push({ path: resolved, source });
|
|
207
|
+
}
|
|
182
208
|
}
|
|
183
209
|
|
|
184
210
|
return results;
|
|
185
211
|
}
|
|
186
212
|
|
|
187
|
-
function
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
200
|
-
const
|
|
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
|
-
|
|
203
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
213
|
-
if (
|
|
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 =
|
|
410
|
+
const loaded = collectFilesystemSkills(cwd, skillPaths);
|
|
238
411
|
const dedupedByName = new Map<string, CachedSkillEntry>();
|
|
239
412
|
|
|
240
|
-
for (
|
|
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
|
|
package/subagent-executor.ts
CHANGED
|
@@ -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 = {
|
|
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
|
|
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 = {
|
|
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) =>
|
|
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 = {
|
|
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 =
|
|
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 = {
|
|
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
|
-
|
|
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,
|