mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 11:02:17 +00:00
feat(coding-agent): discover skills in .agents paths by default
This commit is contained in:
parent
7207c16c84
commit
39cbf47e42
6 changed files with 115 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
|
|
|
|||
|
|
@ -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`)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue