From 09e6debb4b72b0b34748f127988f254685d7fec9 Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Fri, 6 Mar 2026 01:04:06 -0700 Subject: [PATCH] feat(skills): add canonical bmad- naming via skill manifests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add bmad-skill-manifest.yaml sidecars to all 38 capabilities (tasks, agents, workflows) declaring canonicalId as the single source of truth for skill names. Update Claude Code and Codex installers to prefer canonicalId over path-derived names, with graceful fallback. - 24 manifest files covering 38 capabilities - New shared skill-manifest.js utility for manifest loading - resolveSkillName() in path-utils.js bridges manifest → installer - All command generators propagate canonicalId through CSV manifests - Drops bmm module prefix from all user-facing skill names Co-Authored-By: Claude Opus 4.6 --- src/bmm/agents/bmad-skill-manifest.yaml | 39 ++++++++++++++ .../tech-writer/bmad-skill-manifest.yaml | 3 ++ .../bmad-skill-manifest.yaml | 3 ++ .../create-ux-design/bmad-skill-manifest.yaml | 3 ++ .../bmad-skill-manifest.yaml | 3 ++ .../bmad-skill-manifest.yaml | 3 ++ .../bmad-skill-manifest.yaml | 3 ++ .../code-review/bmad-skill-manifest.yaml | 3 ++ .../correct-course/bmad-skill-manifest.yaml | 3 ++ .../create-story/bmad-skill-manifest.yaml | 3 ++ .../dev-story/bmad-skill-manifest.yaml | 3 ++ .../retrospective/bmad-skill-manifest.yaml | 3 ++ .../sprint-planning/bmad-skill-manifest.yaml | 3 ++ .../sprint-status/bmad-skill-manifest.yaml | 3 ++ .../bmad-skill-manifest.yaml | 3 ++ .../quick-dev/bmad-skill-manifest.yaml | 3 ++ .../quick-spec/bmad-skill-manifest.yaml | 3 ++ .../document-project/bmad-skill-manifest.yaml | 3 ++ .../bmad-skill-manifest.yaml | 3 ++ .../bmad-skill-manifest.yaml | 3 ++ src/core/agents/bmad-skill-manifest.yaml | 3 ++ src/core/tasks/bmad-skill-manifest.yaml | 39 ++++++++++++++ .../brainstorming/bmad-skill-manifest.yaml | 3 ++ .../party-mode/bmad-skill-manifest.yaml | 3 ++ .../installers/lib/core/manifest-generator.js | 54 +++++++++++++++---- .../cli/installers/lib/ide/_config-driven.js | 5 +- tools/cli/installers/lib/ide/codex.js | 7 +-- .../lib/ide/shared/agent-command-generator.js | 1 + .../lib/ide/shared/bmad-artifacts.js | 7 +++ .../installers/lib/ide/shared/path-utils.js | 16 ++++++ .../lib/ide/shared/skill-manifest.js | 48 +++++++++++++++++ .../ide/shared/task-tool-command-generator.js | 2 + .../ide/shared/workflow-command-generator.js | 1 + 33 files changed, 271 insertions(+), 14 deletions(-) create mode 100644 src/bmm/agents/bmad-skill-manifest.yaml create mode 100644 src/bmm/agents/tech-writer/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/1-analysis/create-product-brief/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/2-plan-workflows/create-ux-design/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/3-solutioning/check-implementation-readiness/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/3-solutioning/create-architecture/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/3-solutioning/create-epics-and-stories/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/4-implementation/code-review/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/4-implementation/correct-course/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/4-implementation/create-story/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/4-implementation/dev-story/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/4-implementation/retrospective/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/4-implementation/sprint-planning/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/4-implementation/sprint-status/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/bmad-quick-flow/quick-dev/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/bmad-quick-flow/quick-spec/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/document-project/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/generate-project-context/bmad-skill-manifest.yaml create mode 100644 src/bmm/workflows/qa-generate-e2e-tests/bmad-skill-manifest.yaml create mode 100644 src/core/agents/bmad-skill-manifest.yaml create mode 100644 src/core/tasks/bmad-skill-manifest.yaml create mode 100644 src/core/workflows/brainstorming/bmad-skill-manifest.yaml create mode 100644 src/core/workflows/party-mode/bmad-skill-manifest.yaml create mode 100644 tools/cli/installers/lib/ide/shared/skill-manifest.js diff --git a/src/bmm/agents/bmad-skill-manifest.yaml b/src/bmm/agents/bmad-skill-manifest.yaml new file mode 100644 index 000000000..2f3930de8 --- /dev/null +++ b/src/bmm/agents/bmad-skill-manifest.yaml @@ -0,0 +1,39 @@ +analyst.agent.yaml: + canonicalId: bmad-analyst + type: agent + description: "Business Analyst for market research, competitive analysis, and requirements elicitation" + +architect.agent.yaml: + canonicalId: bmad-architect + type: agent + description: "Architect for distributed systems, cloud infrastructure, and API design" + +dev.agent.yaml: + canonicalId: bmad-dev + type: agent + description: "Developer Agent for story execution, test-driven development, and code implementation" + +pm.agent.yaml: + canonicalId: bmad-pm + type: agent + description: "Product Manager for PRD creation, requirements discovery, and stakeholder alignment" + +qa.agent.yaml: + canonicalId: bmad-qa + type: agent + description: "QA Engineer for test automation, API testing, and E2E testing" + +quick-flow-solo-dev.agent.yaml: + canonicalId: bmad-quick-flow-solo-dev + type: agent + description: "Quick Flow Solo Dev for rapid spec creation and lean implementation" + +sm.agent.yaml: + canonicalId: bmad-sm + type: agent + description: "Scrum Master for sprint planning, story preparation, and agile ceremonies" + +ux-designer.agent.yaml: + canonicalId: bmad-ux-designer + type: agent + description: "UX Designer for user research, interaction design, and UI patterns" diff --git a/src/bmm/agents/tech-writer/bmad-skill-manifest.yaml b/src/bmm/agents/tech-writer/bmad-skill-manifest.yaml new file mode 100644 index 000000000..78aaa63eb --- /dev/null +++ b/src/bmm/agents/tech-writer/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-tech-writer +type: agent +description: "Technical Writer for documentation, Mermaid diagrams, and standards compliance" diff --git a/src/bmm/workflows/1-analysis/create-product-brief/bmad-skill-manifest.yaml b/src/bmm/workflows/1-analysis/create-product-brief/bmad-skill-manifest.yaml new file mode 100644 index 000000000..cb3969a6e --- /dev/null +++ b/src/bmm/workflows/1-analysis/create-product-brief/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-create-product-brief +type: workflow +description: "Create product brief through collaborative discovery" diff --git a/src/bmm/workflows/2-plan-workflows/create-ux-design/bmad-skill-manifest.yaml b/src/bmm/workflows/2-plan-workflows/create-ux-design/bmad-skill-manifest.yaml new file mode 100644 index 000000000..f0b8a250f --- /dev/null +++ b/src/bmm/workflows/2-plan-workflows/create-ux-design/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-create-ux-design +type: workflow +description: "Plan UX patterns and design specifications" diff --git a/src/bmm/workflows/3-solutioning/check-implementation-readiness/bmad-skill-manifest.yaml b/src/bmm/workflows/3-solutioning/check-implementation-readiness/bmad-skill-manifest.yaml new file mode 100644 index 000000000..3040413b8 --- /dev/null +++ b/src/bmm/workflows/3-solutioning/check-implementation-readiness/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-check-implementation-readiness +type: workflow +description: "Validate PRD, UX, Architecture and Epics specs are complete" diff --git a/src/bmm/workflows/3-solutioning/create-architecture/bmad-skill-manifest.yaml b/src/bmm/workflows/3-solutioning/create-architecture/bmad-skill-manifest.yaml new file mode 100644 index 000000000..6b35ce8e7 --- /dev/null +++ b/src/bmm/workflows/3-solutioning/create-architecture/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-create-architecture +type: workflow +description: "Create architecture solution design decisions for AI agent consistency" diff --git a/src/bmm/workflows/3-solutioning/create-epics-and-stories/bmad-skill-manifest.yaml b/src/bmm/workflows/3-solutioning/create-epics-and-stories/bmad-skill-manifest.yaml new file mode 100644 index 000000000..92b343dd9 --- /dev/null +++ b/src/bmm/workflows/3-solutioning/create-epics-and-stories/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-create-epics-and-stories +type: workflow +description: "Break requirements into epics and user stories" diff --git a/src/bmm/workflows/4-implementation/code-review/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/code-review/bmad-skill-manifest.yaml new file mode 100644 index 000000000..6b1589a4a --- /dev/null +++ b/src/bmm/workflows/4-implementation/code-review/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-code-review +type: workflow +description: "Perform adversarial code review finding specific issues" diff --git a/src/bmm/workflows/4-implementation/correct-course/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/correct-course/bmad-skill-manifest.yaml new file mode 100644 index 000000000..6a95bd4a7 --- /dev/null +++ b/src/bmm/workflows/4-implementation/correct-course/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-correct-course +type: workflow +description: "Manage significant changes during sprint execution" diff --git a/src/bmm/workflows/4-implementation/create-story/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/create-story/bmad-skill-manifest.yaml new file mode 100644 index 000000000..13f0beb24 --- /dev/null +++ b/src/bmm/workflows/4-implementation/create-story/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-create-story +type: workflow +description: "Creates a dedicated story file with all the context needed for implementation" diff --git a/src/bmm/workflows/4-implementation/dev-story/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/dev-story/bmad-skill-manifest.yaml new file mode 100644 index 000000000..2a79cef01 --- /dev/null +++ b/src/bmm/workflows/4-implementation/dev-story/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-dev-story +type: workflow +description: "Execute story implementation following a context-filled story spec file" diff --git a/src/bmm/workflows/4-implementation/retrospective/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/retrospective/bmad-skill-manifest.yaml new file mode 100644 index 000000000..51a5648ef --- /dev/null +++ b/src/bmm/workflows/4-implementation/retrospective/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-retrospective +type: workflow +description: "Post-epic review to extract lessons and assess success" diff --git a/src/bmm/workflows/4-implementation/sprint-planning/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/sprint-planning/bmad-skill-manifest.yaml new file mode 100644 index 000000000..2c02512ee --- /dev/null +++ b/src/bmm/workflows/4-implementation/sprint-planning/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-sprint-planning +type: workflow +description: "Generate sprint status tracking from epics" diff --git a/src/bmm/workflows/4-implementation/sprint-status/bmad-skill-manifest.yaml b/src/bmm/workflows/4-implementation/sprint-status/bmad-skill-manifest.yaml new file mode 100644 index 000000000..437b880e9 --- /dev/null +++ b/src/bmm/workflows/4-implementation/sprint-status/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-sprint-status +type: workflow +description: "Summarize sprint status and surface risks" diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/bmad-skill-manifest.yaml b/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/bmad-skill-manifest.yaml new file mode 100644 index 000000000..913c63629 --- /dev/null +++ b/src/bmm/workflows/bmad-quick-flow/quick-dev-new-preview/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-quick-dev-new-preview +type: workflow +description: "Unified quick flow - clarify intent, plan, implement, review, present" diff --git a/src/bmm/workflows/bmad-quick-flow/quick-dev/bmad-skill-manifest.yaml b/src/bmm/workflows/bmad-quick-flow/quick-dev/bmad-skill-manifest.yaml new file mode 100644 index 000000000..e04a33271 --- /dev/null +++ b/src/bmm/workflows/bmad-quick-flow/quick-dev/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-quick-dev +type: workflow +description: "Implement a Quick Tech Spec for small changes or features" diff --git a/src/bmm/workflows/bmad-quick-flow/quick-spec/bmad-skill-manifest.yaml b/src/bmm/workflows/bmad-quick-flow/quick-spec/bmad-skill-manifest.yaml new file mode 100644 index 000000000..1a383135c --- /dev/null +++ b/src/bmm/workflows/bmad-quick-flow/quick-spec/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-quick-spec +type: workflow +description: "Very quick process to create implementation-ready quick specs for small changes or features" diff --git a/src/bmm/workflows/document-project/bmad-skill-manifest.yaml b/src/bmm/workflows/document-project/bmad-skill-manifest.yaml new file mode 100644 index 000000000..4e8cb2767 --- /dev/null +++ b/src/bmm/workflows/document-project/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-document-project +type: workflow +description: "Document brownfield projects for AI context" diff --git a/src/bmm/workflows/generate-project-context/bmad-skill-manifest.yaml b/src/bmm/workflows/generate-project-context/bmad-skill-manifest.yaml new file mode 100644 index 000000000..c319972c4 --- /dev/null +++ b/src/bmm/workflows/generate-project-context/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-generate-project-context +type: workflow +description: "Create project-context.md with AI rules" diff --git a/src/bmm/workflows/qa-generate-e2e-tests/bmad-skill-manifest.yaml b/src/bmm/workflows/qa-generate-e2e-tests/bmad-skill-manifest.yaml new file mode 100644 index 000000000..20e08be69 --- /dev/null +++ b/src/bmm/workflows/qa-generate-e2e-tests/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-qa-generate-e2e-tests +type: workflow +description: "Generate end-to-end automated tests for existing features" diff --git a/src/core/agents/bmad-skill-manifest.yaml b/src/core/agents/bmad-skill-manifest.yaml new file mode 100644 index 000000000..21cd90501 --- /dev/null +++ b/src/core/agents/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-master +type: agent +description: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator" diff --git a/src/core/tasks/bmad-skill-manifest.yaml b/src/core/tasks/bmad-skill-manifest.yaml new file mode 100644 index 000000000..4f7e6b40e --- /dev/null +++ b/src/core/tasks/bmad-skill-manifest.yaml @@ -0,0 +1,39 @@ +editorial-review-prose.xml: + canonicalId: bmad-editorial-review-prose + type: task + description: "Clinical copy-editor that reviews text for communication issues" + +editorial-review-structure.xml: + canonicalId: bmad-editorial-review-structure + type: task + description: "Structural editor that proposes cuts, reorganization, and simplification while preserving comprehension" + +help.md: + canonicalId: bmad-help + type: task + description: "Analyzes what is done and the users query and offers advice on what to do next" + +index-docs.xml: + canonicalId: bmad-index-docs + type: task + description: "Generates or updates an index.md to reference all docs in the folder" + +review-adversarial-general.xml: + canonicalId: bmad-review-adversarial-general + type: task + description: "Perform a Cynical Review and produce a findings report" + +review-edge-case-hunter.xml: + canonicalId: bmad-review-edge-case-hunter + type: task + description: "Walk every branching path and boundary condition in content, report only unhandled edge cases" + +shard-doc.xml: + canonicalId: bmad-shard-doc + type: task + description: "Splits large markdown documents into smaller, organized files based on sections" + +workflow.xml: + canonicalId: bmad-workflow + type: task + description: "Execute given workflow by loading its configuration and following instructions" diff --git a/src/core/workflows/brainstorming/bmad-skill-manifest.yaml b/src/core/workflows/brainstorming/bmad-skill-manifest.yaml new file mode 100644 index 000000000..39a8f0ca9 --- /dev/null +++ b/src/core/workflows/brainstorming/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-brainstorming +type: workflow +description: "Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods" diff --git a/src/core/workflows/party-mode/bmad-skill-manifest.yaml b/src/core/workflows/party-mode/bmad-skill-manifest.yaml new file mode 100644 index 000000000..397e8fe3d --- /dev/null +++ b/src/core/workflows/party-mode/bmad-skill-manifest.yaml @@ -0,0 +1,3 @@ +canonicalId: bmad-party-mode +type: workflow +description: "Orchestrates group discussions between all installed BMAD agents" diff --git a/tools/cli/installers/lib/core/manifest-generator.js b/tools/cli/installers/lib/core/manifest-generator.js index 06e2e3f4b..0955a3d6f 100644 --- a/tools/cli/installers/lib/core/manifest-generator.js +++ b/tools/cli/installers/lib/core/manifest-generator.js @@ -5,6 +5,7 @@ const crypto = require('node:crypto'); const csv = require('csv-parse/sync'); const { getSourcePath, getModulePath } = require('../../../lib/project-root'); const prompts = require('../../../lib/prompts'); +const { loadSkillManifest: loadSkillManifestShared, getCanonicalId: getCanonicalIdShared } = require('../ide/shared/skill-manifest'); // Load package.json for version info const packageJson = require('../../../../../package.json'); @@ -23,6 +24,16 @@ class ManifestGenerator { this.selectedIdes = []; } + /** Delegate to shared skill-manifest module */ + async loadSkillManifest(dirPath) { + return loadSkillManifestShared(dirPath); + } + + /** Delegate to shared skill-manifest module */ + getCanonicalId(manifest, filename) { + return getCanonicalIdShared(manifest, filename); + } + /** * Clean text for CSV output by normalizing whitespace. * Note: Quote escaping is handled by escapeCsv() at write time. @@ -150,6 +161,8 @@ class ManifestGenerator { // Recursively find workflow.yaml files const findWorkflows = async (dir, relativePath = '') => { const entries = await fs.readdir(dir, { withFileTypes: true }); + // Load skill manifest for this directory (if present) + const skillManifest = await this.loadSkillManifest(dir); for (const entry of entries) { const fullPath = path.join(dir, entry.name); @@ -221,6 +234,7 @@ class ManifestGenerator { description: this.cleanForCSV(workflow.description), module: moduleName, path: installPath, + canonicalId: this.getCanonicalId(skillManifest, entry.name), }); // Add to files list @@ -294,6 +308,8 @@ class ManifestGenerator { async getAgentsFromDir(dirPath, moduleName, relativePath = '') { const agents = []; const entries = await fs.readdir(dirPath, { withFileTypes: true }); + // Load skill manifest for this directory (if present) + const skillManifest = await this.loadSkillManifest(dirPath); for (const entry of entries) { const fullPath = path.join(dirPath, entry.name); @@ -349,6 +365,7 @@ class ManifestGenerator { principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '', module: moduleName, path: installPath, + canonicalId: this.getCanonicalId(skillManifest, entry.name), }); // Add to files list @@ -388,6 +405,8 @@ class ManifestGenerator { async getTasksFromDir(dirPath, moduleName) { const tasks = []; const files = await fs.readdir(dirPath); + // Load skill manifest for this directory (if present) + const skillManifest = await this.loadSkillManifest(dirPath); for (const file of files) { // Check for both .xml and .md files @@ -447,6 +466,7 @@ class ManifestGenerator { module: moduleName, path: installPath, standalone: standalone, + canonicalId: this.getCanonicalId(skillManifest, file), }); // Add to files list @@ -486,6 +506,8 @@ class ManifestGenerator { async getToolsFromDir(dirPath, moduleName) { const tools = []; const files = await fs.readdir(dirPath); + // Load skill manifest for this directory (if present) + const skillManifest = await this.loadSkillManifest(dirPath); for (const file of files) { // Check for both .xml and .md files @@ -545,6 +567,7 @@ class ManifestGenerator { module: moduleName, path: installPath, standalone: standalone, + canonicalId: this.getCanonicalId(skillManifest, file), }); // Add to files list @@ -735,8 +758,8 @@ class ManifestGenerator { const csvPath = path.join(cfgDir, 'workflow-manifest.csv'); const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; - // Create CSV header - standalone column removed, everything is canonicalized to 4 columns - let csv = 'name,description,module,path\n'; + // Create CSV header - standalone column removed, canonicalId added as optional column + let csv = 'name,description,module,path,canonicalId\n'; // Build workflows map from discovered workflows only // Old entries are NOT preserved - the manifest reflects what actually exists on disk @@ -750,12 +773,19 @@ class ManifestGenerator { description: workflow.description, module: workflow.module, path: workflow.path, + canonicalId: workflow.canonicalId || '', }); } // Write all workflows for (const [, value] of allWorkflows) { - const row = [escapeCsv(value.name), escapeCsv(value.description), escapeCsv(value.module), escapeCsv(value.path)].join(','); + const row = [ + escapeCsv(value.name), + escapeCsv(value.description), + escapeCsv(value.module), + escapeCsv(value.path), + escapeCsv(value.canonicalId), + ].join(','); csv += row + '\n'; } @@ -784,8 +814,8 @@ class ManifestGenerator { } } - // Create CSV header with persona fields - let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path\n'; + // Create CSV header with persona fields and canonicalId + let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path,canonicalId\n'; // Combine existing and new agents, preferring new data for duplicates const allAgents = new Map(); @@ -810,6 +840,7 @@ class ManifestGenerator { principles: agent.principles, module: agent.module, path: agent.path, + canonicalId: agent.canonicalId || '', }); } @@ -827,6 +858,7 @@ class ManifestGenerator { escapeCsv(record.principles), escapeCsv(record.module), escapeCsv(record.path), + escapeCsv(record.canonicalId), ].join(','); csvContent += row + '\n'; } @@ -856,8 +888,8 @@ class ManifestGenerator { } } - // Create CSV header with standalone column - let csvContent = 'name,displayName,description,module,path,standalone\n'; + // Create CSV header with standalone and canonicalId columns + let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n'; // Combine existing and new tasks const allTasks = new Map(); @@ -877,6 +909,7 @@ class ManifestGenerator { module: task.module, path: task.path, standalone: task.standalone, + canonicalId: task.canonicalId || '', }); } @@ -889,6 +922,7 @@ class ManifestGenerator { escapeCsv(record.module), escapeCsv(record.path), escapeCsv(record.standalone), + escapeCsv(record.canonicalId), ].join(','); csvContent += row + '\n'; } @@ -918,8 +952,8 @@ class ManifestGenerator { } } - // Create CSV header with standalone column - let csvContent = 'name,displayName,description,module,path,standalone\n'; + // Create CSV header with standalone and canonicalId columns + let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n'; // Combine existing and new tools const allTools = new Map(); @@ -939,6 +973,7 @@ class ManifestGenerator { module: tool.module, path: tool.path, standalone: tool.standalone, + canonicalId: tool.canonicalId || '', }); } @@ -951,6 +986,7 @@ class ManifestGenerator { escapeCsv(record.module), escapeCsv(record.path), escapeCsv(record.standalone), + escapeCsv(record.canonicalId), ].join(','); csvContent += row + '\n'; } diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 813a6e674..a9ec58a60 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -433,10 +433,11 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}} * @returns {string} Generated filename */ generateFilename(artifact, artifactType, extension = '.md') { - const { toDashPath } = require('./shared/path-utils'); + const { resolveSkillName } = require('./shared/path-utils'); // Reuse central logic to ensure consistent naming conventions - const standardName = toDashPath(artifact.relativePath); + // Prefers canonicalId from manifest when available, falls back to path-derived name + const standardName = resolveSkillName(artifact); // Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md) // This handles any extensions that might slip through toDashPath() diff --git a/tools/cli/installers/lib/ide/codex.js b/tools/cli/installers/lib/ide/codex.js index abee979fd..73e93baf2 100644 --- a/tools/cli/installers/lib/ide/codex.js +++ b/tools/cli/installers/lib/ide/codex.js @@ -7,7 +7,7 @@ const { WorkflowCommandGenerator } = require('./shared/workflow-command-generato const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); const { getTasksFromBmad } = require('./shared/bmad-artifacts'); -const { toDashPath, customAgentDashName } = require('./shared/path-utils'); +const { toDashPath, resolveSkillName, customAgentDashName } = require('./shared/path-utils'); const prompts = require('../../../lib/prompts'); /** @@ -65,6 +65,7 @@ class CodexSetup extends BaseIdeSetup { name: task.name, displayName: task.name, module: task.module, + canonicalId: task.canonicalId || '', path: task.path, sourcePath: task.path, relativePath: path.join(task.module, 'tasks', `${task.name}.md`), @@ -216,8 +217,8 @@ class CodexSetup extends BaseIdeSetup { continue; } - // Get the dash-format name (e.g., bmad-bmm-create-prd.md) and remove .md - const flatName = toDashPath(artifact.relativePath); + // Get the skill name (prefers canonicalId, falls back to path-derived) and remove .md + const flatName = resolveSkillName(artifact); const skillName = flatName.replace(/\.md$/, ''); // Create skill directory diff --git a/tools/cli/installers/lib/ide/shared/agent-command-generator.js b/tools/cli/installers/lib/ide/shared/agent-command-generator.js index 0915c306b..37820992e 100644 --- a/tools/cli/installers/lib/ide/shared/agent-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/agent-command-generator.js @@ -47,6 +47,7 @@ class AgentCommandGenerator { name: agent.name, description: agent.description || `${agent.name} agent`, module: agent.module, + canonicalId: agent.canonicalId || '', relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename agentPath: agentRelPath, // Relative path to actual agent file content: launcherContent, diff --git a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js index 7bcfd6a79..d3edf0cd2 100644 --- a/tools/cli/installers/lib/ide/shared/bmad-artifacts.js +++ b/tools/cli/installers/lib/ide/shared/bmad-artifacts.js @@ -1,5 +1,6 @@ const path = require('node:path'); const fs = require('fs-extra'); +const { loadSkillManifest, getCanonicalId } = require('./skill-manifest'); /** * Helpers for gathering BMAD agents/tasks from the installed tree. @@ -34,6 +35,7 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) { const agentDirPath = path.join(standaloneAgentsDir, agentDir.name); const agentFiles = await fs.readdir(agentDirPath); + const skillManifest = await loadSkillManifest(agentDirPath); for (const file of agentFiles) { if (!file.endsWith('.md')) continue; @@ -48,6 +50,7 @@ async function getAgentsFromBmad(bmadDir, selectedModules = []) { path: filePath, name: file.replace('.md', ''), module: 'standalone', // Mark as standalone agent + canonicalId: getCanonicalId(skillManifest, file), }); } } @@ -84,6 +87,7 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') { } const entries = await fs.readdir(dirPath, { withFileTypes: true }); + const skillManifest = await loadSkillManifest(dirPath); for (const entry of entries) { // Skip if entry.name is undefined or not a string @@ -124,6 +128,7 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') { name: entry.name.replace('.md', ''), module: moduleName, relativePath: newRelativePath, // Keep the .md extension for the full path + canonicalId: getCanonicalId(skillManifest, entry.name), }); } } @@ -139,6 +144,7 @@ async function getTasksFromDir(dirPath, moduleName) { } const files = await fs.readdir(dirPath); + const skillManifest = await loadSkillManifest(dirPath); for (const file of files) { // Include both .md and .xml task files @@ -160,6 +166,7 @@ async function getTasksFromDir(dirPath, moduleName) { path: filePath, name: file.replace(ext, ''), module: moduleName, + canonicalId: getCanonicalId(skillManifest, file), }); } diff --git a/tools/cli/installers/lib/ide/shared/path-utils.js b/tools/cli/installers/lib/ide/shared/path-utils.js index 519669233..45efd2ec1 100644 --- a/tools/cli/installers/lib/ide/shared/path-utils.js +++ b/tools/cli/installers/lib/ide/shared/path-utils.js @@ -264,6 +264,21 @@ function parseUnderscoreName(filename) { }; } +/** + * Resolve the skill name for an artifact. + * Prefers canonicalId from a bmad-skill-manifest.yaml sidecar when available, + * falling back to the path-derived name from toDashPath(). + * + * @param {Object} artifact - Artifact object (must have relativePath; may have canonicalId) + * @returns {string} Filename like 'bmad-create-prd.md' or 'bmad-agent-bmm-pm.md' + */ +function resolveSkillName(artifact) { + if (artifact.canonicalId) { + return `${artifact.canonicalId}.md`; + } + return toDashPath(artifact.relativePath); +} + // Backward compatibility aliases (colon format was same as underscore) const toColonName = toUnderscoreName; const toColonPath = toUnderscorePath; @@ -275,6 +290,7 @@ module.exports = { // New standard (dash-based) toDashName, toDashPath, + resolveSkillName, customAgentDashName, isDashFormat, parseDashName, diff --git a/tools/cli/installers/lib/ide/shared/skill-manifest.js b/tools/cli/installers/lib/ide/shared/skill-manifest.js new file mode 100644 index 000000000..ff940242f --- /dev/null +++ b/tools/cli/installers/lib/ide/shared/skill-manifest.js @@ -0,0 +1,48 @@ +const path = require('node:path'); +const fs = require('fs-extra'); +const yaml = require('yaml'); + +/** + * Load bmad-skill-manifest.yaml from a directory. + * Single-entry manifests (canonicalId at top level) apply to all files in the directory. + * Multi-entry manifests are keyed by source filename. + * @param {string} dirPath - Directory to check for bmad-skill-manifest.yaml + * @returns {Object|null} Parsed manifest or null + */ +async function loadSkillManifest(dirPath) { + const manifestPath = path.join(dirPath, 'bmad-skill-manifest.yaml'); + try { + if (!(await fs.pathExists(manifestPath))) return null; + const content = await fs.readFile(manifestPath, 'utf8'); + const parsed = yaml.parse(content); + if (!parsed || typeof parsed !== 'object') return null; + if (parsed.canonicalId) return { __single: parsed }; + return parsed; + } catch (error) { + console.warn(`Warning: Failed to parse bmad-skill-manifest.yaml in ${dirPath}: ${error.message}`); + return null; + } +} + +/** + * Get the canonicalId for a specific file from a loaded skill manifest. + * @param {Object|null} manifest - Loaded manifest (from loadSkillManifest) + * @param {string} filename - Source filename to look up (e.g., 'pm.md', 'help.md', 'pm.agent.yaml') + * @returns {string} canonicalId or empty string + */ +function getCanonicalId(manifest, filename) { + if (!manifest) return ''; + // Single-entry manifest applies to all files in the directory + if (manifest.__single) return manifest.__single.canonicalId || ''; + // Multi-entry: look up by filename directly + if (manifest[filename]) return manifest[filename].canonicalId || ''; + // Fallback: try alternate extensions for compiled files + const baseName = filename.replace(/\.(md|xml)$/i, ''); + const agentKey = `${baseName}.agent.yaml`; + if (manifest[agentKey]) return manifest[agentKey].canonicalId || ''; + const xmlKey = `${baseName}.xml`; + if (manifest[xmlKey]) return manifest[xmlKey].canonicalId || ''; + return ''; +} + +module.exports = { loadSkillManifest, getCanonicalId }; diff --git a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js index 93e5b9a81..f21a5d174 100644 --- a/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/task-tool-command-generator.js @@ -50,6 +50,7 @@ class TaskToolCommandGenerator { displayName: task.displayName || task.name, description: task.description || `Execute ${task.displayName || task.name}`, module: task.module, + canonicalId: task.canonicalId || '', // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) relativePath: `${task.module}/tasks/${task.name}${taskExt}`, path: taskPath, @@ -75,6 +76,7 @@ class TaskToolCommandGenerator { displayName: tool.displayName || tool.name, description: tool.description || `Execute ${tool.displayName || tool.name}`, module: tool.module, + canonicalId: tool.canonicalId || '', // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) relativePath: `${tool.module}/tools/${tool.name}${toolExt}`, path: toolPath, diff --git a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js index d94e77db1..793252bac 100644 --- a/tools/cli/installers/lib/ide/shared/workflow-command-generator.js +++ b/tools/cli/installers/lib/ide/shared/workflow-command-generator.js @@ -93,6 +93,7 @@ class WorkflowCommandGenerator { name: workflow.name, description: workflow.description || `${workflow.name} workflow`, module: workflow.module, + canonicalId: workflow.canonicalId || '', relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`), workflowPath: workflowRelPath, // Relative path to actual workflow file content: commandContent,