mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 05:00:16 +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]
|
## [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
|
## [0.53.1] - 2026-02-19
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
|
||||||
|
|
@ -292,7 +292,7 @@ Use this skill when the user asks about X.
|
||||||
2. Then that
|
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
|
### Extensions
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -258,14 +258,18 @@ const { session } = await createAgentSession({
|
||||||
|
|
||||||
`cwd` is used by `DefaultResourceLoader` for:
|
`cwd` is used by `DefaultResourceLoader` for:
|
||||||
- Project extensions (`.pi/extensions/`)
|
- 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/`)
|
- Project prompts (`.pi/prompts/`)
|
||||||
- Context files (`AGENTS.md` walking up from cwd)
|
- Context files (`AGENTS.md` walking up from cwd)
|
||||||
- Session directory naming
|
- Session directory naming
|
||||||
|
|
||||||
`agentDir` is used by `DefaultResourceLoader` for:
|
`agentDir` is used by `DefaultResourceLoader` for:
|
||||||
- Global extensions (`extensions/`)
|
- Global extensions (`extensions/`)
|
||||||
- Global skills (`skills/`)
|
- Global skills:
|
||||||
|
- `skills/` under `agentDir` (for example `~/.pi/agent/skills/`)
|
||||||
|
- `~/.agents/skills/`
|
||||||
- Global prompts (`prompts/`)
|
- Global prompts (`prompts/`)
|
||||||
- Global context file (`AGENTS.md`)
|
- Global context file (`AGENTS.md`)
|
||||||
- Settings (`settings.json`)
|
- Settings (`settings.json`)
|
||||||
|
|
|
||||||
|
|
@ -23,8 +23,12 @@ Pi implements the [Agent Skills standard](https://agentskills.io/specification),
|
||||||
|
|
||||||
Pi loads skills from:
|
Pi loads skills from:
|
||||||
|
|
||||||
- Global: `~/.pi/agent/skills/`
|
- Global:
|
||||||
- Project: `.pi/skills/`
|
- `~/.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`
|
- Packages: `skills/` directories or `pi.skills` entries in `package.json`
|
||||||
- Settings: `skills` array with files or directories
|
- Settings: `skills` array with files or directories
|
||||||
- CLI: `--skill <path>` (repeatable, additive even with `--no-skills`)
|
- CLI: `--skill <path>` (repeatable, additive even with `--no-skills`)
|
||||||
|
|
|
||||||
|
|
@ -286,6 +286,41 @@ function collectAutoSkillEntries(dir: string, includeRootFiles = true): string[]
|
||||||
return collectSkillEntries(dir, includeRootFiles);
|
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[] {
|
function collectAutoPromptEntries(dir: string): string[] {
|
||||||
const entries: string[] = [];
|
const entries: string[] = [];
|
||||||
if (!existsSync(dir)) return entries;
|
if (!existsSync(dir)) return entries;
|
||||||
|
|
@ -1548,6 +1583,8 @@ export class DefaultPackageManager implements PackageManager {
|
||||||
prompts: join(projectBaseDir, "prompts"),
|
prompts: join(projectBaseDir, "prompts"),
|
||||||
themes: join(projectBaseDir, "themes"),
|
themes: join(projectBaseDir, "themes"),
|
||||||
};
|
};
|
||||||
|
const userAgentsSkillsDir = join(homedir(), ".agents", "skills");
|
||||||
|
const projectAgentsSkillDirs = collectAncestorAgentsSkillDirs(this.cwd);
|
||||||
|
|
||||||
const addResources = (
|
const addResources = (
|
||||||
resourceType: ResourceType,
|
resourceType: ResourceType,
|
||||||
|
|
@ -1572,7 +1609,7 @@ export class DefaultPackageManager implements PackageManager {
|
||||||
);
|
);
|
||||||
addResources(
|
addResources(
|
||||||
"skills",
|
"skills",
|
||||||
collectAutoSkillEntries(userDirs.skills),
|
[...collectAutoSkillEntries(userDirs.skills), ...collectAutoSkillEntries(userAgentsSkillsDir)],
|
||||||
userMetadata,
|
userMetadata,
|
||||||
userOverrides.skills,
|
userOverrides.skills,
|
||||||
globalBaseDir,
|
globalBaseDir,
|
||||||
|
|
@ -1601,7 +1638,10 @@ export class DefaultPackageManager implements PackageManager {
|
||||||
);
|
);
|
||||||
addResources(
|
addResources(
|
||||||
"skills",
|
"skills",
|
||||||
collectAutoSkillEntries(projectDirs.skills),
|
[
|
||||||
|
...collectAutoSkillEntries(projectDirs.skills),
|
||||||
|
...projectAgentsSkillDirs.flatMap((dir) => collectAutoSkillEntries(dir)),
|
||||||
|
],
|
||||||
projectMetadata,
|
projectMetadata,
|
||||||
projectOverrides.skills,
|
projectOverrides.skills,
|
||||||
projectBaseDir,
|
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", () => {
|
describe("ignore files", () => {
|
||||||
it("should respect .gitignore in skill directories", async () => {
|
it("should respect .gitignore in skill directories", async () => {
|
||||||
const skillsDir = join(agentDir, "skills");
|
const skillsDir = join(agentDir, "skills");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue