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,