fix(coding-agent): stop skill recursion at root closes #2075

This commit is contained in:
Mario Zechner
2026-03-14 04:18:28 +01:00
parent acb0f4d807
commit a8e3c829fa
4 changed files with 51 additions and 9 deletions

View File

@@ -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;
}

View File

@@ -0,0 +1,3 @@
---
description: Root skill should win.
---

View File

@@ -0,0 +1,3 @@
---
description: Nested skill should be ignored.
---

View File

@@ -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"),