fix(skill): resolve namespaced skills by short name

When a skill has a namespaced name like 'superpowers/systematic-debugging',
users see the short name 'systematic-debugging' in the listing but can't
invoke it — the resolver only accepts exact full names.

Add short-name fallback: if exact match fails, try matching the basename
of namespaced skills. Only resolves when unambiguous (single match).

- Exact match still takes priority
- Ambiguous short names (multiple namespaces) fall through to error
- 4 new tests covering all cases

Fixes #2971
This commit is contained in:
YeonGyu-Kim
2026-04-02 10:40:11 +09:00
parent 51d9685571
commit 2275d87a16
2 changed files with 68 additions and 1 deletions
+55
View File
@@ -670,3 +670,58 @@ describe("skill tool - nativeSkills integration", () => {
expect(result).toContain("External plugin skill body")
})
})
describe("skill tool - short name resolution", () => {
it("resolves namespaced skill by short name when unambiguous", async () => {
// given
const loadedSkills = [createMockSkill("superpowers/systematic-debugging")]
const tool = createSkillTool({ skills: loadedSkills })
// when
const result = await tool.execute({ name: "systematic-debugging" }, mockContext)
// then
expect(result).toContain("superpowers/systematic-debugging")
})
it("still resolves by exact full name", async () => {
// given
const loadedSkills = [createMockSkill("superpowers/systematic-debugging")]
const tool = createSkillTool({ skills: loadedSkills })
// when
const result = await tool.execute({ name: "superpowers/systematic-debugging" }, mockContext)
// then
expect(result).toContain("superpowers/systematic-debugging")
})
it("does not resolve short name when ambiguous (multiple matches)", async () => {
// given
const loadedSkills = [
createMockSkill("superpowers/debugging"),
createMockSkill("utils/debugging"),
]
const tool = createSkillTool({ skills: loadedSkills })
// when / then — should not resolve (ambiguous), should suggest both
await expect(tool.execute({ name: "debugging" }, mockContext)).rejects.toThrow(
"not found"
)
})
it("prefers exact match over short name match", async () => {
// given — "debugging" exists as both exact and as part of a namespace
const loadedSkills = [
createMockSkill("debugging"),
createMockSkill("superpowers/debugging"),
]
const tool = createSkillTool({ skills: loadedSkills })
// when
const result = await tool.execute({ name: "debugging" }, mockContext)
// then — should match "debugging" exactly, not "superpowers/debugging"
expect(result).toContain("## Skill: debugging")
})
})
+13 -1
View File
@@ -324,7 +324,19 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition
const requestedName = args.name.replace(/^\//, "")
// Check skills first (exact match, case-insensitive)
const matchedSkill = skills.find(s => s.name.toLowerCase() === requestedName.toLowerCase())
let matchedSkill = skills.find(s => s.name.toLowerCase() === requestedName.toLowerCase())
// Fallback: try matching by short name (basename) for namespaced skills
// e.g. "systematic-debugging" matches "superpowers/systematic-debugging"
if (!matchedSkill) {
const shortNameMatches = skills.filter(s => {
const parts = s.name.split("/")
return parts.length > 1 && parts[parts.length - 1].toLowerCase() === requestedName.toLowerCase()
})
if (shortNameMatches.length === 1) {
matchedSkill = shortNameMatches[0]
}
}
if (matchedSkill) {
if (matchedSkill.definition.agent && (!ctx?.agent || matchedSkill.definition.agent !== ctx.agent)) {