From 39cbf47e42433ce301dabcec398cac6fe5f0fa22 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Fri, 20 Feb 2026 00:15:53 +0100 Subject: [PATCH] feat(coding-agent): discover skills in .agents paths by default --- packages/coding-agent/CHANGELOG.md | 4 ++ packages/coding-agent/README.md | 2 +- packages/coding-agent/docs/sdk.md | 8 ++- packages/coding-agent/docs/skills.md | 8 ++- .../coding-agent/src/core/package-manager.ts | 44 ++++++++++++++- .../coding-agent/test/package-manager.test.ts | 56 +++++++++++++++++++ 6 files changed, 115 insertions(+), 7 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 957b9dff..01759e75 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/README.md b/packages/coding-agent/README.md index c8101897..580a2d69 100644 --- a/packages/coding-agent/README.md +++ b/packages/coding-agent/README.md @@ -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 diff --git a/packages/coding-agent/docs/sdk.md b/packages/coding-agent/docs/sdk.md index db756330..52514201 100644 --- a/packages/coding-agent/docs/sdk.md +++ b/packages/coding-agent/docs/sdk.md @@ -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`) diff --git a/packages/coding-agent/docs/skills.md b/packages/coding-agent/docs/skills.md index 66b09fa1..4bcfc031 100644 --- a/packages/coding-agent/docs/skills.md +++ b/packages/coding-agent/docs/skills.md @@ -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 ` (repeatable, additive even with `--no-skills`) diff --git a/packages/coding-agent/src/core/package-manager.ts b/packages/coding-agent/src/core/package-manager.ts index 82efb53b..cc47cd32 100644 --- a/packages/coding-agent/src/core/package-manager.ts +++ b/packages/coding-agent/src/core/package-manager.ts @@ -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, diff --git a/packages/coding-agent/test/package-manager.test.ts b/packages/coding-agent/test/package-manager.test.ts index 5fcb043b..295abef8 100644 --- a/packages/coding-agent/test/package-manager.test.ts +++ b/packages/coding-agent/test/package-manager.test.ts @@ -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");