From c132185d177a563708d427f2bf63af42a50b2d7a Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sun, 29 Mar 2026 00:35:59 -0700 Subject: [PATCH 1/2] fix: preserve nested async skill names in discovery Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../async-loader.test.ts | 24 +++++++++++ .../opencode-skill-loader/async-loader.ts | 42 +++++++++++++++---- 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/src/features/opencode-skill-loader/async-loader.test.ts b/src/features/opencode-skill-loader/async-loader.test.ts index 43a4aaa71..8ccb925df 100644 --- a/src/features/opencode-skill-loader/async-loader.test.ts +++ b/src/features/opencode-skill-loader/async-loader.test.ts @@ -107,6 +107,30 @@ Direct skill. expect(skills[0].name).toBe("direct-skill") }) + it("preserves nested skill path names during recursive discovery", async () => { + // given + const nestedSkillDir = join(SKILLS_DIR, "superpowers", "brainstorming") + mkdirSync(nestedSkillDir, { recursive: true }) + writeFileSync( + join(nestedSkillDir, "SKILL.md"), + `--- +name: brainstorming +description: Nested brainstorming skill +--- +Nested skill. +` + ) + + // when + const { discoverSkillsInDirAsync } = await import("./async-loader") + const skills = await discoverSkillsInDirAsync(SKILLS_DIR) + + // then + expect(skills).toHaveLength(1) + expect(skills[0]?.name).toBe("superpowers/brainstorming") + expect(skills[0]?.definition.name).toBe("superpowers/brainstorming") + }) + it("skips entries starting with dot", async () => { // given const validContent = `--- diff --git a/src/features/opencode-skill-loader/async-loader.ts b/src/features/opencode-skill-loader/async-loader.ts index 55148bcaf..141d5aa6f 100644 --- a/src/features/opencode-skill-loader/async-loader.ts +++ b/src/features/opencode-skill-loader/async-loader.ts @@ -75,7 +75,8 @@ export async function loadSkillFromPathAsync( skillPath: string, resolvedPath: string, defaultName: string, - scope: SkillScope + scope: SkillScope, + namePrefix = "" ): Promise { try { const content = await readFile(skillPath, "utf-8") @@ -86,7 +87,8 @@ export async function loadSkillFromPathAsync( const mcpJsonMcp = await loadMcpJsonFromDirAsync(resolvedPath) const mcpConfig = mcpJsonMcp || frontmatterMcp - const skillName = data.name || defaultName + const baseName = data.name || defaultName + const skillName = namePrefix ? `${namePrefix}/${baseName}` : baseName const originalDescription = data.description || "" const isOpencodeSource = scope === "opencode" || scope === "opencode-project" const formattedDescription = `(${scope} - Skill) ${originalDescription}` @@ -142,11 +144,17 @@ function parseAllowedTools(allowedTools: string | string[] | undefined): string[ return allowedTools.split(/\s+/).filter(Boolean) } -export async function discoverSkillsInDirAsync(skillsDir: string): Promise { +export async function discoverSkillsInDirAsync( + skillsDir: string, + scope: SkillScope = "opencode-project", + namePrefix = "", + depth = 0, + maxDepth = 2 +): Promise { try { const entries = await readdir(skillsDir, { withFileTypes: true }) - const processEntry = async (entry: Dirent): Promise => { + const processEntry = async (entry: Dirent): Promise => { if (entry.name.startsWith(".")) return null const entryPath = join(skillsDir, entry.name) @@ -158,28 +166,44 @@ export async function discoverSkillsInDirAsync(skillsDir: string): Promise= maxDepth) { + return null + } + + const nestedPrefix = namePrefix ? `${namePrefix}/${dirName}` : dirName + const nestedSkills = await discoverSkillsInDirAsync( + resolvedPath, + scope, + nestedPrefix, + depth + 1, + maxDepth + ) + + return nestedSkills.length > 0 ? nestedSkills : null } } } if (isMarkdownFile(entry)) { const skillName = basename(entry.name, ".md") - return await loadSkillFromPathAsync(entryPath, skillsDir, skillName, "opencode-project") + return await loadSkillFromPathAsync(entryPath, skillsDir, skillName, scope, namePrefix) } return null } const skillPromises = await mapWithConcurrency(entries, processEntry, 16) - return skillPromises.filter((skill): skill is LoadedSkill => skill !== null) + return skillPromises.flatMap((skill): LoadedSkill[] => { + if (skill === null) return [] + return Array.isArray(skill) ? skill : [skill] + }) } catch (error: unknown) { if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") { return [] From e27f183b65660de96a2c374b5e2b09eccc72152a Mon Sep 17 00:00:00 2001 From: Sisyphus Date: Sun, 29 Mar 2026 01:16:10 -0700 Subject: [PATCH 2/2] test: cover remaining nested async skill paths Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../async-loader.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/src/features/opencode-skill-loader/async-loader.test.ts b/src/features/opencode-skill-loader/async-loader.test.ts index 8ccb925df..498770430 100644 --- a/src/features/opencode-skill-loader/async-loader.test.ts +++ b/src/features/opencode-skill-loader/async-loader.test.ts @@ -131,6 +131,54 @@ Nested skill. expect(skills[0]?.definition.name).toBe("superpowers/brainstorming") }) + it("preserves nested skill path names for nested {dirName}.md discovery", async () => { + // given + const nestedSkillDir = join(SKILLS_DIR, "superpowers", "brainstorming") + mkdirSync(nestedSkillDir, { recursive: true }) + writeFileSync( + join(nestedSkillDir, "brainstorming.md"), + `--- +name: brainstorming +description: Nested brainstorming skill +--- +Nested skill. +` + ) + + // when + const { discoverSkillsInDirAsync } = await import("./async-loader") + const skills = await discoverSkillsInDirAsync(SKILLS_DIR) + + // then + expect(skills).toHaveLength(1) + expect(skills[0]?.name).toBe("superpowers/brainstorming") + expect(skills[0]?.definition.name).toBe("superpowers/brainstorming") + }) + + it("preserves nested skill path names for nested direct markdown discovery", async () => { + // given + const nestedSkillDir = join(SKILLS_DIR, "superpowers") + mkdirSync(nestedSkillDir, { recursive: true }) + writeFileSync( + join(nestedSkillDir, "brainstorming.md"), + `--- +name: brainstorming +description: Nested brainstorming skill +--- +Nested skill. +` + ) + + // when + const { discoverSkillsInDirAsync } = await import("./async-loader") + const skills = await discoverSkillsInDirAsync(SKILLS_DIR) + + // then + expect(skills).toHaveLength(1) + expect(skills[0]?.name).toBe("superpowers/brainstorming") + expect(skills[0]?.definition.name).toBe("superpowers/brainstorming") + }) + it("skips entries starting with dot", async () => { // given const validContent = `---