@johnnywu/pi-subagents 1.3.0 → 1.5.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 CHANGED
@@ -1,3 +1,17 @@
1
+ # [1.5.0](https://github.com/jwu/pi-subagents/compare/v1.4.0...v1.5.0) (2026-06-03)
2
+
3
+
4
+ ### Features
5
+
6
+ * support extended thinking levels ([0721713](https://github.com/jwu/pi-subagents/commit/07217133b20b37c5e2c5a4a3c262b7cf6cabc242))
7
+
8
+ # [1.4.0](https://github.com/jwu/pi-subagents/compare/v1.3.0...v1.4.0) (2026-06-02)
9
+
10
+
11
+ ### Features
12
+
13
+ * support wildcard skills ([962eaa4](https://github.com/jwu/pi-subagents/commit/962eaa486cd73342629cfecf4e7bcc8a1ceb4710))
14
+
1
15
  # [1.3.0](https://github.com/jwu/pi-subagents/compare/v1.2.0...v1.3.0) (2026-06-02)
2
16
 
3
17
 
package/README.md CHANGED
@@ -93,14 +93,17 @@ Agents are Markdown files with YAML frontmatter.
93
93
  | `description` | no | — | Human-readable summary |
94
94
  | `tools` | no | _none_ | Comma-separated tool whitelist (`read, write, bash, grep`, etc.) |
95
95
  | `model` | no | parent's model | Provider/model-id (`anthropic/claude-sonnet-4-6`) |
96
- | `thinking` | no | `off` | Reasoning level: `off`, `low`, `medium`, `high` |
96
+ | `thinking` | no | `off` | Reasoning level: `off`, `minimal`, `low`, `medium`, `high`, `xhigh` |
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
@@ -2,7 +2,7 @@ import * as fs from 'node:fs/promises';
2
2
  import * as os from 'node:os';
3
3
  import * as path from 'node:path';
4
4
 
5
- export type ThinkingLevel = 'off' | 'low' | 'medium' | 'high';
5
+ export type ThinkingLevel = 'off' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh';
6
6
  export type SystemPromptMode = 'replace' | 'append';
7
7
  export type AgentSource = 'global' | 'project';
8
8
 
@@ -104,7 +104,7 @@ function parseAgentFile(content: string, filePath: string, source: AgentSource):
104
104
  if (!data.name) throw new Error('missing required field: name');
105
105
 
106
106
  const thinking = (data.thinking ?? 'off') as ThinkingLevel;
107
- if (!['off', 'low', 'medium', 'high'].includes(thinking)) {
107
+ if (!['off', 'minimal', 'low', 'medium', 'high', 'xhigh'].includes(thinking)) {
108
108
  throw new Error(`invalid thinking: ${data.thinking}`);
109
109
  }
110
110
 
@@ -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
- if (!data.description || !data.description.trim()) return undefined;
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
- function readSkill(
154
- filePath: string,
155
- requestedName: string,
156
- fileSystem: SkillResolverFs,
157
- requireNameMatch: boolean,
158
- ): ResolvedSkill | undefined {
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 || path.basename(path.dirname(filePath));
164
- const markdownName = path.basename(filePath, '.md');
165
- if (requireNameMatch && fileSkillName !== requestedName && markdownName !== requestedName) {
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
- name: fileSkillName || requestedName,
171
- description: frontmatter.description,
172
- location: filePath,
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 cwd = options.cwd;
181
- const globalDir = options.globalDir ?? defaultGlobalSkillsDir();
182
- const fileSystem = options.fs ?? defaultSkillFs;
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 pendingPackageResolution: string[] = [];
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
- pendingPackageResolution.push(trimmed);
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 packageSkills =
208
- pendingPackageResolution.length > 0
209
- ? await resolvePackageSkillFiles(options)
210
- : { files: [], skippedPackages: [] };
211
-
212
- for (const trimmed of pendingPackageResolution) {
213
- let packageSkill: ResolvedSkill | undefined;
214
- for (const filePath of packageSkills.files) {
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
- if (packageSkill) resolved.push(packageSkill);
224
- else missing.push(trimmed);
289
+ const skill = collected.byName.get(requestedName);
290
+ if (skill) addSkill(skill);
291
+ else missing.push(requestedName);
225
292
  }
226
293
 
227
- return { resolved, missing, skippedPackages: packageSkills.skippedPackages };
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@johnnywu/pi-subagents",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "description": "Sub-agents extension for pi coding agent.",
5
5
  "homepage": "https://github.com/jwu/pi-subagents#readme",
6
6
  "repository": {