@johnnywu/pi-subagents 1.3.0 → 1.4.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 +7 -0
- package/README.md +4 -0
- package/extensions/skill-resolver.ts +150 -78
- package/extensions/subagent-executor.ts +7 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
# [1.4.0](https://github.com/jwu/pi-subagents/compare/v1.3.0...v1.4.0) (2026-06-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* support wildcard skills ([962eaa4](https://github.com/jwu/pi-subagents/commit/962eaa486cd73342629cfecf4e7bcc8a1ceb4710))
|
|
7
|
+
|
|
1
8
|
# [1.3.0](https://github.com/jwu/pi-subagents/compare/v1.2.0...v1.3.0) (2026-06-02)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -95,12 +95,15 @@ Agents are Markdown files with YAML frontmatter.
|
|
|
95
95
|
| `model` | no | parent's model | Provider/model-id (`anthropic/claude-sonnet-4-6`) |
|
|
96
96
|
| `thinking` | no | `off` | Reasoning level: `off`, `low`, `medium`, `high` |
|
|
97
97
|
| `systemPrompt` | no | `append` | How the body is applied: `append` (append to pi default system prompt and project context) or `replace` (replace default prompt and skip project context) |
|
|
98
|
+
| `skills` | no | _none_ | Comma-separated skill names or simple wildcard patterns (`*`, `obsidian-*`) to load (resolved from project `.agents/skills/`, `.pi/skills/`, global `~/.pi/agent/skills/`, or npm packages) |
|
|
98
99
|
| `allowedAgents` | no | _all_ | Comma-separated list of sub-agents this agent may spawn |
|
|
99
100
|
| `maxDepth` | no | `10` | Maximum recursion depth (`0` = no sub-agents, `1` = one level, etc.) |
|
|
100
101
|
| `debug` | no | `false` | When `true`, export the effective runtime system prompt to `debug-system-prompt.md` |
|
|
101
102
|
|
|
102
103
|
The Markdown body after the frontmatter is the agent's system prompt.
|
|
103
104
|
|
|
105
|
+
`skills` entries match the skill frontmatter `name`. Use `skills: *` to load all available skills, or prefix-style patterns such as `skills: obsidian-*` to load matching skills. Only `*` is supported as a wildcard; glob features like `?` or `{a,b}` are not supported.
|
|
106
|
+
|
|
104
107
|
### Example with all fields
|
|
105
108
|
|
|
106
109
|
```markdown
|
|
@@ -111,6 +114,7 @@ tools: subagent, read, grep, find
|
|
|
111
114
|
model: anthropic/claude-sonnet-4-6
|
|
112
115
|
thinking: high
|
|
113
116
|
systemPrompt: append
|
|
117
|
+
skills: tdd, obsidian-*
|
|
114
118
|
allowedAgents: code-reviewer, refactor, test-writer
|
|
115
119
|
maxDepth: 2
|
|
116
120
|
debug: false
|
|
@@ -17,6 +17,7 @@ export interface ResolvedSkill {
|
|
|
17
17
|
export interface SkillResolverFs {
|
|
18
18
|
exists(filePath: string): boolean;
|
|
19
19
|
readFile(filePath: string): string;
|
|
20
|
+
listFiles?(dir: string): string[];
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
export interface ResolveSkillsOptions {
|
|
@@ -31,6 +32,7 @@ export interface ResolveSkillsResult {
|
|
|
31
32
|
resolved: ResolvedSkill[];
|
|
32
33
|
missing: string[];
|
|
33
34
|
skippedPackages: string[];
|
|
35
|
+
warnings: string[];
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
const defaultSkillFs: SkillResolverFs = {
|
|
@@ -40,6 +42,16 @@ const defaultSkillFs: SkillResolverFs = {
|
|
|
40
42
|
readFile(filePath) {
|
|
41
43
|
return fs.readFileSync(filePath, 'utf-8');
|
|
42
44
|
},
|
|
45
|
+
listFiles(dir) {
|
|
46
|
+
try {
|
|
47
|
+
return fs
|
|
48
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
49
|
+
.map((entry) => path.join(dir, entry.name))
|
|
50
|
+
.sort();
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
},
|
|
43
55
|
};
|
|
44
56
|
|
|
45
57
|
function defaultGlobalSkillsDir(): string {
|
|
@@ -89,33 +101,7 @@ function parseSkillFrontmatter(content: string): { name: string; description: st
|
|
|
89
101
|
}
|
|
90
102
|
delete data._currentBlockKey;
|
|
91
103
|
|
|
92
|
-
|
|
93
|
-
return { name: data.name || '', description: data.description.trim() };
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function findLocalSkillFile(
|
|
97
|
-
skillName: string,
|
|
98
|
-
cwd: string,
|
|
99
|
-
globalDir: string,
|
|
100
|
-
fileSystem: SkillResolverFs,
|
|
101
|
-
): string | undefined {
|
|
102
|
-
// Priority 1: Project .agents/skills/<name>/SKILL.md
|
|
103
|
-
const projectAgentsPath = path.join(cwd, '.agents', 'skills', skillName, 'SKILL.md');
|
|
104
|
-
if (fileSystem.exists(projectAgentsPath)) return projectAgentsPath;
|
|
105
|
-
|
|
106
|
-
// Priority 1: Project .pi/skills/<name>/SKILL.md
|
|
107
|
-
const projectPiPath = path.join(cwd, '.pi', 'skills', skillName, 'SKILL.md');
|
|
108
|
-
if (fileSystem.exists(projectPiPath)) return projectPiPath;
|
|
109
|
-
|
|
110
|
-
// Priority 4: Global ~/.pi/agent/skills/<name>/SKILL.md
|
|
111
|
-
const globalPiPath = path.join(globalDir, skillName, 'SKILL.md');
|
|
112
|
-
if (fileSystem.exists(globalPiPath)) return globalPiPath;
|
|
113
|
-
|
|
114
|
-
// Priority 4: Global ~/.agents/skills/<name>/SKILL.md
|
|
115
|
-
const globalAgentsPath = path.join(os.homedir(), '.agents', 'skills', skillName, 'SKILL.md');
|
|
116
|
-
if (fileSystem.exists(globalAgentsPath)) return globalAgentsPath;
|
|
117
|
-
|
|
118
|
-
return undefined;
|
|
104
|
+
return { name: data.name || '', description: (data.description ?? '').trim() };
|
|
119
105
|
}
|
|
120
106
|
|
|
121
107
|
async function resolvePackageSkillFiles(options: ResolveSkillsOptions): Promise<{
|
|
@@ -150,79 +136,165 @@ async function resolvePackageSkillFiles(options: ResolveSkillsOptions): Promise<
|
|
|
150
136
|
};
|
|
151
137
|
}
|
|
152
138
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
):
|
|
139
|
+
interface ReadSkillResult {
|
|
140
|
+
skill?: ResolvedSkill;
|
|
141
|
+
warning?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function readSkill(filePath: string, fileSystem: SkillResolverFs): ReadSkillResult | undefined {
|
|
159
145
|
const content = fileSystem.readFile(filePath);
|
|
160
146
|
const frontmatter = parseSkillFrontmatter(content);
|
|
161
147
|
if (!frontmatter) return undefined;
|
|
162
148
|
|
|
163
|
-
const fileSkillName = frontmatter.name
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return undefined;
|
|
149
|
+
const fileSkillName = frontmatter.name.trim();
|
|
150
|
+
if (!fileSkillName) {
|
|
151
|
+
return { warning: `skill missing required field: name: ${filePath}` };
|
|
167
152
|
}
|
|
153
|
+
if (!frontmatter.description) return undefined;
|
|
168
154
|
|
|
169
155
|
return {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
156
|
+
skill: {
|
|
157
|
+
name: fileSkillName,
|
|
158
|
+
description: frontmatter.description,
|
|
159
|
+
location: filePath,
|
|
160
|
+
},
|
|
173
161
|
};
|
|
174
162
|
}
|
|
175
163
|
|
|
164
|
+
interface CollectedSkills {
|
|
165
|
+
byName: Map<string, ResolvedSkill>;
|
|
166
|
+
warnings: string[];
|
|
167
|
+
skippedPackages: string[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const collectedSkillsCache = new Map<string, Promise<CollectedSkills>>();
|
|
171
|
+
|
|
172
|
+
function listSkillFilesInDir(dir: string, fileSystem: SkillResolverFs): string[] {
|
|
173
|
+
if (!fileSystem.listFiles) return [];
|
|
174
|
+
|
|
175
|
+
return fileSystem
|
|
176
|
+
.listFiles(dir)
|
|
177
|
+
.map((entry) => path.join(entry, 'SKILL.md'))
|
|
178
|
+
.filter((filePath) => fileSystem.exists(filePath));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function addSkillFile(
|
|
182
|
+
filePath: string,
|
|
183
|
+
fileSystem: SkillResolverFs,
|
|
184
|
+
byName: Map<string, ResolvedSkill>,
|
|
185
|
+
warnings: string[],
|
|
186
|
+
): void {
|
|
187
|
+
try {
|
|
188
|
+
const result = readSkill(filePath, fileSystem);
|
|
189
|
+
if (!result) return;
|
|
190
|
+
if (result.warning) warnings.push(result.warning);
|
|
191
|
+
if (result.skill && !byName.has(result.skill.name)) byName.set(result.skill.name, result.skill);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
warnings.push(
|
|
194
|
+
`skill could not be read: ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function collectSkills(options: ResolveSkillsOptions): Promise<CollectedSkills> {
|
|
200
|
+
const cwd = options.cwd;
|
|
201
|
+
const globalDir = options.globalDir ?? defaultGlobalSkillsDir();
|
|
202
|
+
const fileSystem = options.fs ?? defaultSkillFs;
|
|
203
|
+
const byName = new Map<string, ResolvedSkill>();
|
|
204
|
+
const warnings: string[] = [];
|
|
205
|
+
|
|
206
|
+
for (const dir of [
|
|
207
|
+
path.join(cwd, '.agents', 'skills'),
|
|
208
|
+
path.join(cwd, '.pi', 'skills'),
|
|
209
|
+
globalDir,
|
|
210
|
+
path.join(os.homedir(), '.agents', 'skills'),
|
|
211
|
+
]) {
|
|
212
|
+
for (const filePath of listSkillFilesInDir(dir, fileSystem)) {
|
|
213
|
+
addSkillFile(filePath, fileSystem, byName, warnings);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const packageSkills = await resolvePackageSkillFiles(options);
|
|
218
|
+
for (const filePath of packageSkills.files) {
|
|
219
|
+
addSkillFile(filePath, fileSystem, byName, warnings);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { byName, warnings, skippedPackages: packageSkills.skippedPackages };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function skillsCacheKey(options: ResolveSkillsOptions): string | undefined {
|
|
226
|
+
if (options.fs) return undefined;
|
|
227
|
+
return JSON.stringify({
|
|
228
|
+
cwd: options.cwd,
|
|
229
|
+
agentDir: options.agentDir,
|
|
230
|
+
globalDir: options.globalDir ?? defaultGlobalSkillsDir(),
|
|
231
|
+
packageSkillFiles: options.packageSkillFiles,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function getCollectedSkills(options: ResolveSkillsOptions): Promise<CollectedSkills> {
|
|
236
|
+
const cacheKey = skillsCacheKey(options);
|
|
237
|
+
if (!cacheKey) return collectSkills(options);
|
|
238
|
+
|
|
239
|
+
let cached = collectedSkillsCache.get(cacheKey);
|
|
240
|
+
if (!cached) {
|
|
241
|
+
cached = collectSkills(options);
|
|
242
|
+
collectedSkillsCache.set(cacheKey, cached);
|
|
243
|
+
}
|
|
244
|
+
return cached;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function wildcardToRegex(pattern: string): RegExp {
|
|
248
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
249
|
+
return new RegExp(`^${escaped}$`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function hasWildcard(pattern: string): boolean {
|
|
253
|
+
return pattern.includes('*');
|
|
254
|
+
}
|
|
255
|
+
|
|
176
256
|
export async function resolveSkills(
|
|
177
257
|
skillNames: string[],
|
|
178
258
|
options: ResolveSkillsOptions,
|
|
179
259
|
): Promise<ResolveSkillsResult> {
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
260
|
+
const requestedNames = skillNames.map((name) => name.trim()).filter(Boolean);
|
|
261
|
+
if (requestedNames.length === 0) {
|
|
262
|
+
return { resolved: [], missing: [], skippedPackages: [], warnings: [] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const collected = await getCollectedSkills(options);
|
|
183
266
|
const resolved: ResolvedSkill[] = [];
|
|
184
267
|
const missing: string[] = [];
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
for (const name of skillNames) {
|
|
188
|
-
const trimmed = name.trim();
|
|
189
|
-
if (!trimmed) continue;
|
|
190
|
-
|
|
191
|
-
const localFilePath = findLocalSkillFile(trimmed, cwd, globalDir, fileSystem);
|
|
192
|
-
if (localFilePath) {
|
|
193
|
-
try {
|
|
194
|
-
const skill = readSkill(localFilePath, trimmed, fileSystem, false);
|
|
195
|
-
if (skill) {
|
|
196
|
-
resolved.push(skill);
|
|
197
|
-
continue;
|
|
198
|
-
}
|
|
199
|
-
} catch {
|
|
200
|
-
// Try package skills before marking the skill as missing.
|
|
201
|
-
}
|
|
202
|
-
}
|
|
268
|
+
const seen = new Set<string>();
|
|
203
269
|
|
|
204
|
-
|
|
270
|
+
function addSkill(skill: ResolvedSkill): void {
|
|
271
|
+
if (seen.has(skill.name)) return;
|
|
272
|
+
seen.add(skill.name);
|
|
273
|
+
resolved.push(skill);
|
|
205
274
|
}
|
|
206
275
|
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
try {
|
|
216
|
-
packageSkill = readSkill(filePath, trimmed, fileSystem, true);
|
|
217
|
-
if (packageSkill) break;
|
|
218
|
-
} catch {
|
|
219
|
-
// Try the next package skill file.
|
|
276
|
+
for (const requestedName of requestedNames) {
|
|
277
|
+
if (hasWildcard(requestedName)) {
|
|
278
|
+
const regex = wildcardToRegex(requestedName);
|
|
279
|
+
let matched = false;
|
|
280
|
+
for (const skill of collected.byName.values()) {
|
|
281
|
+
if (!regex.test(skill.name)) continue;
|
|
282
|
+
matched = true;
|
|
283
|
+
addSkill(skill);
|
|
220
284
|
}
|
|
285
|
+
if (!matched) missing.push(requestedName);
|
|
286
|
+
continue;
|
|
221
287
|
}
|
|
222
288
|
|
|
223
|
-
|
|
224
|
-
|
|
289
|
+
const skill = collected.byName.get(requestedName);
|
|
290
|
+
if (skill) addSkill(skill);
|
|
291
|
+
else missing.push(requestedName);
|
|
225
292
|
}
|
|
226
293
|
|
|
227
|
-
return {
|
|
294
|
+
return {
|
|
295
|
+
resolved,
|
|
296
|
+
missing,
|
|
297
|
+
skippedPackages: collected.skippedPackages,
|
|
298
|
+
warnings: collected.warnings,
|
|
299
|
+
};
|
|
228
300
|
}
|
|
@@ -98,6 +98,7 @@ export interface BuildSubagentSystemPromptResult {
|
|
|
98
98
|
prompt: string;
|
|
99
99
|
missingSkills: string[];
|
|
100
100
|
skippedSkillPackages: string[];
|
|
101
|
+
skillWarnings: string[];
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
const TASK_FILE_THRESHOLD = 8000;
|
|
@@ -367,6 +368,7 @@ export async function buildSubagentSystemPrompt(
|
|
|
367
368
|
|
|
368
369
|
const missingSkills: string[] = [];
|
|
369
370
|
const skippedSkillPackages: string[] = [];
|
|
371
|
+
const skillWarnings: string[] = [];
|
|
370
372
|
const skillNames = options.agent.skills;
|
|
371
373
|
if (skillNames && skillNames.length > 0) {
|
|
372
374
|
const resolvedSkills = await resolveSkills(skillNames, {
|
|
@@ -375,6 +377,7 @@ export async function buildSubagentSystemPrompt(
|
|
|
375
377
|
});
|
|
376
378
|
missingSkills.push(...resolvedSkills.missing);
|
|
377
379
|
skippedSkillPackages.push(...resolvedSkills.skippedPackages);
|
|
380
|
+
skillWarnings.push(...resolvedSkills.warnings);
|
|
378
381
|
|
|
379
382
|
const skillInjection = formatSkillsForPrompt(resolvedSkills.resolved);
|
|
380
383
|
if (skillInjection) {
|
|
@@ -382,7 +385,7 @@ export async function buildSubagentSystemPrompt(
|
|
|
382
385
|
}
|
|
383
386
|
}
|
|
384
387
|
|
|
385
|
-
return { prompt, missingSkills, skippedSkillPackages };
|
|
388
|
+
return { prompt, missingSkills, skippedSkillPackages, skillWarnings };
|
|
386
389
|
}
|
|
387
390
|
|
|
388
391
|
function progressFromPartialResult(partialResult: unknown): AgentProgress | undefined {
|
|
@@ -437,6 +440,9 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
437
440
|
for (const source of promptResult.skippedSkillPackages) {
|
|
438
441
|
console.warn(`[pi-subagents] package not installed, skipping skills: ${source}`);
|
|
439
442
|
}
|
|
443
|
+
for (const warning of promptResult.skillWarnings) {
|
|
444
|
+
console.warn(`[pi-subagents] ${warning}`);
|
|
445
|
+
}
|
|
440
446
|
for (const name of promptResult.missingSkills) {
|
|
441
447
|
console.warn(`[pi-subagents] skill not found: ${name}`);
|
|
442
448
|
}
|