fix(coding-agent): stop skill recursion at root closes #2075
This commit is contained in:
@@ -140,8 +140,9 @@ export interface LoadSkillsFromDirOptions {
|
||||
* Load skills from a directory.
|
||||
*
|
||||
* Discovery rules:
|
||||
* - direct .md children in the root
|
||||
* - recursive SKILL.md under subdirectories
|
||||
* - if a directory contains SKILL.md, treat it as a skill root and do not recurse further
|
||||
* - otherwise, load direct .md children in the root
|
||||
* - recurse into subdirectories to find SKILL.md
|
||||
*/
|
||||
export function loadSkillsFromDir(options: LoadSkillsFromDirOptions): LoadSkillsResult {
|
||||
const { dir, source } = options;
|
||||
@@ -169,6 +170,35 @@ function loadSkillsFromDirInternal(
|
||||
try {
|
||||
const entries = readdirSync(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name !== "SKILL.md") {
|
||||
continue;
|
||||
}
|
||||
|
||||
const fullPath = join(dir, entry.name);
|
||||
|
||||
let isFile = entry.isFile();
|
||||
if (entry.isSymbolicLink()) {
|
||||
try {
|
||||
isFile = statSync(fullPath).isFile();
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
const relPath = toPosixPath(relative(root, fullPath));
|
||||
if (!isFile || ig.ignores(relPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = loadSkillFromFile(fullPath, source);
|
||||
if (result.skill) {
|
||||
skills.push(result.skill);
|
||||
}
|
||||
diagnostics.push(...result.diagnostics);
|
||||
return { skills, diagnostics };
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.name.startsWith(".")) {
|
||||
continue;
|
||||
@@ -208,13 +238,7 @@ function loadSkillsFromDirInternal(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!isFile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isRootMd = includeRootFiles && entry.name.endsWith(".md");
|
||||
const isSkillMd = !includeRootFiles && entry.name === "SKILL.md";
|
||||
if (!isRootMd && !isSkillMd) {
|
||||
if (!isFile || !includeRootFiles || !entry.name.endsWith(".md")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
3
packages/coding-agent/test/fixtures/skills/root-skill-preferred/SKILL.md
vendored
Normal file
3
packages/coding-agent/test/fixtures/skills/root-skill-preferred/SKILL.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
description: Root skill should win.
|
||||
---
|
||||
3
packages/coding-agent/test/fixtures/skills/root-skill-preferred/nested-child/SKILL.md
vendored
Normal file
3
packages/coding-agent/test/fixtures/skills/root-skill-preferred/nested-child/SKILL.md
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
---
|
||||
description: Nested skill should be ignored.
|
||||
---
|
||||
@@ -86,6 +86,18 @@ describe("skills", () => {
|
||||
expect(diagnostics).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should prefer a directory's root SKILL.md over nested SKILL.md files", () => {
|
||||
const { skills, diagnostics } = loadSkillsFromDir({
|
||||
dir: join(fixturesDir, "root-skill-preferred"),
|
||||
source: "test",
|
||||
});
|
||||
|
||||
expect(skills).toHaveLength(1);
|
||||
expect(skills[0].name).toBe("root-skill-preferred");
|
||||
expect(skills[0].description).toBe("Root skill should win.");
|
||||
expect(diagnostics).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("should skip files without frontmatter", () => {
|
||||
const { skills, diagnostics } = loadSkillsFromDir({
|
||||
dir: join(fixturesDir, "no-frontmatter"),
|
||||
|
||||
Reference in New Issue
Block a user