diff --git a/packages/coding-agent/src/discovery/claude.ts b/packages/coding-agent/src/discovery/claude.ts index cd5bc71d8..69b5ca8b6 100644 --- a/packages/coding-agent/src/discovery/claude.ts +++ b/packages/coding-agent/src/discovery/claude.ts @@ -269,20 +269,27 @@ function readClaudeCommandToggles(): { enableUser: boolean; enableProject: boole } } -function getClaudeCommandNamespaceAlias(commandsDir: string, filePath: string): string | null { - const relativeName = path.relative(commandsDir, filePath).replace(/\.md$/, ""); - if (!/[\\/]/.test(relativeName)) return null; - return relativeName.replace(/[\\/]+/g, ":"); +function getClaudeRelativeCommandName(commandsDir: string, filePath: string): string { + return path.relative(commandsDir, filePath).replace(/\.md$/, ""); } function addClaudeCommandNamespaceAliases(commands: SlashCommand[], commandsDir: string): SlashCommand[] { + const rootCommands: SlashCommand[] = []; + const nestedCommands: SlashCommand[] = []; const aliases: SlashCommand[] = []; + for (const command of commands) { - const alias = getClaudeCommandNamespaceAlias(commandsDir, command.path); - if (alias === null) continue; - aliases.push({ ...command, name: alias }); + const relativeName = getClaudeRelativeCommandName(commandsDir, command.path); + if (!/[\\/]/.test(relativeName)) { + rootCommands.push(command); + continue; + } + + nestedCommands.push(command); + aliases.push({ ...command, name: relativeName.replace(/[\\/]+/g, ":") }); } - return aliases.length === 0 ? commands : [...commands, ...aliases]; + + return nestedCommands.length === 0 ? commands : [...rootCommands, ...nestedCommands, ...aliases]; } async function loadSlashCommands(ctx: LoadContext): Promise> { diff --git a/packages/coding-agent/test/discovery/claude-commands.test.ts b/packages/coding-agent/test/discovery/claude-commands.test.ts index 019d41b3e..77fc675ef 100644 --- a/packages/coding-agent/test/discovery/claude-commands.test.ts +++ b/packages/coding-agent/test/discovery/claude-commands.test.ts @@ -60,4 +60,23 @@ describe("Claude Code slash command discovery", () => { expect(names).toContain("audit"); expect(names).toContain("team:audit"); }); + test("keeps root commands ahead of nested basename duplicates", async () => { + const rootApply = path.join(project, ".claude", "commands", "apply.md"); + const nestedApply = path.join(project, ".claude", "commands", "agent", "apply.md"); + await writeFile(rootApply, "Root apply prompt\n"); + await writeFile(nestedApply, "Nested apply prompt\n"); + + const result = await loadCapability(slashCommandCapability.id, { + cwd: project, + providers: ["claude"], + }); + const apply = result.items.find(command => command.name === "apply"); + const agentApply = result.items.find(command => command.name === "agent:apply"); + + expect(result.warnings).toEqual([]); + expect(apply?.path).toBe(rootApply); + expect(apply?.content).toBe("Root apply prompt\n"); + expect(agentApply?.path).toBe(nestedApply); + expect(agentApply?.content).toBe("Nested apply prompt\n"); + }); });