feat(coding-agent): discover skills in .agents paths by default

This commit is contained in:
Mario Zechner 2026-02-20 00:15:53 +01:00
parent 7207c16c84
commit 39cbf47e42
6 changed files with 115 additions and 7 deletions

View file

@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Added default skill auto-discovery for `.agents/skills` locations. Pi now discovers project skills from `.agents/skills` in `cwd` and ancestor directories (up to git repo root, or filesystem root when not in a repo), and global skills from `~/.agents/skills`, in addition to existing `.pi` skill paths.
## [0.53.1] - 2026-02-19
### Changed

View file

@ -292,7 +292,7 @@ Use this skill when the user asks about X.
2. Then that
```
Place in `~/.pi/agent/skills/`, `.pi/skills/`, or a [pi package](#pi-packages) to share with others. See [docs/skills.md](docs/skills.md).
Place in `~/.pi/agent/skills/`, `~/.agents/skills/`, `.pi/skills/`, or `.agents/skills/` (from `cwd` up through parent directories) or a [pi package](#pi-packages) to share with others. See [docs/skills.md](docs/skills.md).
### Extensions

View file

@ -258,14 +258,18 @@ const { session } = await createAgentSession({
`cwd` is used by `DefaultResourceLoader` for:
- Project extensions (`.pi/extensions/`)
- Project skills (`.pi/skills/`)
- Project skills:
- `.pi/skills/`
- `.agents/skills/` in `cwd` and ancestor directories (up to git repo root, or filesystem root when not in a repo)
- Project prompts (`.pi/prompts/`)
- Context files (`AGENTS.md` walking up from cwd)
- Session directory naming
`agentDir` is used by `DefaultResourceLoader` for:
- Global extensions (`extensions/`)
- Global skills (`skills/`)
- Global skills:
- `skills/` under `agentDir` (for example `~/.pi/agent/skills/`)
- `~/.agents/skills/`
- Global prompts (`prompts/`)
- Global context file (`AGENTS.md`)
- Settings (`settings.json`)

View file

@ -23,8 +23,12 @@ Pi implements the [Agent Skills standard](https://agentskills.io/specification),
Pi loads skills from:
- Global: `~/.pi/agent/skills/`
- Project: `.pi/skills/`
- Global:
- `~/.pi/agent/skills/`
- `~/.agents/skills/`
- Project:
- `.pi/skills/`
- `.agents/skills/` in `cwd` and ancestor directories (up to git repo root, or filesystem root when not in a repo)
- Packages: `skills/` directories or `pi.skills` entries in `package.json`
- Settings: `skills` array with files or directories
- CLI: `--skill <path>` (repeatable, additive even with `--no-skills`)

View file

@ -286,6 +286,41 @@ function collectAutoSkillEntries(dir: string, includeRootFiles = true): string[]
return collectSkillEntries(dir, includeRootFiles);
}
function findGitRepoRoot(startDir: string): string | null {
let dir = resolve(startDir);
while (true) {
if (existsSync(join(dir, ".git"))) {
return dir;
}
const parent = dirname(dir);
if (parent === dir) {
return null;
}
dir = parent;
}
}
function collectAncestorAgentsSkillDirs(startDir: string): string[] {
const skillDirs: string[] = [];
const resolvedStartDir = resolve(startDir);
const gitRepoRoot = findGitRepoRoot(resolvedStartDir);
let dir = resolvedStartDir;
while (true) {
skillDirs.push(join(dir, ".agents", "skills"));
if (gitRepoRoot && dir === gitRepoRoot) {
break;
}
const parent = dirname(dir);
if (parent === dir) {
break;
}
dir = parent;
}
return skillDirs;
}
function collectAutoPromptEntries(dir: string): string[] {
const entries: string[] = [];
if (!existsSync(dir)) return entries;
@ -1548,6 +1583,8 @@ export class DefaultPackageManager implements PackageManager {
prompts: join(projectBaseDir, "prompts"),
themes: join(projectBaseDir, "themes"),
};
const userAgentsSkillsDir = join(homedir(), ".agents", "skills");
const projectAgentsSkillDirs = collectAncestorAgentsSkillDirs(this.cwd);
const addResources = (
resourceType: ResourceType,
@ -1572,7 +1609,7 @@ export class DefaultPackageManager implements PackageManager {
);
addResources(
"skills",
collectAutoSkillEntries(userDirs.skills),
[...collectAutoSkillEntries(userDirs.skills), ...collectAutoSkillEntries(userAgentsSkillsDir)],
userMetadata,
userOverrides.skills,
globalBaseDir,
@ -1601,7 +1638,10 @@ export class DefaultPackageManager implements PackageManager {
);
addResources(
"skills",
collectAutoSkillEntries(projectDirs.skills),
[
...collectAutoSkillEntries(projectDirs.skills),
...projectAgentsSkillDirs.flatMap((dir) => collectAutoSkillEntries(dir)),
],
projectMetadata,
projectOverrides.skills,
projectBaseDir,

View file

@ -147,6 +147,62 @@ Content`,
});
});
describe(".agents/skills auto-discovery", () => {
it("should scan .agents/skills from cwd up to git repo root", async () => {
const repoRoot = join(tempDir, "repo");
const nestedCwd = join(repoRoot, "packages", "feature");
mkdirSync(nestedCwd, { recursive: true });
mkdirSync(join(repoRoot, ".git"), { recursive: true });
const aboveRepoSkill = join(tempDir, ".agents", "skills", "above-repo", "SKILL.md");
mkdirSync(join(tempDir, ".agents", "skills", "above-repo"), { recursive: true });
writeFileSync(aboveRepoSkill, "---\nname: above-repo\ndescription: above\n---\n");
const repoRootSkill = join(repoRoot, ".agents", "skills", "repo-root", "SKILL.md");
mkdirSync(join(repoRoot, ".agents", "skills", "repo-root"), { recursive: true });
writeFileSync(repoRootSkill, "---\nname: repo-root\ndescription: repo\n---\n");
const nestedSkill = join(repoRoot, "packages", ".agents", "skills", "nested", "SKILL.md");
mkdirSync(join(repoRoot, "packages", ".agents", "skills", "nested"), { recursive: true });
writeFileSync(nestedSkill, "---\nname: nested\ndescription: nested\n---\n");
const pm = new DefaultPackageManager({
cwd: nestedCwd,
agentDir,
settingsManager,
});
const result = await pm.resolve();
expect(result.skills.some((r) => r.path === repoRootSkill && r.enabled)).toBe(true);
expect(result.skills.some((r) => r.path === nestedSkill && r.enabled)).toBe(true);
expect(result.skills.some((r) => r.path === aboveRepoSkill)).toBe(false);
});
it("should scan .agents/skills up to filesystem root when not in a git repo", async () => {
const nonRepoRoot = join(tempDir, "non-repo");
const nestedCwd = join(nonRepoRoot, "a", "b");
mkdirSync(nestedCwd, { recursive: true });
const rootSkill = join(nonRepoRoot, ".agents", "skills", "root", "SKILL.md");
mkdirSync(join(nonRepoRoot, ".agents", "skills", "root"), { recursive: true });
writeFileSync(rootSkill, "---\nname: root\ndescription: root\n---\n");
const middleSkill = join(nonRepoRoot, "a", ".agents", "skills", "middle", "SKILL.md");
mkdirSync(join(nonRepoRoot, "a", ".agents", "skills", "middle"), { recursive: true });
writeFileSync(middleSkill, "---\nname: middle\ndescription: middle\n---\n");
const pm = new DefaultPackageManager({
cwd: nestedCwd,
agentDir,
settingsManager,
});
const result = await pm.resolve();
expect(result.skills.some((r) => r.path === rootSkill && r.enabled)).toBe(true);
expect(result.skills.some((r) => r.path === middleSkill && r.enabled)).toBe(true);
});
});
describe("ignore files", () => {
it("should respect .gitignore in skill directories", async () => {
const skillsDir = join(agentDir, "skills");