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:
Alex Verkhovsky 2026-03-06 01:04:06 -07:00
parent dd66ad2bcc
commit e58d9bd639
33 changed files with 271 additions and 14 deletions

View File

@ -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"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-tech-writer
type: agent
description: "Technical Writer for documentation, Mermaid diagrams, and standards compliance"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-create-product-brief
type: workflow
description: "Create product brief through collaborative discovery"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-create-ux-design
type: workflow
description: "Plan UX patterns and design specifications"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-check-implementation-readiness
type: workflow
description: "Validate PRD, UX, Architecture and Epics specs are complete"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-create-architecture
type: workflow
description: "Create architecture solution design decisions for AI agent consistency"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-create-epics-and-stories
type: workflow
description: "Break requirements into epics and user stories"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-code-review
type: workflow
description: "Perform adversarial code review finding specific issues"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-correct-course
type: workflow
description: "Manage significant changes during sprint execution"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-create-story
type: workflow
description: "Creates a dedicated story file with all the context needed for implementation"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-dev-story
type: workflow
description: "Execute story implementation following a context-filled story spec file"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-retrospective
type: workflow
description: "Post-epic review to extract lessons and assess success"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-sprint-planning
type: workflow
description: "Generate sprint status tracking from epics"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-sprint-status
type: workflow
description: "Summarize sprint status and surface risks"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-quick-dev-new-preview
type: workflow
description: "Unified quick flow - clarify intent, plan, implement, review, present"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-quick-dev
type: workflow
description: "Implement a Quick Tech Spec for small changes or features"

View File

@ -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"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-document-project
type: workflow
description: "Document brownfield projects for AI context"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-generate-project-context
type: workflow
description: "Create project-context.md with AI rules"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-qa-generate-e2e-tests
type: workflow
description: "Generate end-to-end automated tests for existing features"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-master
type: agent
description: "BMad Master Executor, Knowledge Custodian, and Workflow Orchestrator"

View File

@ -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"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-brainstorming
type: workflow
description: "Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods"

View File

@ -0,0 +1,3 @@
canonicalId: bmad-party-mode
type: workflow
description: "Orchestrates group discussions between all installed BMAD agents"

View File

@ -5,6 +5,7 @@ const crypto = require('node:crypto');
const csv = require('csv-parse/sync'); const csv = require('csv-parse/sync');
const { getSourcePath, getModulePath } = require('../../../lib/project-root'); const { getSourcePath, getModulePath } = require('../../../lib/project-root');
const prompts = require('../../../lib/prompts'); const prompts = require('../../../lib/prompts');
const { loadSkillManifest: loadSkillManifestShared, getCanonicalId: getCanonicalIdShared } = require('../ide/shared/skill-manifest');
// Load package.json for version info // Load package.json for version info
const packageJson = require('../../../../../package.json'); const packageJson = require('../../../../../package.json');
@ -23,6 +24,16 @@ class ManifestGenerator {
this.selectedIdes = []; 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. * Clean text for CSV output by normalizing whitespace.
* Note: Quote escaping is handled by escapeCsv() at write time. * Note: Quote escaping is handled by escapeCsv() at write time.
@ -150,6 +161,8 @@ class ManifestGenerator {
// Recursively find workflow.yaml files // Recursively find workflow.yaml files
const findWorkflows = async (dir, relativePath = '') => { const findWorkflows = async (dir, relativePath = '') => {
const entries = await fs.readdir(dir, { withFileTypes: true }); 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) { for (const entry of entries) {
const fullPath = path.join(dir, entry.name); const fullPath = path.join(dir, entry.name);
@ -221,6 +234,7 @@ class ManifestGenerator {
description: this.cleanForCSV(workflow.description), description: this.cleanForCSV(workflow.description),
module: moduleName, module: moduleName,
path: installPath, path: installPath,
canonicalId: this.getCanonicalId(skillManifest, entry.name),
}); });
// Add to files list // Add to files list
@ -294,6 +308,8 @@ class ManifestGenerator {
async getAgentsFromDir(dirPath, moduleName, relativePath = '') { async getAgentsFromDir(dirPath, moduleName, relativePath = '') {
const agents = []; const agents = [];
const entries = await fs.readdir(dirPath, { withFileTypes: true }); 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) { for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name); const fullPath = path.join(dirPath, entry.name);
@ -349,6 +365,7 @@ class ManifestGenerator {
principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '', principles: principlesMatch ? this.cleanForCSV(principlesMatch[1]) : '',
module: moduleName, module: moduleName,
path: installPath, path: installPath,
canonicalId: this.getCanonicalId(skillManifest, entry.name),
}); });
// Add to files list // Add to files list
@ -388,6 +405,8 @@ class ManifestGenerator {
async getTasksFromDir(dirPath, moduleName) { async getTasksFromDir(dirPath, moduleName) {
const tasks = []; const tasks = [];
const files = await fs.readdir(dirPath); 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) { for (const file of files) {
// Check for both .xml and .md files // Check for both .xml and .md files
@ -447,6 +466,7 @@ class ManifestGenerator {
module: moduleName, module: moduleName,
path: installPath, path: installPath,
standalone: standalone, standalone: standalone,
canonicalId: this.getCanonicalId(skillManifest, file),
}); });
// Add to files list // Add to files list
@ -486,6 +506,8 @@ class ManifestGenerator {
async getToolsFromDir(dirPath, moduleName) { async getToolsFromDir(dirPath, moduleName) {
const tools = []; const tools = [];
const files = await fs.readdir(dirPath); 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) { for (const file of files) {
// Check for both .xml and .md files // Check for both .xml and .md files
@ -545,6 +567,7 @@ class ManifestGenerator {
module: moduleName, module: moduleName,
path: installPath, path: installPath,
standalone: standalone, standalone: standalone,
canonicalId: this.getCanonicalId(skillManifest, file),
}); });
// Add to files list // Add to files list
@ -735,8 +758,8 @@ class ManifestGenerator {
const csvPath = path.join(cfgDir, 'workflow-manifest.csv'); const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`; const escapeCsv = (value) => `"${String(value ?? '').replaceAll('"', '""')}"`;
// Create CSV header - standalone column removed, everything is canonicalized to 4 columns // Create CSV header - standalone column removed, canonicalId added as optional column
let csv = 'name,description,module,path\n'; let csv = 'name,description,module,path,canonicalId\n';
// Build workflows map from discovered workflows only // Build workflows map from discovered workflows only
// Old entries are NOT preserved - the manifest reflects what actually exists on disk // Old entries are NOT preserved - the manifest reflects what actually exists on disk
@ -750,12 +773,19 @@ class ManifestGenerator {
description: workflow.description, description: workflow.description,
module: workflow.module, module: workflow.module,
path: workflow.path, path: workflow.path,
canonicalId: workflow.canonicalId || '',
}); });
} }
// Write all workflows // Write all workflows
for (const [, value] of allWorkflows) { 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'; csv += row + '\n';
} }
@ -784,8 +814,8 @@ class ManifestGenerator {
} }
} }
// Create CSV header with persona fields // Create CSV header with persona fields and canonicalId
let csvContent = 'name,displayName,title,icon,capabilities,role,identity,communicationStyle,principles,module,path\n'; 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 // Combine existing and new agents, preferring new data for duplicates
const allAgents = new Map(); const allAgents = new Map();
@ -810,6 +840,7 @@ class ManifestGenerator {
principles: agent.principles, principles: agent.principles,
module: agent.module, module: agent.module,
path: agent.path, path: agent.path,
canonicalId: agent.canonicalId || '',
}); });
} }
@ -827,6 +858,7 @@ class ManifestGenerator {
escapeCsv(record.principles), escapeCsv(record.principles),
escapeCsv(record.module), escapeCsv(record.module),
escapeCsv(record.path), escapeCsv(record.path),
escapeCsv(record.canonicalId),
].join(','); ].join(',');
csvContent += row + '\n'; csvContent += row + '\n';
} }
@ -856,8 +888,8 @@ class ManifestGenerator {
} }
} }
// Create CSV header with standalone column // Create CSV header with standalone and canonicalId columns
let csvContent = 'name,displayName,description,module,path,standalone\n'; let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n';
// Combine existing and new tasks // Combine existing and new tasks
const allTasks = new Map(); const allTasks = new Map();
@ -877,6 +909,7 @@ class ManifestGenerator {
module: task.module, module: task.module,
path: task.path, path: task.path,
standalone: task.standalone, standalone: task.standalone,
canonicalId: task.canonicalId || '',
}); });
} }
@ -889,6 +922,7 @@ class ManifestGenerator {
escapeCsv(record.module), escapeCsv(record.module),
escapeCsv(record.path), escapeCsv(record.path),
escapeCsv(record.standalone), escapeCsv(record.standalone),
escapeCsv(record.canonicalId),
].join(','); ].join(',');
csvContent += row + '\n'; csvContent += row + '\n';
} }
@ -918,8 +952,8 @@ class ManifestGenerator {
} }
} }
// Create CSV header with standalone column // Create CSV header with standalone and canonicalId columns
let csvContent = 'name,displayName,description,module,path,standalone\n'; let csvContent = 'name,displayName,description,module,path,standalone,canonicalId\n';
// Combine existing and new tools // Combine existing and new tools
const allTools = new Map(); const allTools = new Map();
@ -939,6 +973,7 @@ class ManifestGenerator {
module: tool.module, module: tool.module,
path: tool.path, path: tool.path,
standalone: tool.standalone, standalone: tool.standalone,
canonicalId: tool.canonicalId || '',
}); });
} }
@ -951,6 +986,7 @@ class ManifestGenerator {
escapeCsv(record.module), escapeCsv(record.module),
escapeCsv(record.path), escapeCsv(record.path),
escapeCsv(record.standalone), escapeCsv(record.standalone),
escapeCsv(record.canonicalId),
].join(','); ].join(',');
csvContent += row + '\n'; csvContent += row + '\n';
} }

View File

@ -433,10 +433,11 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @returns {string} Generated filename * @returns {string} Generated filename
*/ */
generateFilename(artifact, artifactType, extension = '.md') { 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 // 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) // Clean up potential double extensions from source files (e.g. .yaml.md, .xml.md -> .md)
// This handles any extensions that might slip through toDashPath() // This handles any extensions that might slip through toDashPath()

View File

@ -7,7 +7,7 @@ const { WorkflowCommandGenerator } = require('./shared/workflow-command-generato
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
const { getTasksFromBmad } = require('./shared/bmad-artifacts'); 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'); const prompts = require('../../../lib/prompts');
/** /**
@ -65,6 +65,7 @@ class CodexSetup extends BaseIdeSetup {
name: task.name, name: task.name,
displayName: task.name, displayName: task.name,
module: task.module, module: task.module,
canonicalId: task.canonicalId || '',
path: task.path, path: task.path,
sourcePath: task.path, sourcePath: task.path,
relativePath: path.join(task.module, 'tasks', `${task.name}.md`), relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
@ -216,8 +217,8 @@ class CodexSetup extends BaseIdeSetup {
continue; continue;
} }
// Get the dash-format name (e.g., bmad-bmm-create-prd.md) and remove .md // Get the skill name (prefers canonicalId, falls back to path-derived) and remove .md
const flatName = toDashPath(artifact.relativePath); const flatName = resolveSkillName(artifact);
const skillName = flatName.replace(/\.md$/, ''); const skillName = flatName.replace(/\.md$/, '');
// Create skill directory // Create skill directory

View File

@ -47,6 +47,7 @@ class AgentCommandGenerator {
name: agent.name, name: agent.name,
description: agent.description || `${agent.name} agent`, description: agent.description || `${agent.name} agent`,
module: agent.module, module: agent.module,
canonicalId: agent.canonicalId || '',
relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename relativePath: path.join(agent.module, 'agents', agentPathInModule), // For command filename
agentPath: agentRelPath, // Relative path to actual agent file agentPath: agentRelPath, // Relative path to actual agent file
content: launcherContent, content: launcherContent,

View File

@ -1,5 +1,6 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { loadSkillManifest, getCanonicalId } = require('./skill-manifest');
/** /**
* Helpers for gathering BMAD agents/tasks from the installed tree. * 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 agentDirPath = path.join(standaloneAgentsDir, agentDir.name);
const agentFiles = await fs.readdir(agentDirPath); const agentFiles = await fs.readdir(agentDirPath);
const skillManifest = await loadSkillManifest(agentDirPath);
for (const file of agentFiles) { for (const file of agentFiles) {
if (!file.endsWith('.md')) continue; if (!file.endsWith('.md')) continue;
@ -48,6 +50,7 @@ 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),
}); });
} }
} }
@ -84,6 +87,7 @@ async function getAgentsFromDir(dirPath, moduleName, relativePath = '') {
} }
const entries = await fs.readdir(dirPath, { withFileTypes: true }); const entries = await fs.readdir(dirPath, { withFileTypes: true });
const skillManifest = await loadSkillManifest(dirPath);
for (const entry of entries) { for (const entry of entries) {
// Skip if entry.name is undefined or not a string // 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', ''), 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),
}); });
} }
} }
@ -139,6 +144,7 @@ async function getTasksFromDir(dirPath, moduleName) {
} }
const files = await fs.readdir(dirPath); const files = await fs.readdir(dirPath);
const skillManifest = await loadSkillManifest(dirPath);
for (const file of files) { for (const file of files) {
// Include both .md and .xml task files // Include both .md and .xml task files
@ -160,6 +166,7 @@ async function getTasksFromDir(dirPath, moduleName) {
path: filePath, path: filePath,
name: file.replace(ext, ''), name: file.replace(ext, ''),
module: moduleName, module: moduleName,
canonicalId: getCanonicalId(skillManifest, file),
}); });
} }

View 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) // Backward compatibility aliases (colon format was same as underscore)
const toColonName = toUnderscoreName; const toColonName = toUnderscoreName;
const toColonPath = toUnderscorePath; const toColonPath = toUnderscorePath;
@ -275,6 +290,7 @@ module.exports = {
// New standard (dash-based) // New standard (dash-based)
toDashName, toDashName,
toDashPath, toDashPath,
resolveSkillName,
customAgentDashName, customAgentDashName,
isDashFormat, isDashFormat,
parseDashName, parseDashName,

View File

@ -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 };

View File

@ -50,6 +50,7 @@ class TaskToolCommandGenerator {
displayName: task.displayName || task.name, displayName: task.displayName || task.name,
description: task.description || `Execute ${task.displayName || task.name}`, description: task.description || `Execute ${task.displayName || task.name}`,
module: task.module, module: task.module,
canonicalId: task.canonicalId || '',
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
relativePath: `${task.module}/tasks/${task.name}${taskExt}`, relativePath: `${task.module}/tasks/${task.name}${taskExt}`,
path: taskPath, path: taskPath,
@ -75,6 +76,7 @@ class TaskToolCommandGenerator {
displayName: tool.displayName || tool.name, displayName: tool.displayName || tool.name,
description: tool.description || `Execute ${tool.displayName || tool.name}`, description: tool.description || `Execute ${tool.displayName || tool.name}`,
module: tool.module, module: tool.module,
canonicalId: tool.canonicalId || '',
// Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows) // Use forward slashes for cross-platform consistency (not path.join which uses backslashes on Windows)
relativePath: `${tool.module}/tools/${tool.name}${toolExt}`, relativePath: `${tool.module}/tools/${tool.name}${toolExt}`,
path: toolPath, path: toolPath,

View File

@ -93,6 +93,7 @@ class WorkflowCommandGenerator {
name: workflow.name, name: workflow.name,
description: workflow.description || `${workflow.name} workflow`, description: workflow.description || `${workflow.name} workflow`,
module: workflow.module, module: workflow.module,
canonicalId: workflow.canonicalId || '',
relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`), relativePath: path.join(workflow.module, 'workflows', `${workflow.name}.md`),
workflowPath: workflowRelPath, // Relative path to actual workflow file workflowPath: workflowRelPath, // Relative path to actual workflow file
content: commandContent, content: commandContent,