feat(skills): add canonical bmad- naming via skill manifests
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 <noreply@anthropic.com>
This commit is contained in:
parent
dd66ad2bcc
commit
e58d9bd639
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-tech-writer
|
||||
type: agent
|
||||
description: "Technical Writer for documentation, Mermaid diagrams, and standards compliance"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-create-product-brief
|
||||
type: workflow
|
||||
description: "Create product brief through collaborative discovery"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-create-ux-design
|
||||
type: workflow
|
||||
description: "Plan UX patterns and design specifications"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-check-implementation-readiness
|
||||
type: workflow
|
||||
description: "Validate PRD, UX, Architecture and Epics specs are complete"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-create-architecture
|
||||
type: workflow
|
||||
description: "Create architecture solution design decisions for AI agent consistency"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-create-epics-and-stories
|
||||
type: workflow
|
||||
description: "Break requirements into epics and user stories"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-code-review
|
||||
type: workflow
|
||||
description: "Perform adversarial code review finding specific issues"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-correct-course
|
||||
type: workflow
|
||||
description: "Manage significant changes during sprint execution"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-create-story
|
||||
type: workflow
|
||||
description: "Creates a dedicated story file with all the context needed for implementation"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-dev-story
|
||||
type: workflow
|
||||
description: "Execute story implementation following a context-filled story spec file"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-retrospective
|
||||
type: workflow
|
||||
description: "Post-epic review to extract lessons and assess success"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-sprint-planning
|
||||
type: workflow
|
||||
description: "Generate sprint status tracking from epics"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-sprint-status
|
||||
type: workflow
|
||||
description: "Summarize sprint status and surface risks"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-quick-dev-new-preview
|
||||
type: workflow
|
||||
description: "Unified quick flow - clarify intent, plan, implement, review, present"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-quick-dev
|
||||
type: workflow
|
||||
description: "Implement a Quick Tech Spec for small changes or features"
|
||||
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-document-project
|
||||
type: workflow
|
||||
description: "Document brownfield projects for AI context"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-generate-project-context
|
||||
type: workflow
|
||||
description: "Create project-context.md with AI rules"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-qa-generate-e2e-tests
|
||||
type: workflow
|
||||
description: "Generate end-to-end automated tests for existing features"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-master
|
||||
type: agent
|
||||
description: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator"
|
||||
|
|
@ -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"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-brainstorming
|
||||
type: workflow
|
||||
description: "Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods"
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
canonicalId: bmad-party-mode
|
||||
type: workflow
|
||||
description: "Orchestrates group discussions between all installed BMAD agents"
|
||||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue