refactor(agents): derive identity from directory name

Remove `name`, `canonicalId`, and `module` from agent sidecar manifests.
Both are now computed at build time: canonicalId = dirName,
name = dirName minus the `bmad-agent-` prefix.

- Remove agent exemption in manifest-generator collectSkills()
- Update manifest-generator collectAgents() to derive identity
- Update skill-manifest.js getCanonicalId() to return '' for agents
- Update bmad-artifacts.js to derive canonicalId from dir/filename
- Remove canonicalId-preferred branch from path-utils resolveSkillName()
- Update stale comments in _config-driven.js
- Strip name/canonicalId/module from 9 agent sidecar YAMLs
- Strip canonicalId from legacy multi-entry agent manifest
- Update test fixtures and assertions (216/216 pass)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Alex Verkhovsky 2026-03-16 09:48:31 -06:00
parent bed9052d49
commit 81bd715251
16 changed files with 104 additions and 93 deletions

View File

@ -1,5 +1,4 @@
type: agent type: agent
name: analyst
displayName: Mary displayName: Mary
title: Business Analyst title: Business Analyst
icon: "📊" icon: "📊"
@ -8,5 +7,3 @@ role: Strategic Business Analyst + Requirements Expert
identity: "Senior analyst with deep expertise in market research, competitive analysis, and requirements elicitation. Specializes in translating vague needs into actionable specs." identity: "Senior analyst with deep expertise in market research, competitive analysis, and requirements elicitation. Specializes in translating vague needs into actionable specs."
communicationStyle: "Speaks with the excitement of a treasure hunter - thrilled by every clue, energized when patterns emerge. Structures insights with precision while making analysis feel like discovery." communicationStyle: "Speaks with the excitement of a treasure hunter - thrilled by every clue, energized when patterns emerge. Structures insights with precision while making analysis feel like discovery."
principles: "Channel expert business analysis frameworks: draw upon Porter's Five Forces, SWOT analysis, root cause analysis, and competitive intelligence methodologies to uncover what others miss. Every business challenge has root causes waiting to be discovered. Ground findings in verifiable evidence. Articulate requirements with absolute precision. Ensure all stakeholder voices heard." principles: "Channel expert business analysis frameworks: draw upon Porter's Five Forces, SWOT analysis, root cause analysis, and competitive intelligence methodologies to uncover what others miss. Every business challenge has root causes waiting to be discovered. Ground findings in verifiable evidence. Articulate requirements with absolute precision. Ensure all stakeholder voices heard."
module: bmm
canonicalId: bmad-analyst

View File

@ -1,5 +1,4 @@
type: agent type: agent
name: architect
displayName: Winston displayName: Winston
title: Architect title: Architect
icon: "🏗️" icon: "🏗️"
@ -8,5 +7,3 @@ role: System Architect + Technical Design Leader
identity: "Senior architect with expertise in distributed systems, cloud infrastructure, and API design. Specializes in scalable patterns and technology selection." identity: "Senior architect with expertise in distributed systems, cloud infrastructure, and API design. Specializes in scalable patterns and technology selection."
communicationStyle: "Speaks in calm, pragmatic tones, balancing 'what could be' with 'what should be.'" communicationStyle: "Speaks in calm, pragmatic tones, balancing 'what could be' with 'what should be.'"
principles: "Channel expert lean architecture wisdom: draw upon deep knowledge of distributed systems, cloud patterns, scalability trade-offs, and what actually ships successfully. User journeys drive technical decisions. Embrace boring technology for stability. Design simple solutions that scale when needed. Developer productivity is architecture. Connect every decision to business value and user impact." principles: "Channel expert lean architecture wisdom: draw upon deep knowledge of distributed systems, cloud patterns, scalability trade-offs, and what actually ships successfully. User journeys drive technical decisions. Embrace boring technology for stability. Design simple solutions that scale when needed. Developer productivity is architecture. Connect every decision to business value and user impact."
module: bmm
canonicalId: bmad-architect

View File

@ -1,5 +1,4 @@
type: agent type: agent
name: dev
displayName: Amelia displayName: Amelia
title: Developer Agent title: Developer Agent
icon: "💻" icon: "💻"
@ -8,5 +7,3 @@ role: Senior Software Engineer
identity: "Executes approved stories with strict adherence to story details and team standards and practices." identity: "Executes approved stories with strict adherence to story details and team standards and practices."
communicationStyle: "Ultra-succinct. Speaks in file paths and AC IDs - every statement citable. No fluff, all precision." communicationStyle: "Ultra-succinct. Speaks in file paths and AC IDs - every statement citable. No fluff, all precision."
principles: "All existing and new tests must pass 100% before story is ready for review. Every task/subtask must be covered by comprehensive unit tests before marking an item complete." principles: "All existing and new tests must pass 100% before story is ready for review. Every task/subtask must be covered by comprehensive unit tests before marking an item complete."
module: bmm
canonicalId: bmad-dev

View File

@ -1,5 +1,4 @@
type: agent type: agent
name: pm
displayName: John displayName: John
title: Product Manager title: Product Manager
icon: "📋" icon: "📋"
@ -8,5 +7,3 @@ role: "Product Manager specializing in collaborative PRD creation through user i
identity: "Product management veteran with 8+ years launching B2B and consumer products. Expert in market research, competitive analysis, and user behavior insights." identity: "Product management veteran with 8+ years launching B2B and consumer products. Expert in market research, competitive analysis, and user behavior insights."
communicationStyle: "Asks 'WHY?' relentlessly like a detective on a case. Direct and data-sharp, cuts through fluff to what actually matters." communicationStyle: "Asks 'WHY?' relentlessly like a detective on a case. Direct and data-sharp, cuts through fluff to what actually matters."
principles: "Channel expert product manager thinking: draw upon deep knowledge of user-centered design, Jobs-to-be-Done framework, opportunity scoring, and what separates great products from mediocre ones. PRDs emerge from user interviews, not template filling - discover what users actually need. Ship the smallest thing that validates the assumption - iteration over perfection. Technical feasibility is a constraint, not the driver - user value first." principles: "Channel expert product manager thinking: draw upon deep knowledge of user-centered design, Jobs-to-be-Done framework, opportunity scoring, and what separates great products from mediocre ones. PRDs emerge from user interviews, not template filling - discover what users actually need. Ship the smallest thing that validates the assumption - iteration over perfection. Technical feasibility is a constraint, not the driver - user value first."
module: bmm
canonicalId: bmad-pm

View File

@ -1,5 +1,4 @@
type: agent type: agent
name: qa
displayName: Quinn displayName: Quinn
title: QA Engineer title: QA Engineer
icon: "🧪" icon: "🧪"
@ -8,5 +7,3 @@ role: QA Engineer
identity: "Pragmatic test automation engineer focused on rapid test coverage. Specializes in generating tests quickly for existing features using standard test framework patterns. Simpler, more direct approach than the advanced Test Architect module." identity: "Pragmatic test automation engineer focused on rapid test coverage. Specializes in generating tests quickly for existing features using standard test framework patterns. Simpler, more direct approach than the advanced Test Architect module."
communicationStyle: "Practical and straightforward. Gets tests written fast without overthinking. 'Ship it and iterate' mentality. Focuses on coverage first, optimization later." communicationStyle: "Practical and straightforward. Gets tests written fast without overthinking. 'Ship it and iterate' mentality. Focuses on coverage first, optimization later."
principles: "Generate API and E2E tests for implemented code. Tests should pass on first run." principles: "Generate API and E2E tests for implemented code. Tests should pass on first run."
module: bmm
canonicalId: bmad-qa

View File

@ -1,5 +1,4 @@
type: agent type: agent
name: quick-flow-solo-dev
displayName: Barry displayName: Barry
title: Quick Flow Solo Dev title: Quick Flow Solo Dev
icon: "🚀" icon: "🚀"
@ -8,5 +7,3 @@ role: Elite Full-Stack Developer + Quick Flow Specialist
identity: "Barry handles Quick Flow - from tech spec creation through implementation. Minimum ceremony, lean artifacts, ruthless efficiency." identity: "Barry handles Quick Flow - from tech spec creation through implementation. Minimum ceremony, lean artifacts, ruthless efficiency."
communicationStyle: "Direct, confident, and implementation-focused. Uses tech slang (e.g., refactor, patch, extract, spike) and gets straight to the point. No fluff, just results. Stays focused on the task at hand." communicationStyle: "Direct, confident, and implementation-focused. Uses tech slang (e.g., refactor, patch, extract, spike) and gets straight to the point. No fluff, just results. Stays focused on the task at hand."
principles: "Planning and execution are two sides of the same coin. Specs are for building, not bureaucracy. Code that ships is better than perfect code that doesn't." principles: "Planning and execution are two sides of the same coin. Specs are for building, not bureaucracy. Code that ships is better than perfect code that doesn't."
module: bmm
canonicalId: bmad-quick-flow-solo-dev

View File

@ -1,5 +1,4 @@
type: agent type: agent
name: sm
displayName: Bob displayName: Bob
title: Scrum Master title: Scrum Master
icon: "🏃" icon: "🏃"
@ -8,5 +7,3 @@ role: Technical Scrum Master + Story Preparation Specialist
identity: "Certified Scrum Master with deep technical background. Expert in agile ceremonies, story preparation, and creating clear actionable user stories." identity: "Certified Scrum Master with deep technical background. Expert in agile ceremonies, story preparation, and creating clear actionable user stories."
communicationStyle: "Crisp and checklist-driven. Every word has a purpose, every requirement crystal clear. Zero tolerance for ambiguity." communicationStyle: "Crisp and checklist-driven. Every word has a purpose, every requirement crystal clear. Zero tolerance for ambiguity."
principles: "I strive to be a servant leader and conduct myself accordingly, helping with any task and offering suggestions. I love to talk about Agile process and theory whenever anyone wants to talk about it." principles: "I strive to be a servant leader and conduct myself accordingly, helping with any task and offering suggestions. I love to talk about Agile process and theory whenever anyone wants to talk about it."
module: bmm
canonicalId: bmad-sm

View File

@ -1,5 +1,4 @@
type: agent type: agent
name: tech-writer
displayName: Paige displayName: Paige
title: Technical Writer title: Technical Writer
icon: "📚" icon: "📚"
@ -8,5 +7,3 @@ role: Technical Documentation Specialist + Knowledge Curator
identity: "Experienced technical writer expert in CommonMark, DITA, OpenAPI. Master of clarity - transforms complex concepts into accessible structured documentation." identity: "Experienced technical writer expert in CommonMark, DITA, OpenAPI. Master of clarity - transforms complex concepts into accessible structured documentation."
communicationStyle: "Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines." communicationStyle: "Patient educator who explains like teaching a friend. Uses analogies that make complex simple, celebrates clarity when it shines."
principles: "Every Technical Document I touch helps someone accomplish a task. Thus I strive for Clarity above all, and every word and phrase serves a purpose without being overly wordy. I believe a picture/diagram is worth 1000s of words and will include diagrams over drawn out text. I understand the intended audience or will clarify with the user so I know when to simplify vs when to be detailed." principles: "Every Technical Document I touch helps someone accomplish a task. Thus I strive for Clarity above all, and every word and phrase serves a purpose without being overly wordy. I believe a picture/diagram is worth 1000s of words and will include diagrams over drawn out text. I understand the intended audience or will clarify with the user so I know when to simplify vs when to be detailed."
module: bmm
canonicalId: bmad-tech-writer

View File

@ -1,5 +1,4 @@
type: agent type: agent
name: ux-designer
displayName: Sally displayName: Sally
title: UX Designer title: UX Designer
icon: "🎨" icon: "🎨"
@ -8,5 +7,3 @@ role: User Experience Designer + UI Specialist
identity: "Senior UX Designer with 7+ years creating intuitive experiences across web and mobile. Expert in user research, interaction design, AI-assisted tools." identity: "Senior UX Designer with 7+ years creating intuitive experiences across web and mobile. Expert in user research, interaction design, AI-assisted tools."
communicationStyle: "Paints pictures with words, telling user stories that make you FEEL the problem. Empathetic advocate with creative storytelling flair." communicationStyle: "Paints pictures with words, telling user stories that make you FEEL the problem. Empathetic advocate with creative storytelling flair."
principles: "Every decision serves genuine user needs. Start simple, evolve through feedback. Balance empathy with edge case attention. AI tools accelerate human-centered design. Data-informed but always creative." principles: "Every decision serves genuine user needs. Start simple, evolve through feedback. Balance empathy with edge case attention. AI tools accelerate human-centered design. Data-informed but always creative."
module: bmm
canonicalId: bmad-ux-designer

View File

@ -1,39 +1,31 @@
analyst.agent.yaml: analyst.agent.yaml:
canonicalId: bmad-analyst
type: agent type: agent
description: "Business Analyst for market research, competitive analysis, and requirements elicitation" description: "Business Analyst for market research, competitive analysis, and requirements elicitation"
architect.agent.yaml: architect.agent.yaml:
canonicalId: bmad-architect
type: agent type: agent
description: "Architect for distributed systems, cloud infrastructure, and API design" description: "Architect for distributed systems, cloud infrastructure, and API design"
dev.agent.yaml: dev.agent.yaml:
canonicalId: bmad-dev
type: agent type: agent
description: "Developer Agent for story execution, test-driven development, and code implementation" description: "Developer Agent for story execution, test-driven development, and code implementation"
pm.agent.yaml: pm.agent.yaml:
canonicalId: bmad-pm
type: agent type: agent
description: "Product Manager for PRD creation, requirements discovery, and stakeholder alignment" description: "Product Manager for PRD creation, requirements discovery, and stakeholder alignment"
qa.agent.yaml: qa.agent.yaml:
canonicalId: bmad-qa
type: agent type: agent
description: "QA Engineer for test automation, API testing, and E2E testing" description: "QA Engineer for test automation, API testing, and E2E testing"
quick-flow-solo-dev.agent.yaml: quick-flow-solo-dev.agent.yaml:
canonicalId: bmad-quick-flow-solo-dev
type: agent type: agent
description: "Quick Flow Solo Dev for rapid spec creation and lean implementation" description: "Quick Flow Solo Dev for rapid spec creation and lean implementation"
sm.agent.yaml: sm.agent.yaml:
canonicalId: bmad-sm
type: agent type: agent
description: "Scrum Master for sprint planning, story preparation, and agile ceremonies" description: "Scrum Master for sprint planning, story preparation, and agile ceremonies"
ux-designer.agent.yaml: ux-designer.agent.yaml:
canonicalId: bmad-ux-designer
type: agent type: agent
description: "UX Designer for user research, interaction design, and UI patterns" description: "UX Designer for user research, interaction design, and UI patterns"

View File

@ -71,8 +71,8 @@ async function createTestBmadFixture() {
await fs.ensureDir(path.join(fixtureDir, 'core', 'agents')); await fs.ensureDir(path.join(fixtureDir, 'core', 'agents'));
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-master.md'), minimalAgent); await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-master.md'), minimalAgent);
// Skill manifest so the installer uses 'bmad-master' as the canonical skill name // No canonicalId in manifest — identity is derived from the filename
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-skill-manifest.yaml'), 'bmad-master.md:\n canonicalId: bmad-master\n'); await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-skill-manifest.yaml'), 'bmad-master.md:\n type: agent\n');
// Minimal compiled agent for bmm module (tests use selectedModules: ['bmm']) // Minimal compiled agent for bmm module (tests use selectedModules: ['bmm'])
await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents')); await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents'));
@ -91,7 +91,7 @@ async function createSkillCollisionFixture() {
path.join(configDir, 'agent-manifest.csv'), path.join(configDir, 'agent-manifest.csv'),
[ [
'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path,canonicalId', 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path,canonicalId',
'"bmad-master","BMAD Master","","","","","","","","core","_bmad/core/agents/bmad-master.md","bmad-master"', '"bmad-master","BMAD Master","","","","","","","","core","_bmad/core/agents/bmad-master.md","bmad-agent-bmad-master"',
'', '',
].join('\n'), ].join('\n'),
); );
@ -247,7 +247,7 @@ async function runTests() {
assert(result.success === true, 'Windsurf setup succeeds against temp project'); assert(result.success === true, 'Windsurf setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.windsurf', 'skills', 'bmad-master', 'SKILL.md'); const skillFile = path.join(tempProjectDir, '.windsurf', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Windsurf install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile), 'Windsurf install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.windsurf', 'workflows'))), 'Windsurf setup removes legacy workflows dir'); assert(!(await fs.pathExists(path.join(tempProjectDir, '.windsurf', 'workflows'))), 'Windsurf setup removes legacy workflows dir');
@ -295,7 +295,7 @@ async function runTests() {
assert(result.success === true, 'Kiro setup succeeds against temp project'); assert(result.success === true, 'Kiro setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.kiro', 'skills', 'bmad-master', 'SKILL.md'); const skillFile = path.join(tempProjectDir, '.kiro', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Kiro install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile), 'Kiro install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.kiro', 'steering'))), 'Kiro setup removes legacy steering dir'); assert(!(await fs.pathExists(path.join(tempProjectDir, '.kiro', 'steering'))), 'Kiro setup removes legacy steering dir');
@ -343,7 +343,7 @@ async function runTests() {
assert(result.success === true, 'Antigravity setup succeeds against temp project'); assert(result.success === true, 'Antigravity setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.agent', 'skills', 'bmad-master', 'SKILL.md'); const skillFile = path.join(tempProjectDir, '.agent', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Antigravity install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile), 'Antigravity install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.agent', 'workflows'))), 'Antigravity setup removes legacy workflows dir'); assert(!(await fs.pathExists(path.join(tempProjectDir, '.agent', 'workflows'))), 'Antigravity setup removes legacy workflows dir');
@ -396,7 +396,7 @@ async function runTests() {
assert(result.success === true, 'Auggie setup succeeds against temp project'); assert(result.success === true, 'Auggie setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.augment', 'skills', 'bmad-master', 'SKILL.md'); const skillFile = path.join(tempProjectDir, '.augment', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Auggie install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile), 'Auggie install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.augment', 'commands'))), 'Auggie setup removes legacy commands dir'); assert(!(await fs.pathExists(path.join(tempProjectDir, '.augment', 'commands'))), 'Auggie setup removes legacy commands dir');
@ -457,7 +457,7 @@ async function runTests() {
assert(result.success === true, 'OpenCode setup succeeds against temp project'); assert(result.success === true, 'OpenCode setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.opencode', 'skills', 'bmad-master', 'SKILL.md'); const skillFile = path.join(tempProjectDir, '.opencode', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'OpenCode install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile), 'OpenCode install writes SKILL.md directory output');
for (const legacyDir of ['agents', 'commands', 'agent', 'command']) { for (const legacyDir of ['agents', 'commands', 'agent', 'command']) {
@ -511,13 +511,16 @@ async function runTests() {
assert(result9.success === true, 'Claude Code setup succeeds against temp project'); assert(result9.success === true, 'Claude Code setup succeeds against temp project');
const skillFile9 = path.join(tempProjectDir9, '.claude', 'skills', 'bmad-master', 'SKILL.md'); const skillFile9 = path.join(tempProjectDir9, '.claude', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile9), 'Claude Code install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile9), 'Claude Code install writes SKILL.md directory output');
// Verify name frontmatter matches directory name // Verify name frontmatter matches directory name
const skillContent9 = await fs.readFile(skillFile9, 'utf8'); const skillContent9 = await fs.readFile(skillFile9, 'utf8');
const nameMatch9 = skillContent9.match(/^name:\s*(.+)$/m); const nameMatch9 = skillContent9.match(/^name:\s*(.+)$/m);
assert(nameMatch9 && nameMatch9[1].trim() === 'bmad-master', 'Claude Code skill name frontmatter matches directory name exactly'); assert(
nameMatch9 && nameMatch9[1].trim() === 'bmad-agent-bmad-master',
'Claude Code skill name frontmatter matches directory name exactly',
);
assert(!(await fs.pathExists(legacyDir9)), 'Claude Code setup removes legacy commands dir'); assert(!(await fs.pathExists(legacyDir9)), 'Claude Code setup removes legacy commands dir');
@ -604,13 +607,16 @@ async function runTests() {
assert(result11.success === true, 'Codex setup succeeds against temp project'); assert(result11.success === true, 'Codex setup succeeds against temp project');
const skillFile11 = path.join(tempProjectDir11, '.agents', 'skills', 'bmad-master', 'SKILL.md'); const skillFile11 = path.join(tempProjectDir11, '.agents', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile11), 'Codex install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile11), 'Codex install writes SKILL.md directory output');
// Verify name frontmatter matches directory name // Verify name frontmatter matches directory name
const skillContent11 = await fs.readFile(skillFile11, 'utf8'); const skillContent11 = await fs.readFile(skillFile11, 'utf8');
const nameMatch11 = skillContent11.match(/^name:\s*(.+)$/m); const nameMatch11 = skillContent11.match(/^name:\s*(.+)$/m);
assert(nameMatch11 && nameMatch11[1].trim() === 'bmad-master', 'Codex skill name frontmatter matches directory name exactly'); assert(
nameMatch11 && nameMatch11[1].trim() === 'bmad-agent-bmad-master',
'Codex skill name frontmatter matches directory name exactly',
);
assert(!(await fs.pathExists(legacyDir11)), 'Codex setup removes legacy prompts dir'); assert(!(await fs.pathExists(legacyDir11)), 'Codex setup removes legacy prompts dir');
@ -694,13 +700,16 @@ async function runTests() {
assert(result13c.success === true, 'Cursor setup succeeds against temp project'); assert(result13c.success === true, 'Cursor setup succeeds against temp project');
const skillFile13c = path.join(tempProjectDir13c, '.cursor', 'skills', 'bmad-master', 'SKILL.md'); const skillFile13c = path.join(tempProjectDir13c, '.cursor', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile13c), 'Cursor install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile13c), 'Cursor install writes SKILL.md directory output');
// Verify name frontmatter matches directory name // Verify name frontmatter matches directory name
const skillContent13c = await fs.readFile(skillFile13c, 'utf8'); const skillContent13c = await fs.readFile(skillFile13c, 'utf8');
const nameMatch13c = skillContent13c.match(/^name:\s*(.+)$/m); const nameMatch13c = skillContent13c.match(/^name:\s*(.+)$/m);
assert(nameMatch13c && nameMatch13c[1].trim() === 'bmad-master', 'Cursor skill name frontmatter matches directory name exactly'); assert(
nameMatch13c && nameMatch13c[1].trim() === 'bmad-agent-bmad-master',
'Cursor skill name frontmatter matches directory name exactly',
);
assert(!(await fs.pathExists(legacyDir13c)), 'Cursor setup removes legacy commands dir'); assert(!(await fs.pathExists(legacyDir13c)), 'Cursor setup removes legacy commands dir');
@ -747,14 +756,14 @@ async function runTests() {
assert(result13.success === true, 'Roo setup succeeds against temp project'); assert(result13.success === true, 'Roo setup succeeds against temp project');
const skillFile13 = path.join(tempProjectDir13, '.roo', 'skills', 'bmad-master', 'SKILL.md'); const skillFile13 = path.join(tempProjectDir13, '.roo', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile13), 'Roo install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile13), 'Roo install writes SKILL.md directory output');
// Verify name frontmatter matches directory name (Roo constraint: lowercase alphanumeric + hyphens) // Verify name frontmatter matches directory name (Roo constraint: lowercase alphanumeric + hyphens)
const skillContent13 = await fs.readFile(skillFile13, 'utf8'); const skillContent13 = await fs.readFile(skillFile13, 'utf8');
const nameMatch13 = skillContent13.match(/^name:\s*(.+)$/m); const nameMatch13 = skillContent13.match(/^name:\s*(.+)$/m);
assert( assert(
nameMatch13 && nameMatch13[1].trim() === 'bmad-master', nameMatch13 && nameMatch13[1].trim() === 'bmad-agent-bmad-master',
'Roo skill name frontmatter matches directory name exactly (lowercase alphanumeric + hyphens)', 'Roo skill name frontmatter matches directory name exactly (lowercase alphanumeric + hyphens)',
); );
@ -871,13 +880,16 @@ async function runTests() {
assert(result17.success === true, 'GitHub Copilot setup succeeds against temp project'); assert(result17.success === true, 'GitHub Copilot setup succeeds against temp project');
const skillFile17 = path.join(tempProjectDir17, '.github', 'skills', 'bmad-master', 'SKILL.md'); const skillFile17 = path.join(tempProjectDir17, '.github', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile17), 'GitHub Copilot install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile17), 'GitHub Copilot install writes SKILL.md directory output');
// Verify name frontmatter matches directory name // Verify name frontmatter matches directory name
const skillContent17 = await fs.readFile(skillFile17, 'utf8'); const skillContent17 = await fs.readFile(skillFile17, 'utf8');
const nameMatch17 = skillContent17.match(/^name:\s*(.+)$/m); const nameMatch17 = skillContent17.match(/^name:\s*(.+)$/m);
assert(nameMatch17 && nameMatch17[1].trim() === 'bmad-master', 'GitHub Copilot skill name frontmatter matches directory name exactly'); assert(
nameMatch17 && nameMatch17[1].trim() === 'bmad-agent-bmad-master',
'GitHub Copilot skill name frontmatter matches directory name exactly',
);
assert(!(await fs.pathExists(legacyAgentsDir17)), 'GitHub Copilot setup removes legacy agents dir'); assert(!(await fs.pathExists(legacyAgentsDir17)), 'GitHub Copilot setup removes legacy agents dir');
@ -937,13 +949,16 @@ async function runTests() {
assert(result18.success === true, 'Cline setup succeeds against temp project'); assert(result18.success === true, 'Cline setup succeeds against temp project');
const skillFile18 = path.join(tempProjectDir18, '.cline', 'skills', 'bmad-master', 'SKILL.md'); const skillFile18 = path.join(tempProjectDir18, '.cline', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile18), 'Cline install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile18), 'Cline install writes SKILL.md directory output');
// Verify name frontmatter matches directory name // Verify name frontmatter matches directory name
const skillContent18 = await fs.readFile(skillFile18, 'utf8'); const skillContent18 = await fs.readFile(skillFile18, 'utf8');
const nameMatch18 = skillContent18.match(/^name:\s*(.+)$/m); const nameMatch18 = skillContent18.match(/^name:\s*(.+)$/m);
assert(nameMatch18 && nameMatch18[1].trim() === 'bmad-master', 'Cline skill name frontmatter matches directory name exactly'); assert(
nameMatch18 && nameMatch18[1].trim() === 'bmad-agent-bmad-master',
'Cline skill name frontmatter matches directory name exactly',
);
assert(!(await fs.pathExists(path.join(tempProjectDir18, '.clinerules', 'workflows'))), 'Cline setup removes legacy workflows dir'); assert(!(await fs.pathExists(path.join(tempProjectDir18, '.clinerules', 'workflows'))), 'Cline setup removes legacy workflows dir');
@ -999,12 +1014,15 @@ async function runTests() {
assert(result19.success === true, 'CodeBuddy setup succeeds against temp project'); assert(result19.success === true, 'CodeBuddy setup succeeds against temp project');
const skillFile19 = path.join(tempProjectDir19, '.codebuddy', 'skills', 'bmad-master', 'SKILL.md'); const skillFile19 = path.join(tempProjectDir19, '.codebuddy', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile19), 'CodeBuddy install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile19), 'CodeBuddy install writes SKILL.md directory output');
const skillContent19 = await fs.readFile(skillFile19, 'utf8'); const skillContent19 = await fs.readFile(skillFile19, 'utf8');
const nameMatch19 = skillContent19.match(/^name:\s*(.+)$/m); const nameMatch19 = skillContent19.match(/^name:\s*(.+)$/m);
assert(nameMatch19 && nameMatch19[1].trim() === 'bmad-master', 'CodeBuddy skill name frontmatter matches directory name exactly'); assert(
nameMatch19 && nameMatch19[1].trim() === 'bmad-agent-bmad-master',
'CodeBuddy skill name frontmatter matches directory name exactly',
);
assert(!(await fs.pathExists(path.join(tempProjectDir19, '.codebuddy', 'commands'))), 'CodeBuddy setup removes legacy commands dir'); assert(!(await fs.pathExists(path.join(tempProjectDir19, '.codebuddy', 'commands'))), 'CodeBuddy setup removes legacy commands dir');
@ -1059,12 +1077,15 @@ async function runTests() {
assert(result20.success === true, 'Crush setup succeeds against temp project'); assert(result20.success === true, 'Crush setup succeeds against temp project');
const skillFile20 = path.join(tempProjectDir20, '.crush', 'skills', 'bmad-master', 'SKILL.md'); const skillFile20 = path.join(tempProjectDir20, '.crush', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile20), 'Crush install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile20), 'Crush install writes SKILL.md directory output');
const skillContent20 = await fs.readFile(skillFile20, 'utf8'); const skillContent20 = await fs.readFile(skillFile20, 'utf8');
const nameMatch20 = skillContent20.match(/^name:\s*(.+)$/m); const nameMatch20 = skillContent20.match(/^name:\s*(.+)$/m);
assert(nameMatch20 && nameMatch20[1].trim() === 'bmad-master', 'Crush skill name frontmatter matches directory name exactly'); assert(
nameMatch20 && nameMatch20[1].trim() === 'bmad-agent-bmad-master',
'Crush skill name frontmatter matches directory name exactly',
);
assert(!(await fs.pathExists(path.join(tempProjectDir20, '.crush', 'commands'))), 'Crush setup removes legacy commands dir'); assert(!(await fs.pathExists(path.join(tempProjectDir20, '.crush', 'commands'))), 'Crush setup removes legacy commands dir');
@ -1118,12 +1139,12 @@ async function runTests() {
assert(result21.success === true, 'Trae setup succeeds against temp project'); assert(result21.success === true, 'Trae setup succeeds against temp project');
const skillFile21 = path.join(tempProjectDir21, '.trae', 'skills', 'bmad-master', 'SKILL.md'); const skillFile21 = path.join(tempProjectDir21, '.trae', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile21), 'Trae install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile21), 'Trae install writes SKILL.md directory output');
const skillContent21 = await fs.readFile(skillFile21, 'utf8'); const skillContent21 = await fs.readFile(skillFile21, 'utf8');
const nameMatch21 = skillContent21.match(/^name:\s*(.+)$/m); const nameMatch21 = skillContent21.match(/^name:\s*(.+)$/m);
assert(nameMatch21 && nameMatch21[1].trim() === 'bmad-master', 'Trae skill name frontmatter matches directory name exactly'); assert(nameMatch21 && nameMatch21[1].trim() === 'bmad-agent-bmad-master', 'Trae skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir21, '.trae', 'rules'))), 'Trae setup removes legacy rules dir'); assert(!(await fs.pathExists(path.join(tempProjectDir21, '.trae', 'rules'))), 'Trae setup removes legacy rules dir');
@ -1235,12 +1256,15 @@ async function runTests() {
assert(result23.success === true, 'Gemini setup succeeds against temp project'); assert(result23.success === true, 'Gemini setup succeeds against temp project');
const skillFile23 = path.join(tempProjectDir23, '.gemini', 'skills', 'bmad-master', 'SKILL.md'); const skillFile23 = path.join(tempProjectDir23, '.gemini', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile23), 'Gemini install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile23), 'Gemini install writes SKILL.md directory output');
const skillContent23 = await fs.readFile(skillFile23, 'utf8'); const skillContent23 = await fs.readFile(skillFile23, 'utf8');
const nameMatch23 = skillContent23.match(/^name:\s*(.+)$/m); const nameMatch23 = skillContent23.match(/^name:\s*(.+)$/m);
assert(nameMatch23 && nameMatch23[1].trim() === 'bmad-master', 'Gemini skill name frontmatter matches directory name exactly'); assert(
nameMatch23 && nameMatch23[1].trim() === 'bmad-agent-bmad-master',
'Gemini skill name frontmatter matches directory name exactly',
);
assert(!(await fs.pathExists(path.join(tempProjectDir23, '.gemini', 'commands'))), 'Gemini setup removes legacy commands dir'); assert(!(await fs.pathExists(path.join(tempProjectDir23, '.gemini', 'commands'))), 'Gemini setup removes legacy commands dir');
@ -1292,13 +1316,16 @@ async function runTests() {
assert(result24.success === true, 'iFlow setup succeeds against temp project'); assert(result24.success === true, 'iFlow setup succeeds against temp project');
const skillFile24 = path.join(tempProjectDir24, '.iflow', 'skills', 'bmad-master', 'SKILL.md'); const skillFile24 = path.join(tempProjectDir24, '.iflow', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile24), 'iFlow install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile24), 'iFlow install writes SKILL.md directory output');
// Verify name frontmatter matches directory name // Verify name frontmatter matches directory name
const skillContent24 = await fs.readFile(skillFile24, 'utf8'); const skillContent24 = await fs.readFile(skillFile24, 'utf8');
const nameMatch24 = skillContent24.match(/^name:\s*(.+)$/m); const nameMatch24 = skillContent24.match(/^name:\s*(.+)$/m);
assert(nameMatch24 && nameMatch24[1].trim() === 'bmad-master', 'iFlow skill name frontmatter matches directory name exactly'); assert(
nameMatch24 && nameMatch24[1].trim() === 'bmad-agent-bmad-master',
'iFlow skill name frontmatter matches directory name exactly',
);
assert(!(await fs.pathExists(path.join(tempProjectDir24, '.iflow', 'commands'))), 'iFlow setup removes legacy commands dir'); assert(!(await fs.pathExists(path.join(tempProjectDir24, '.iflow', 'commands'))), 'iFlow setup removes legacy commands dir');
@ -1342,13 +1369,16 @@ async function runTests() {
assert(result25.success === true, 'QwenCoder setup succeeds against temp project'); assert(result25.success === true, 'QwenCoder setup succeeds against temp project');
const skillFile25 = path.join(tempProjectDir25, '.qwen', 'skills', 'bmad-master', 'SKILL.md'); const skillFile25 = path.join(tempProjectDir25, '.qwen', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile25), 'QwenCoder install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile25), 'QwenCoder install writes SKILL.md directory output');
// Verify name frontmatter matches directory name // Verify name frontmatter matches directory name
const skillContent25 = await fs.readFile(skillFile25, 'utf8'); const skillContent25 = await fs.readFile(skillFile25, 'utf8');
const nameMatch25 = skillContent25.match(/^name:\s*(.+)$/m); const nameMatch25 = skillContent25.match(/^name:\s*(.+)$/m);
assert(nameMatch25 && nameMatch25[1].trim() === 'bmad-master', 'QwenCoder skill name frontmatter matches directory name exactly'); assert(
nameMatch25 && nameMatch25[1].trim() === 'bmad-agent-bmad-master',
'QwenCoder skill name frontmatter matches directory name exactly',
);
assert(!(await fs.pathExists(path.join(tempProjectDir25, '.qwen', 'commands'))), 'QwenCoder setup removes legacy commands dir'); assert(!(await fs.pathExists(path.join(tempProjectDir25, '.qwen', 'commands'))), 'QwenCoder setup removes legacy commands dir');
@ -1403,13 +1433,16 @@ async function runTests() {
assert(result26.success === true, 'Rovo Dev setup succeeds against temp project'); assert(result26.success === true, 'Rovo Dev setup succeeds against temp project');
const skillFile26 = path.join(tempProjectDir26, '.rovodev', 'skills', 'bmad-master', 'SKILL.md'); const skillFile26 = path.join(tempProjectDir26, '.rovodev', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile26), 'Rovo Dev install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile26), 'Rovo Dev install writes SKILL.md directory output');
// Verify name frontmatter matches directory name // Verify name frontmatter matches directory name
const skillContent26 = await fs.readFile(skillFile26, 'utf8'); const skillContent26 = await fs.readFile(skillFile26, 'utf8');
const nameMatch26 = skillContent26.match(/^name:\s*(.+)$/m); const nameMatch26 = skillContent26.match(/^name:\s*(.+)$/m);
assert(nameMatch26 && nameMatch26[1].trim() === 'bmad-master', 'Rovo Dev skill name frontmatter matches directory name exactly'); assert(
nameMatch26 && nameMatch26[1].trim() === 'bmad-agent-bmad-master',
'Rovo Dev skill name frontmatter matches directory name exactly',
);
assert(!(await fs.pathExists(path.join(tempProjectDir26, '.rovodev', 'workflows'))), 'Rovo Dev setup removes legacy workflows dir'); assert(!(await fs.pathExists(path.join(tempProjectDir26, '.rovodev', 'workflows'))), 'Rovo Dev setup removes legacy workflows dir');
@ -1480,7 +1513,7 @@ async function runTests() {
assert(osContent27.includes('OS skill content'), 'bmad-os-review-pr skill content is unchanged'); assert(osContent27.includes('OS skill content'), 'bmad-os-review-pr skill content is unchanged');
// Regular bmad skill should have been replaced by fresh install // Regular bmad skill should have been replaced by fresh install
const newSkillFile27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-master', 'SKILL.md'); const newSkillFile27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(newSkillFile27), 'Fresh bmad skills are installed alongside preserved bmad-os-* skills'); assert(await fs.pathExists(newSkillFile27), 'Fresh bmad skills are installed alongside preserved bmad-os-* skills');
// Stale non-bmad-os skill must have been removed by cleanup // Stale non-bmad-os skill must have been removed by cleanup
@ -1538,7 +1571,7 @@ async function runTests() {
const detectedAfter28 = await ideManager28.detectInstalledIdes(tempProjectDir28); const detectedAfter28 = await ideManager28.detectInstalledIdes(tempProjectDir28);
assert(detectedAfter28.includes('pi'), 'Pi is detected after install'); assert(detectedAfter28.includes('pi'), 'Pi is detected after install');
const skillFile28 = path.join(tempProjectDir28, '.pi', 'skills', 'bmad-master', 'SKILL.md'); const skillFile28 = path.join(tempProjectDir28, '.pi', 'skills', 'bmad-agent-bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile28), 'Pi install writes SKILL.md directory output'); assert(await fs.pathExists(skillFile28), 'Pi install writes SKILL.md directory output');
// Parse YAML frontmatter between --- markers // Parse YAML frontmatter between --- markers
@ -1551,7 +1584,7 @@ async function runTests() {
// Verify name in frontmatter matches directory name // Verify name in frontmatter matches directory name
const fmName28 = frontmatter28.match(/^name:\s*(.+)$/m); const fmName28 = frontmatter28.match(/^name:\s*(.+)$/m);
assert(fmName28 && fmName28[1].trim() === 'bmad-master', 'Pi skill name frontmatter matches directory name exactly'); assert(fmName28 && fmName28[1].trim() === 'bmad-agent-bmad-master', 'Pi skill name frontmatter matches directory name exactly');
// Verify description exists and is non-empty // Verify description exists and is non-empty
const fmDesc28 = frontmatter28.match(/^description:\s*(.+)$/m); const fmDesc28 = frontmatter28.match(/^description:\s*(.+)$/m);

View File

@ -191,8 +191,7 @@ class ManifestGenerator {
: `${this.bmadFolderName}/${moduleName}/${skillFile}`; : `${this.bmadFolderName}/${moduleName}/${skillFile}`;
// Skills derive canonicalId from directory name — never from manifest // Skills derive canonicalId from directory name — never from manifest
// (agent-type skills legitimately use canonicalId for agent-manifest mapping, so skip warning) if (manifest && manifest.__single && manifest.__single.canonicalId) {
if (manifest && manifest.__single && manifest.__single.canonicalId && artifactType !== 'agent') {
console.warn( console.warn(
`Warning: Skill manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for skills (directory name is the canonical ID)`, `Warning: Skill manifest at ${dir}/bmad-skill-manifest.yaml contains canonicalId — this field is ignored for skills (directory name is the canonical ID)`,
); );
@ -516,9 +515,13 @@ class ManifestGenerator {
? `${this.bmadFolderName}/core/agents/${dirRelativePath}` ? `${this.bmadFolderName}/core/agents/${dirRelativePath}`
: `${this.bmadFolderName}/${moduleName}/agents/${dirRelativePath}`; : `${this.bmadFolderName}/${moduleName}/agents/${dirRelativePath}`;
// Derive identity from directory name — never from manifest YAML
const derivedName = entry.name.replace(/^bmad-agent-/, '');
const derivedCanonicalId = entry.name;
agents.push({ agents.push({
name: m.name || entry.name, name: derivedName,
displayName: m.displayName || m.name || entry.name, displayName: m.displayName || derivedName,
title: m.title || '', title: m.title || '',
icon: m.icon || '', icon: m.icon || '',
capabilities: m.capabilities ? this.cleanForCSV(m.capabilities) : '', capabilities: m.capabilities ? this.cleanForCSV(m.capabilities) : '',
@ -526,14 +529,14 @@ class ManifestGenerator {
identity: m.identity ? this.cleanForCSV(m.identity) : '', identity: m.identity ? this.cleanForCSV(m.identity) : '',
communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '', communicationStyle: m.communicationStyle ? this.cleanForCSV(m.communicationStyle) : '',
principles: m.principles ? this.cleanForCSV(m.principles) : '', principles: m.principles ? this.cleanForCSV(m.principles) : '',
module: m.module || moduleName, module: moduleName,
path: installPath, path: installPath,
canonicalId: m.canonicalId || '', canonicalId: derivedCanonicalId,
}); });
this.files.push({ this.files.push({
type: 'agent', type: 'agent',
name: m.name || entry.name, name: derivedName,
module: moduleName, module: moduleName,
path: installPath, path: installPath,
}); });

View File

@ -487,7 +487,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
async writeSkillFile(targetPath, artifact, content) { async writeSkillFile(targetPath, artifact, content) {
const { resolveSkillName } = require('./shared/path-utils'); const { resolveSkillName } = require('./shared/path-utils');
// Get the skill name (prefers canonicalId, falls back to path-derived) and remove .md // Get the skill name (path-derived) and remove .md
const flatName = resolveSkillName(artifact); const flatName = resolveSkillName(artifact);
const skillName = path.basename(flatName.replace(/\.md$/, '')); const skillName = path.basename(flatName.replace(/\.md$/, ''));
@ -612,7 +612,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
const { resolveSkillName } = require('./shared/path-utils'); const { resolveSkillName } = require('./shared/path-utils');
// Reuse central logic to ensure consistent naming conventions // Reuse central logic to ensure consistent naming conventions
// Prefers canonicalId from manifest when available, falls back to path-derived name // Uses path-derived name via toDashPath()
const standardName = resolveSkillName(artifact); const standardName = resolveSkillName(artifact);
// Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md) // Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md)

View File

@ -50,7 +50,8 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) {
path: filePath, path: filePath,
name: file.replace('.md', ''), name: file.replace('.md', ''),
module: 'standalone', // Mark as standalone agent module: 'standalone', // Mark as standalone agent
canonicalId: getCanonicalId(skillManifest, file), canonicalId: agentDir.name, // Derive from directory name
relativePath: `agents/${agentDir.name}/${file}`,
}); });
} }
} }
@ -128,7 +129,7 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') {
name: entry.name.replace('.md', ''), name: entry.name.replace('.md', ''),
module: moduleName, module: moduleName,
relativePath: newRelativePath, // Keep the .md extension for the full path relativePath: newRelativePath, // Keep the .md extension for the full path
canonicalId: getCanonicalId(skillManifest, entry.name), canonicalId: entry.name.replace('.md', ''), // Derive from filename
}); });
} }
} }

View File

@ -315,16 +315,12 @@ function parseUnderscoreName(filename) {
/** /**
* Resolve the skill name for an artifact. * Resolve the skill name for an artifact.
* Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available, * Always uses path derivation via toDashPath().
* falling back to the path-derived name from toDashPath().
* *
* @param {Object} artifact - Artifact object (must have relativePath; may have canonicalId) * @param {Object} artifact - Artifact object (must have relativePath)
* @returns {string} Filename like 'bmad-create-prd.md' or 'bmad-agent-bmm-pm.md' * @returns {string} Filename like 'bmad-create-prd.md' or 'bmad-agent-bmm-pm.md'
*/ */
function resolveSkillName(artifact) { function resolveSkillName(artifact) {
if (artifact.canonicalId) {
return `${artifact.canonicalId}.md`;
}
return toDashPath(artifact.relativePath); return toDashPath(artifact.relativePath);
} }

View File

@ -33,15 +33,28 @@ async function loadSkillManifest(dirPath) {
function getCanonicalId(manifest, filename) { function getCanonicalId(manifest, filename) {
if (!manifest) return ''; if (!manifest) return '';
// Single-entry manifest applies to all files in the directory // Single-entry manifest applies to all files in the directory
if (manifest.__single) return manifest.__single.canonicalId || ''; // Agent-type manifests derive canonicalId from directory name — never from YAML
if (manifest.__single) {
if (manifest.__single.type === 'agent') return '';
return manifest.__single.canonicalId || '';
}
// Multi-entry: look up by filename directly // Multi-entry: look up by filename directly
if (manifest[filename]) return manifest[filename].canonicalId || ''; if (manifest[filename]) {
if (manifest[filename].type === 'agent') return '';
return manifest[filename].canonicalId || '';
}
// Fallback: try alternate extensions for compiled files // Fallback: try alternate extensions for compiled files
const baseName = filename.replace(/\.(md|xml)$/i, ''); const baseName = filename.replace(/\.(md|xml)$/i, '');
const agentKey = `${baseName}.agent.yaml`; const agentKey = `${baseName}.agent.yaml`;
if (manifest[agentKey]) return manifest[agentKey].canonicalId || ''; if (manifest[agentKey]) {
if (manifest[agentKey].type === 'agent') return '';
return manifest[agentKey].canonicalId || '';
}
const xmlKey = `${baseName}.xml`; const xmlKey = `${baseName}.xml`;
if (manifest[xmlKey]) return manifest[xmlKey].canonicalId || ''; if (manifest[xmlKey]) {
if (manifest[xmlKey].type === 'agent') return '';
return manifest[xmlKey].canonicalId || '';
}
return ''; return '';
} }