Merge pull request #3013 from code-yeongyu/fix/issue-2971-skill-shortname

fix(skill): resolve namespaced skills by short name
This commit is contained in:
YeonGyu-Kim
2026-04-02 10:43:52 +09:00
committed by GitHub
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)) {