This commit is contained in:
Alex Verkhovsky 2026-03-06 20:26:28 -06:00 committed by GitHub
commit 36dc8febfe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1093 additions and 496 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

@ -12,9 +12,12 @@
*/ */
const path = require('node:path'); const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra'); const fs = require('fs-extra');
const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder'); const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder');
const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator'); const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator');
const { IdeManager } = require('../tools/cli/installers/lib/ide/manager');
const { clearCache, loadPlatformCodes } = require('../tools/cli/installers/lib/ide/platform-codes');
// ANSI colors // ANSI colors
const colors = { const colors = {
@ -45,6 +48,39 @@ function assert(condition, testName, errorMessage = '') {
} }
} }
async function createTestBmadFixture() {
const fixtureDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-fixture-'));
// Minimal workflow manifest (generators check for this)
await fs.ensureDir(path.join(fixtureDir, '_config'));
await fs.writeFile(path.join(fixtureDir, '_config', 'workflow-manifest.csv'), '');
// Minimal compiled agent for core/agents (contains <agent tag and frontmatter)
const minimalAgent = [
'---',
'name: "test agent"',
'description: "Minimal test agent fixture"',
'---',
'',
'You are a test agent.',
'',
'<agent id="test-agent.agent.yaml" name="Test Agent" title="Test Agent">',
'<persona>Test persona</persona>',
'</agent>',
].join('\n');
await fs.ensureDir(path.join(fixtureDir, 'core', 'agents'));
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-master.md'), minimalAgent);
// Skill manifest so the installer uses 'bmad-master' as the canonical skill name
await fs.writeFile(path.join(fixtureDir, 'core', 'agents', 'bmad-skill-manifest.yaml'), 'bmad-master.md:\n canonicalId: bmad-master\n');
// Minimal compiled agent for bmm module (tests use selectedModules: ['bmm'])
await fs.ensureDir(path.join(fixtureDir, 'bmm', 'agents'));
await fs.writeFile(path.join(fixtureDir, 'bmm', 'agents', 'test-bmm-agent.md'), minimalAgent);
return fixtureDir;
}
/** /**
* Test Suite * Test Suite
*/ */
@ -158,9 +194,311 @@ async function runTests() {
console.log(''); console.log('');
// ============================================================ // ============================================================
// Test 5: QA Agent Compilation // Test 4: Windsurf Native Skills Install
// ============================================================ // ============================================================
console.log(`${colors.yellow}Test Suite 5: QA Agent Compilation${colors.reset}\n`); console.log(`${colors.yellow}Test Suite 4: Windsurf Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes = await loadPlatformCodes();
const windsurfInstaller = platformCodes.platforms.windsurf?.installer;
assert(windsurfInstaller?.target_dir === '.windsurf/skills', 'Windsurf target_dir uses native skills path');
assert(windsurfInstaller?.skill_format === true, 'Windsurf installer enables native skill output');
assert(
Array.isArray(windsurfInstaller?.legacy_targets) && windsurfInstaller.legacy_targets.includes('.windsurf/workflows'),
'Windsurf installer cleans legacy workflow output',
);
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-windsurf-test-'));
const installedBmadDir = await createTestBmadFixture();
const legacyDir = path.join(tempProjectDir, '.windsurf', 'workflows', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(tempProjectDir, '.windsurf', 'workflows', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const result = await ideManager.setup('windsurf', tempProjectDir, installedBmadDir, {
silent: true,
selectedModules: ['bmm'],
});
assert(result.success === true, 'Windsurf setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.windsurf', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Windsurf install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.windsurf', 'workflows'))), 'Windsurf setup removes legacy workflows dir');
await fs.remove(tempProjectDir);
await fs.remove(installedBmadDir);
} catch (error) {
assert(false, 'Windsurf native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Test 5: Kiro Native Skills Install
// ============================================================
console.log(`${colors.yellow}Test Suite 5: Kiro Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes = await loadPlatformCodes();
const kiroInstaller = platformCodes.platforms.kiro?.installer;
assert(kiroInstaller?.target_dir === '.kiro/skills', 'Kiro target_dir uses native skills path');
assert(kiroInstaller?.skill_format === true, 'Kiro installer enables native skill output');
assert(
Array.isArray(kiroInstaller?.legacy_targets) && kiroInstaller.legacy_targets.includes('.kiro/steering'),
'Kiro installer cleans legacy steering output',
);
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kiro-test-'));
const installedBmadDir = await createTestBmadFixture();
const legacyDir = path.join(tempProjectDir, '.kiro', 'steering', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(tempProjectDir, '.kiro', 'steering', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const result = await ideManager.setup('kiro', tempProjectDir, installedBmadDir, {
silent: true,
selectedModules: ['bmm'],
});
assert(result.success === true, 'Kiro setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.kiro', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Kiro install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.kiro', 'steering'))), 'Kiro setup removes legacy steering dir');
await fs.remove(tempProjectDir);
await fs.remove(installedBmadDir);
} catch (error) {
assert(false, 'Kiro native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Test 6: Antigravity Native Skills Install
// ============================================================
console.log(`${colors.yellow}Test Suite 6: Antigravity Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes = await loadPlatformCodes();
const antigravityInstaller = platformCodes.platforms.antigravity?.installer;
assert(antigravityInstaller?.target_dir === '.agent/skills', 'Antigravity target_dir uses native skills path');
assert(antigravityInstaller?.skill_format === true, 'Antigravity installer enables native skill output');
assert(
Array.isArray(antigravityInstaller?.legacy_targets) && antigravityInstaller.legacy_targets.includes('.agent/workflows'),
'Antigravity installer cleans legacy workflow output',
);
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-antigravity-test-'));
const installedBmadDir = await createTestBmadFixture();
const legacyDir = path.join(tempProjectDir, '.agent', 'workflows', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(tempProjectDir, '.agent', 'workflows', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const result = await ideManager.setup('antigravity', tempProjectDir, installedBmadDir, {
silent: true,
selectedModules: ['bmm'],
});
assert(result.success === true, 'Antigravity setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.agent', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Antigravity install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.agent', 'workflows'))), 'Antigravity setup removes legacy workflows dir');
await fs.remove(tempProjectDir);
await fs.remove(installedBmadDir);
} catch (error) {
assert(false, 'Antigravity native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Test 7: Auggie Native Skills Install
// ============================================================
console.log(`${colors.yellow}Test Suite 7: Auggie Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes = await loadPlatformCodes();
const auggieInstaller = platformCodes.platforms.auggie?.installer;
assert(auggieInstaller?.target_dir === '.augment/skills', 'Auggie target_dir uses native skills path');
assert(auggieInstaller?.skill_format === true, 'Auggie installer enables native skill output');
assert(
Array.isArray(auggieInstaller?.legacy_targets) && auggieInstaller.legacy_targets.includes('.augment/commands'),
'Auggie installer cleans legacy command output',
);
assert(
auggieInstaller?.ancestor_conflict_check !== true,
'Auggie installer does not enable ancestor conflict checks without verified inheritance',
);
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-auggie-test-'));
const installedBmadDir = await createTestBmadFixture();
const legacyDir = path.join(tempProjectDir, '.augment', 'commands', 'bmad-legacy-dir');
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(tempProjectDir, '.augment', 'commands', 'bmad-legacy.md'), 'legacy\n');
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const result = await ideManager.setup('auggie', tempProjectDir, installedBmadDir, {
silent: true,
selectedModules: ['bmm'],
});
assert(result.success === true, 'Auggie setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.augment', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'Auggie install writes SKILL.md directory output');
assert(!(await fs.pathExists(path.join(tempProjectDir, '.augment', 'commands'))), 'Auggie setup removes legacy commands dir');
await fs.remove(tempProjectDir);
await fs.remove(installedBmadDir);
} catch (error) {
assert(false, 'Auggie native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Test 8: OpenCode Native Skills Install
// ============================================================
console.log(`${colors.yellow}Test Suite 8: OpenCode Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes = await loadPlatformCodes();
const opencodeInstaller = platformCodes.platforms.opencode?.installer;
assert(opencodeInstaller?.target_dir === '.opencode/skills', 'OpenCode target_dir uses native skills path');
assert(opencodeInstaller?.skill_format === true, 'OpenCode installer enables native skill output');
assert(opencodeInstaller?.ancestor_conflict_check === true, 'OpenCode installer enables ancestor conflict checks');
assert(
Array.isArray(opencodeInstaller?.legacy_targets) &&
['.opencode/agents', '.opencode/commands', '.opencode/agent', '.opencode/command'].every((legacyTarget) =>
opencodeInstaller.legacy_targets.includes(legacyTarget),
),
'OpenCode installer cleans split legacy agent and command output',
);
const tempProjectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-test-'));
const installedBmadDir = await createTestBmadFixture();
const legacyDirs = [
path.join(tempProjectDir, '.opencode', 'agents', 'bmad-legacy-agent'),
path.join(tempProjectDir, '.opencode', 'commands', 'bmad-legacy-command'),
path.join(tempProjectDir, '.opencode', 'agent', 'bmad-legacy-agent-singular'),
path.join(tempProjectDir, '.opencode', 'command', 'bmad-legacy-command-singular'),
];
for (const legacyDir of legacyDirs) {
await fs.ensureDir(legacyDir);
await fs.writeFile(path.join(legacyDir, 'SKILL.md'), 'legacy\n');
await fs.writeFile(path.join(path.dirname(legacyDir), `${path.basename(legacyDir)}.md`), 'legacy\n');
}
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const result = await ideManager.setup('opencode', tempProjectDir, installedBmadDir, {
silent: true,
selectedModules: ['bmm'],
});
assert(result.success === true, 'OpenCode setup succeeds against temp project');
const skillFile = path.join(tempProjectDir, '.opencode', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile), 'OpenCode install writes SKILL.md directory output');
for (const legacyDir of ['agents', 'commands', 'agent', 'command']) {
assert(
!(await fs.pathExists(path.join(tempProjectDir, '.opencode', legacyDir))),
`OpenCode setup removes legacy .opencode/${legacyDir} dir`,
);
}
await fs.remove(tempProjectDir);
await fs.remove(installedBmadDir);
} catch (error) {
assert(false, 'OpenCode native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Test 9: OpenCode Ancestor Conflict
// ============================================================
console.log(`${colors.yellow}Test Suite 9: OpenCode Ancestor Conflict${colors.reset}\n`);
try {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-ancestor-test-'));
const parentProjectDir = path.join(tempRoot, 'parent');
const childProjectDir = path.join(parentProjectDir, 'child');
const installedBmadDir = await createTestBmadFixture();
await fs.ensureDir(path.join(parentProjectDir, '.git'));
await fs.ensureDir(path.join(parentProjectDir, '.opencode', 'skills', 'bmad-existing'));
await fs.ensureDir(childProjectDir);
await fs.writeFile(path.join(parentProjectDir, '.opencode', 'skills', 'bmad-existing', 'SKILL.md'), 'legacy\n');
const ideManager = new IdeManager();
await ideManager.ensureInitialized();
const result = await ideManager.setup('opencode', childProjectDir, installedBmadDir, {
silent: true,
selectedModules: ['bmm'],
});
const expectedConflictDir = await fs.realpath(path.join(parentProjectDir, '.opencode', 'skills'));
assert(result.success === false, 'OpenCode setup refuses install when ancestor skills already exist');
assert(result.handlerResult?.reason === 'ancestor-conflict', 'OpenCode ancestor rejection reports ancestor-conflict reason');
assert(
result.handlerResult?.conflictDir === expectedConflictDir,
'OpenCode ancestor rejection points at ancestor .opencode/skills dir',
);
await fs.remove(tempRoot);
await fs.remove(installedBmadDir);
} catch (error) {
assert(false, 'OpenCode ancestor conflict protection test succeeds', error.message);
}
console.log('');
// ============================================================
// Test 10: QA Agent Compilation
// ============================================================
console.log(`${colors.yellow}Test Suite 10: QA Agent Compilation${colors.reset}\n`);
try { try {
const builder = new YamlXmlBuilder(); const builder = new YamlXmlBuilder();

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

@ -1,5 +1,7 @@
const os = require('node:os');
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const yaml = require('yaml');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const prompts = require('../../../lib/prompts'); const prompts = require('../../../lib/prompts');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
@ -24,6 +26,34 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
super(platformCode, platformConfig.name, platformConfig.preferred); super(platformCode, platformConfig.name, platformConfig.preferred);
this.platformConfig = platformConfig; this.platformConfig = platformConfig;
this.installerConfig = platformConfig.installer || null; this.installerConfig = platformConfig.installer || null;
// Set configDir from target_dir so base-class detect() works
if (this.installerConfig?.target_dir) {
this.configDir = this.installerConfig.target_dir;
}
}
/**
* Detect whether this IDE already has configuration in the project.
* For skill_format platforms, checks for bmad-prefixed entries in target_dir
* (matching old codex.js behavior) instead of just checking directory existence.
* @param {string} projectDir - Project directory
* @returns {Promise<boolean>}
*/
async detect(projectDir) {
if (this.installerConfig?.skill_format && this.configDir) {
const dir = path.join(projectDir || process.cwd(), this.configDir);
if (await fs.pathExists(dir)) {
try {
const entries = await fs.readdir(dir);
return entries.some((e) => typeof e === 'string' && e.startsWith('bmad'));
} catch {
return false;
}
}
return false;
}
return super.detect(projectDir);
} }
/** /**
@ -39,8 +69,8 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
const conflict = await this.findAncestorConflict(projectDir); const conflict = await this.findAncestorConflict(projectDir);
if (conflict) { if (conflict) {
await prompts.log.error( await prompts.log.error(
`Found existing BMAD commands in ancestor installation: ${conflict}\n` + `Found existing BMAD skills in ancestor installation: ${conflict}\n` +
` ${this.name} inherits commands from parent directories, so this would cause duplicates.\n` + ` ${this.name} inherits skills from parent directories, so this would cause duplicates.\n` +
` Please remove the BMAD files from that directory first:\n` + ` Please remove the BMAD files from that directory first:\n` +
` rm -rf "${conflict}"/bmad*`, ` rm -rf "${conflict}"/bmad*`,
); );
@ -165,8 +195,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
for (const artifact of artifacts) { for (const artifact of artifacts) {
const content = this.renderTemplate(template, artifact); const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, 'agent', extension); const filename = this.generateFilename(artifact, 'agent', extension);
if (config.skill_format) {
await this.writeSkillFile(targetPath, artifact, content);
} else {
const filePath = path.join(targetPath, filename); const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content); await this.writeFile(filePath, content);
}
count++; count++;
} }
@ -198,8 +233,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType); const { content: template, extension } = await this.loadTemplate(workflowTemplateType, '', config, finalTemplateType);
const content = this.renderTemplate(template, artifact); const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, 'workflow', extension); const filename = this.generateFilename(artifact, 'workflow', extension);
if (config.skill_format) {
await this.writeSkillFile(targetPath, artifact, content);
} else {
const filePath = path.join(targetPath, filename); const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content); await this.writeFile(filePath, content);
}
count++; count++;
} }
} }
@ -241,8 +281,13 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
const content = this.renderTemplate(template, artifact); const content = this.renderTemplate(template, artifact);
const filename = this.generateFilename(artifact, artifact.type, extension); const filename = this.generateFilename(artifact, artifact.type, extension);
if (config.skill_format) {
await this.writeSkillFile(targetPath, artifact, content);
} else {
const filePath = path.join(targetPath, filename); const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content); await this.writeFile(filePath, content);
}
if (artifact.type === 'task') { if (artifact.type === 'task') {
taskCount++; taskCount++;
@ -409,22 +454,146 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
// No default // No default
} }
let rendered = template // Replace _bmad placeholder with actual folder name BEFORE inserting paths,
// so that paths containing '_bmad' are not corrupted by the blanket replacement.
let rendered = template.replaceAll('_bmad', this.bmadFolderName);
// Replace {{bmadFolderName}} placeholder if present
rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName);
rendered = rendered
.replaceAll('{{name}}', artifact.name || '') .replaceAll('{{name}}', artifact.name || '')
.replaceAll('{{module}}', artifact.module || 'core') .replaceAll('{{module}}', artifact.module || 'core')
.replaceAll('{{path}}', pathToUse) .replaceAll('{{path}}', pathToUse)
.replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`) .replaceAll('{{description}}', artifact.description || `${artifact.name} ${artifact.type || ''}`)
.replaceAll('{{workflow_path}}', pathToUse); .replaceAll('{{workflow_path}}', pathToUse);
// Replace _bmad placeholder with actual folder name
rendered = rendered.replaceAll('_bmad', this.bmadFolderName);
// Replace {{bmadFolderName}} placeholder if present
rendered = rendered.replaceAll('{{bmadFolderName}}', this.bmadFolderName);
return rendered; return rendered;
} }
/**
* Write artifact as a skill directory with SKILL.md inside.
* Writes artifact as a skill directory with SKILL.md inside.
* @param {string} targetPath - Base skills directory
* @param {Object} artifact - Artifact data
* @param {string} content - Rendered template content
*/
async writeSkillFile(targetPath, artifact, content) {
const { resolveSkillName } = require('./shared/path-utils');
// Get the skill name (prefers canonicalId, falls back to path-derived) and remove .md
const flatName = resolveSkillName(artifact);
const skillName = path.basename(flatName.replace(/\.md$/, ''));
if (!skillName) {
throw new Error(`Cannot derive skill name for artifact: ${artifact.relativePath || JSON.stringify(artifact)}`);
}
// Create skill directory
const skillDir = path.join(targetPath, skillName);
await this.ensureDir(skillDir);
// Transform content: rewrite frontmatter for skills format
const skillContent = this.transformToSkillFormat(content, skillName);
await this.writeFile(path.join(skillDir, 'SKILL.md'), skillContent);
}
/**
* Transform artifact content to Agent Skills format.
* Rewrites frontmatter to contain only unquoted name and description.
* @param {string} content - Original content with YAML frontmatter
* @param {string} skillName - Skill name (must match directory name)
* @returns {string} Transformed content
*/
transformToSkillFormat(content, skillName) {
// Normalize line endings
content = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
// Parse frontmatter
const fmMatch = content.match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/);
if (!fmMatch) {
// No frontmatter -- wrap with minimal frontmatter
const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd();
return `---\n${fm}\n---\n\n${content}`;
}
const frontmatter = fmMatch[1];
const body = fmMatch[2];
// Parse frontmatter with yaml library to extract description
let description;
try {
const parsed = yaml.parse(frontmatter);
const rawDesc = parsed?.description;
description = typeof rawDesc === 'string' && rawDesc ? rawDesc : `${skillName} skill`;
} catch {
description = `${skillName} skill`;
}
// Build new frontmatter with only name and description, unquoted
const newFrontmatter = yaml.stringify({ name: skillName, description: String(description) }, { lineWidth: 0 }).trimEnd();
return `---\n${newFrontmatter}\n---\n${body}`;
}
/**
* Install a custom agent launcher.
* For skill_format platforms, produces <skillDir>/SKILL.md.
* For flat platforms, produces a single file in target_dir.
* @param {string} projectDir - Project directory
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root)
* @param {Object} metadata - Agent metadata
* @returns {Object|null} Info about created file/skill
*/
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
if (!this.installerConfig?.target_dir) return null;
const { customAgentDashName } = require('./shared/path-utils');
const targetPath = path.join(projectDir, this.installerConfig.target_dir);
await this.ensureDir(targetPath);
// Build artifact to reuse existing template rendering.
// The default-agent template already includes the _bmad/ prefix before {{path}},
// but agentPath is relative to project root (e.g. "_bmad/custom/agents/fred.md").
// Strip the bmadFolderName prefix so the template doesn't produce a double path.
const bmadPrefix = this.bmadFolderName + '/';
const normalizedPath = agentPath.startsWith(bmadPrefix) ? agentPath.slice(bmadPrefix.length) : agentPath;
const artifact = {
type: 'agent-launcher',
name: agentName,
description: metadata?.description || `${agentName} agent`,
agentPath: normalizedPath,
relativePath: normalizedPath,
module: 'custom',
};
const { content: template } = await this.loadTemplate(
this.installerConfig.template_type || 'default',
'agent',
this.installerConfig,
'default-agent',
);
const content = this.renderTemplate(template, artifact);
if (this.installerConfig.skill_format) {
const skillName = customAgentDashName(agentName).replace(/\.md$/, '');
const skillDir = path.join(targetPath, skillName);
await this.ensureDir(skillDir);
const skillContent = this.transformToSkillFormat(content, skillName);
const skillPath = path.join(skillDir, 'SKILL.md');
await this.writeFile(skillPath, skillContent);
return { path: path.relative(projectDir, skillPath), command: `$${skillName}` };
}
// Flat file output
const filename = customAgentDashName(agentName);
const filePath = path.join(targetPath, filename);
await this.writeFile(filePath, content);
return { path: path.relative(projectDir, filePath), command: agentName };
}
/** /**
* Generate filename for artifact * Generate filename for artifact
* @param {Object} artifact - Artifact data * @param {Object} artifact - Artifact data
@ -433,10 +602,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()
@ -476,10 +646,14 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
if (this.installerConfig?.legacy_targets) { if (this.installerConfig?.legacy_targets) {
if (!options.silent) await prompts.log.message(' Migrating legacy directories...'); if (!options.silent) await prompts.log.message(' Migrating legacy directories...');
for (const legacyDir of this.installerConfig.legacy_targets) { for (const legacyDir of this.installerConfig.legacy_targets) {
if (this.isGlobalPath(legacyDir)) {
await this.warnGlobalLegacy(legacyDir, options);
} else {
await this.cleanupTarget(projectDir, legacyDir, options); await this.cleanupTarget(projectDir, legacyDir, options);
await this.removeEmptyParents(projectDir, legacyDir); await this.removeEmptyParents(projectDir, legacyDir);
} }
} }
}
// Clean all target directories // Clean all target directories
if (this.installerConfig?.targets) { if (this.installerConfig?.targets) {
@ -501,6 +675,41 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
} }
} }
/**
* Check if a path is global (starts with ~ or is absolute)
* @param {string} p - Path to check
* @returns {boolean}
*/
isGlobalPath(p) {
return p.startsWith('~') || path.isAbsolute(p);
}
/**
* Warn about stale BMAD files in a global legacy directory (never auto-deletes)
* @param {string} legacyDir - Legacy directory path (may start with ~)
* @param {Object} options - Options (silent, etc.)
*/
async warnGlobalLegacy(legacyDir, options = {}) {
try {
const expanded = legacyDir.startsWith('~/')
? path.join(os.homedir(), legacyDir.slice(2))
: legacyDir === '~'
? os.homedir()
: legacyDir;
if (!(await fs.pathExists(expanded))) return;
const entries = await fs.readdir(expanded);
const bmadFiles = entries.filter((e) => typeof e === 'string' && e.startsWith('bmad'));
if (bmadFiles.length > 0 && !options.silent) {
await prompts.log.warn(`Found ${bmadFiles.length} stale BMAD file(s) in ${expanded}. Remove manually: rm ${expanded}/bmad-*`);
}
} catch {
// Errors reading global paths are silently ignored
}
}
/** /**
* Cleanup a specific target directory * Cleanup a specific target directory
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory

View File

@ -1,440 +0,0 @@
const path = require('node:path');
const os = require('node:os');
const fs = require('fs-extra');
const yaml = require('yaml');
const { BaseIdeSetup } = require('./_base-ide');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
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 prompts = require('../../../lib/prompts');
/**
* Codex setup handler (CLI mode)
*/
class CodexSetup extends BaseIdeSetup {
constructor() {
super('codex', 'Codex', false);
}
/**
* Setup Codex configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
// Always use CLI mode
const mode = 'cli';
const { artifacts, counts } = await this.collectClaudeArtifacts(projectDir, bmadDir, options);
// Clean up old .codex/prompts locations (both global and project)
const oldGlobalDir = this.getOldCodexPromptDir(null, 'global');
await this.clearOldBmadFiles(oldGlobalDir, options);
const oldProjectDir = this.getOldCodexPromptDir(projectDir, 'project');
await this.clearOldBmadFiles(oldProjectDir, options);
// Install to .agents/skills
const destDir = this.getCodexSkillsDir(projectDir);
await fs.ensureDir(destDir);
await this.clearOldBmadSkills(destDir, options);
// Collect and write agent skills
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
const agentCount = await this.writeSkillArtifacts(destDir, agentArtifacts, 'agent-launcher');
// Collect and write task skills
const tasks = await getTasksFromBmad(bmadDir, options.selectedModules || []);
const taskArtifacts = [];
for (const task of tasks) {
const content = await this.readAndProcessWithProject(
task.path,
{
module: task.module,
name: task.name,
},
projectDir,
);
taskArtifacts.push({
type: 'task',
name: task.name,
displayName: task.name,
module: task.module,
path: task.path,
sourcePath: task.path,
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
content,
});
}
const ttGen = new TaskToolCommandGenerator(this.bmadFolderName);
const taskSkillArtifacts = taskArtifacts.map((artifact) => ({
...artifact,
content: ttGen.generateCommandContent(artifact, artifact.type),
}));
const tasksWritten = await this.writeSkillArtifacts(destDir, taskSkillArtifacts, 'task');
// Collect and write workflow skills
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
const workflowCount = await this.writeSkillArtifacts(destDir, workflowArtifacts, 'workflow-command');
const written = agentCount + workflowCount + tasksWritten;
if (!options.silent) {
await prompts.log.success(
`${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} skills → ${destDir}`,
);
}
return {
success: true,
mode,
artifacts,
counts,
destination: destDir,
written,
};
}
/**
* Detect Codex installation by checking for BMAD skills
*/
async detect(projectDir) {
const dir = this.getCodexSkillsDir(projectDir || process.cwd());
if (await fs.pathExists(dir)) {
try {
const entries = await fs.readdir(dir);
if (entries && entries.some((entry) => entry && typeof entry === 'string' && entry.startsWith('bmad'))) {
return true;
}
} catch {
// Ignore errors
}
}
return false;
}
/**
* Collect Claude-style artifacts for Codex export.
* Returns the normalized artifact list for further processing.
*/
async collectClaudeArtifacts(projectDir, bmadDir, options = {}) {
const selectedModules = options.selectedModules || [];
const artifacts = [];
// Generate agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, selectedModules);
for (const artifact of agentArtifacts) {
artifacts.push({
type: 'agent',
module: artifact.module,
sourcePath: artifact.sourcePath,
relativePath: artifact.relativePath,
content: artifact.content,
});
}
const tasks = await getTasksFromBmad(bmadDir, selectedModules);
for (const task of tasks) {
const content = await this.readAndProcessWithProject(
task.path,
{
module: task.module,
name: task.name,
},
projectDir,
);
artifacts.push({
type: 'task',
name: task.name,
displayName: task.name,
module: task.module,
path: task.path,
sourcePath: task.path,
relativePath: path.join(task.module, 'tasks', `${task.name}.md`),
content,
});
}
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts, counts: workflowCounts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
artifacts.push(...workflowArtifacts);
return {
artifacts,
counts: {
agents: agentArtifacts.length,
tasks: tasks.length,
workflows: workflowCounts.commands,
workflowLaunchers: workflowCounts.launchers,
},
};
}
getCodexSkillsDir(projectDir) {
if (!projectDir) {
throw new Error('projectDir is required for project-scoped skill installation');
}
return path.join(projectDir, '.agents', 'skills');
}
/**
* Get the old .codex/prompts directory for cleanup purposes
*/
getOldCodexPromptDir(projectDir = null, location = 'global') {
if (location === 'project' && projectDir) {
return path.join(projectDir, '.codex', 'prompts');
}
return path.join(os.homedir(), '.codex', 'prompts');
}
/**
* Write artifacts as Agent Skills (agentskills.io format).
* Each artifact becomes a directory containing SKILL.md.
* @param {string} destDir - Base skills directory
* @param {Array} artifacts - Artifacts to write
* @param {string} artifactType - Type filter (e.g., 'agent-launcher', 'workflow-command', 'task')
* @returns {number} Number of skills written
*/
async writeSkillArtifacts(destDir, artifacts, artifactType) {
let writtenCount = 0;
for (const artifact of artifacts) {
// Filter by type if the artifact has a type field
if (artifact.type && artifact.type !== artifactType) {
continue;
}
// Get the dash-format name (e.g., bmad-bmm-create-prd.md) and remove .md
const flatName = toDashPath(artifact.relativePath);
const skillName = flatName.replace(/\.md$/, '');
// Create skill directory
const skillDir = path.join(destDir, skillName);
await fs.ensureDir(skillDir);
// Transform content: rewrite frontmatter for skills format
const skillContent = this.transformToSkillFormat(artifact.content, skillName);
// Write SKILL.md with platform-native line endings
const platformContent = skillContent.replaceAll('\n', os.EOL);
await fs.writeFile(path.join(skillDir, 'SKILL.md'), platformContent, 'utf8');
writtenCount++;
}
return writtenCount;
}
/**
* Transform artifact content from Codex prompt format to Agent Skills format.
* Removes disable-model-invocation, ensures name matches directory.
* @param {string} content - Original content with YAML frontmatter
* @param {string} skillName - Skill name (must match directory name)
* @returns {string} Transformed content
*/
transformToSkillFormat(content, skillName) {
// Normalize line endings so body matches rebuilt frontmatter (both LF)
content = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
// Parse frontmatter
const fmMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
if (!fmMatch) {
// No frontmatter -- wrap with minimal frontmatter
const fm = yaml.stringify({ name: skillName, description: skillName }).trimEnd();
return `---\n${fm}\n---\n\n${content}`;
}
const frontmatter = fmMatch[1];
const body = fmMatch[2];
// Parse frontmatter with yaml library to handle all quoting variants
let description;
try {
const parsed = yaml.parse(frontmatter);
description = parsed?.description || `${skillName} skill`;
} catch {
description = `${skillName} skill`;
}
// Build new frontmatter with only skills-spec fields, let yaml handle quoting
const newFrontmatter = yaml.stringify({ name: skillName, description }, { lineWidth: 0 }).trimEnd();
return `---\n${newFrontmatter}\n---\n${body}`;
}
/**
* Remove existing BMAD skill directories from the skills directory.
*/
async clearOldBmadSkills(destDir, options = {}) {
if (!(await fs.pathExists(destDir))) {
return;
}
let entries;
try {
entries = await fs.readdir(destDir);
} catch (error) {
if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`);
return;
}
if (!entries || !Array.isArray(entries)) {
return;
}
for (const entry of entries) {
if (!entry || typeof entry !== 'string') {
continue;
}
if (!entry.startsWith('bmad')) {
continue;
}
const entryPath = path.join(destDir, entry);
try {
await fs.remove(entryPath);
} catch (error) {
if (!options.silent) {
await prompts.log.message(` Skipping ${entry}: ${error.message}`);
}
}
}
}
/**
* Clean old BMAD files from legacy .codex/prompts directories.
*/
async clearOldBmadFiles(destDir, options = {}) {
if (!(await fs.pathExists(destDir))) {
return;
}
let entries;
try {
entries = await fs.readdir(destDir);
} catch (error) {
// Directory exists but can't be read - skip cleanup
if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`);
return;
}
if (!entries || !Array.isArray(entries)) {
return;
}
for (const entry of entries) {
// Skip non-strings or undefined entries
if (!entry || typeof entry !== 'string') {
continue;
}
if (!entry.startsWith('bmad')) {
continue;
}
const entryPath = path.join(destDir, entry);
try {
await fs.remove(entryPath);
} catch (error) {
if (!options.silent) {
await prompts.log.message(` Skipping ${entry}: ${error.message}`);
}
}
}
}
async readAndProcessWithProject(filePath, metadata, projectDir) {
const rawContent = await fs.readFile(filePath, 'utf8');
const content = rawContent.replaceAll('\r\n', '\n').replaceAll('\r', '\n');
return super.processContent(content, metadata, projectDir);
}
/**
* Get instructions for project-specific installation
* @param {string} projectDir - Optional project directory
* @param {string} destDir - Optional destination directory
* @returns {string} Instructions text
*/
getProjectSpecificInstructions(projectDir = null, destDir = null) {
const lines = [
'Project-Specific Codex Configuration',
'',
`Skills installed to: ${destDir || '<project>/.agents/skills'}`,
'',
'Codex automatically discovers skills in .agents/skills/ at and above the current directory and in your home directory.',
'No additional configuration is needed.',
];
return lines.join('\n');
}
/**
* Cleanup Codex configuration - cleans both new .agents/skills and old .codex/prompts
*/
async cleanup(projectDir = null) {
// Clean old .codex/prompts locations
const oldGlobalDir = this.getOldCodexPromptDir(null, 'global');
await this.clearOldBmadFiles(oldGlobalDir);
if (projectDir) {
const oldProjectDir = this.getOldCodexPromptDir(projectDir, 'project');
await this.clearOldBmadFiles(oldProjectDir);
// Clean new .agents/skills location
const destDir = this.getCodexSkillsDir(projectDir);
await this.clearOldBmadSkills(destDir);
}
}
/**
* Install a custom agent launcher for Codex as an Agent Skill
* @param {string} projectDir - Project directory
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root)
* @param {Object} metadata - Agent metadata
* @returns {Object|null} Info about created skill
*/
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
const destDir = this.getCodexSkillsDir(projectDir);
// Skill name from the dash name (without .md)
const skillName = customAgentDashName(agentName).replace(/\.md$/, '');
const skillDir = path.join(destDir, skillName);
await fs.ensureDir(skillDir);
const description = metadata?.description || `${agentName} agent`;
const fm = yaml.stringify({ name: skillName, description }).trimEnd();
const skillContent =
`---\n${fm}\n---\n` +
"\nYou must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.\n" +
'\n<agent-activation CRITICAL="TRUE">\n' +
`1. LOAD the FULL agent file from @${agentPath}\n` +
'2. READ its entire contents - this contains the complete agent persona, menu, and instructions\n' +
'3. FOLLOW every step in the <activation> section precisely\n' +
'4. DISPLAY the welcome/greeting as instructed\n' +
'5. PRESENT the numbered menu\n' +
'6. WAIT for user input before proceeding\n' +
'</agent-activation>\n';
// Write with platform-native line endings
const platformContent = skillContent.replaceAll('\n', os.EOL);
const skillPath = path.join(skillDir, 'SKILL.md');
await fs.writeFile(skillPath, platformContent, 'utf8');
return {
path: path.relative(projectDir, skillPath),
command: `$${skillName}`,
};
}
}
module.exports = { CodexSetup };

View File

@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts');
* Dynamically discovers and loads IDE handlers * Dynamically discovers and loads IDE handlers
* *
* Loading strategy: * Loading strategy:
* 1. Custom installer files (codex.js, github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic * 1. Custom installer files (github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns * 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
*/ */
class IdeManager { class IdeManager {
@ -44,7 +44,7 @@ class IdeManager {
/** /**
* Dynamically load all IDE handlers * Dynamically load all IDE handlers
* 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js, rovodev.js) * 1. Load custom installer files first (github-copilot.js, kilo.js, rovodev.js)
* 2. Load config-driven handlers from platform-codes.yaml * 2. Load config-driven handlers from platform-codes.yaml
*/ */
async loadHandlers() { async loadHandlers() {
@ -58,10 +58,11 @@ class IdeManager {
/** /**
* Load custom installer files (unique installation logic) * Load custom installer files (unique installation logic)
* These files have special installation patterns that don't fit the config-driven model * These files have special installation patterns that don't fit the config-driven model
* Note: codex was migrated to config-driven (platform-codes.yaml) and no longer needs a custom installer
*/ */
async loadCustomInstallerFiles() { async loadCustomInstallerFiles() {
const ideDir = __dirname; const ideDir = __dirname;
const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js', 'rovodev.js']; const customFiles = ['github-copilot.js', 'kilo.js', 'rovodev.js'];
for (const file of customFiles) { for (const file of customFiles) {
const filePath = path.join(ideDir, file); const filePath = path.join(ideDir, file);
@ -189,14 +190,6 @@ class IdeManager {
if (r.tasks > 0) parts.push(`${r.tasks} tasks`); if (r.tasks > 0) parts.push(`${r.tasks} tasks`);
if (r.tools > 0) parts.push(`${r.tools} tools`); if (r.tools > 0) parts.push(`${r.tools} tools`);
detail = parts.join(', '); detail = parts.join(', ');
} else if (handlerResult && handlerResult.counts) {
// Codex handler returns { success, counts: { agents, workflows, tasks }, written }
const c = handlerResult.counts;
const parts = [];
if (c.agents > 0) parts.push(`${c.agents} agents`);
if (c.workflows > 0) parts.push(`${c.workflows} workflows`);
if (c.tasks > 0) parts.push(`${c.tasks} tasks`);
detail = parts.join(', ');
} else if (handlerResult && handlerResult.modes !== undefined) { } else if (handlerResult && handlerResult.modes !== undefined) {
// Kilo handler returns { success, modes, workflows, tasks, tools } // Kilo handler returns { success, modes, workflows, tasks, tools }
const parts = []; const parts = [];

View File

@ -20,8 +20,11 @@ platforms:
category: ide category: ide
description: "Google's AI development environment" description: "Google's AI development environment"
installer: installer:
target_dir: .agent/workflows legacy_targets:
- .agent/workflows
target_dir: .agent/skills
template_type: antigravity template_type: antigravity
skill_format: true
auggie: auggie:
name: "Auggie" name: "Auggie"
@ -29,8 +32,11 @@ platforms:
category: cli category: cli
description: "AI development tool" description: "AI development tool"
installer: installer:
target_dir: .augment/commands legacy_targets:
- .augment/commands
target_dir: .augment/skills
template_type: default template_type: default
skill_format: true
claude-code: claude-code:
name: "Claude Code" name: "Claude Code"
@ -38,8 +44,11 @@ platforms:
category: cli category: cli
description: "Anthropic's official CLI for Claude" description: "Anthropic's official CLI for Claude"
installer: installer:
target_dir: .claude/commands legacy_targets:
- .claude/commands
target_dir: .claude/skills
template_type: default template_type: default
skill_format: true
ancestor_conflict_check: true ancestor_conflict_check: true
cline: cline:
@ -56,7 +65,15 @@ platforms:
preferred: false preferred: false
category: cli category: cli
description: "OpenAI Codex integration" description: "OpenAI Codex integration"
# No installer config - uses custom codex.js installer:
legacy_targets:
- .codex/prompts
- ~/.codex/prompts
target_dir: .agents/skills
template_type: default
skill_format: true
ancestor_conflict_check: true
artifact_types: [agents, workflows, tasks]
codebuddy: codebuddy:
name: "CodeBuddy" name: "CodeBuddy"
@ -82,8 +99,11 @@ platforms:
category: ide category: ide
description: "AI-first code editor" description: "AI-first code editor"
installer: installer:
target_dir: .cursor/commands legacy_targets:
- .cursor/commands
target_dir: .cursor/skills
template_type: default template_type: default
skill_format: true
gemini: gemini:
name: "Gemini CLI" name: "Gemini CLI"
@ -123,8 +143,11 @@ platforms:
category: ide category: ide
description: "Amazon's AI-powered IDE" description: "Amazon's AI-powered IDE"
installer: installer:
target_dir: .kiro/steering legacy_targets:
- .kiro/steering
target_dir: .kiro/skills
template_type: kiro template_type: kiro
skill_format: true
opencode: opencode:
name: "OpenCode" name: "OpenCode"
@ -133,15 +156,14 @@ platforms:
description: "OpenCode terminal coding assistant" description: "OpenCode terminal coding assistant"
installer: installer:
legacy_targets: legacy_targets:
- .opencode/agents
- .opencode/commands
- .opencode/agent - .opencode/agent
- .opencode/command - .opencode/command
targets: target_dir: .opencode/skills
- target_dir: .opencode/agents
template_type: opencode template_type: opencode
artifact_types: [agents] skill_format: true
- target_dir: .opencode/commands ancestor_conflict_check: true
template_type: opencode
artifact_types: [workflows, tasks, tools]
qwen: qwen:
name: "QwenCoder" name: "QwenCoder"
@ -183,8 +205,11 @@ platforms:
category: ide category: ide
description: "AI-powered IDE with cascade flows" description: "AI-powered IDE with cascade flows"
installer: installer:
target_dir: .windsurf/workflows legacy_targets:
- .windsurf/workflows
target_dir: .windsurf/skills
template_type: windsurf template_type: windsurf
skill_format: true
# ============================================================================ # ============================================================================
# Installer Config Schema # Installer Config Schema
@ -203,9 +228,11 @@ platforms:
# artifact_types: [agents, workflows, tasks, tools] # artifact_types: [agents, workflows, tasks, tools]
# artifact_types: array (optional) # Filter which artifacts to install (default: all) # artifact_types: array (optional) # Filter which artifacts to install (default: all)
# skip_existing: boolean (optional) # Skip files that already exist (default: false) # skip_existing: boolean (optional) # Skip files that already exist (default: false)
# skill_format: boolean (optional) # Use directory-per-skill output: <name>/SKILL.md
# # with clean frontmatter (name + description, unquoted)
# ancestor_conflict_check: boolean (optional) # Refuse install when ancestor dir has BMAD files # ancestor_conflict_check: boolean (optional) # Refuse install when ancestor dir has BMAD files
# # in the same target_dir (for IDEs that inherit # # in the same target_dir (for IDEs that inherit
# # commands from parent directories) # # skills from parent directories)
# ============================================================================ # ============================================================================
# Platform Categories # Platform Categories

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,

View File

@ -0,0 +1,215 @@
# Native Skills Migration Checklist
Branch: `refactor/all-is-skills`
Scope: migrate the BMAD-supported platforms that fully support the Agent Skills standard from legacy installer outputs to native skills output.
Current branch status:
- `Claude Code` has already been moved to `.claude/skills`
- `Codex CLI` has already been moved to `.agents/skills`
This checklist now includes those completed platforms plus the remaining full-support platforms.
## Claude Code
Support assumption: full Agent Skills support. BMAD has already migrated from `.claude/commands` to `.claude/skills`.
- [ ] Confirm current implementation still matches Claude Code skills expectations
- [ ] Confirm legacy cleanup for `.claude/commands`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy command output
- [ ] Confirm ancestor conflict protection
- [ ] Implement/extend automated tests as needed
- [ ] Commit any follow-up fixes if required
## Codex CLI
Support assumption: full Agent Skills support. BMAD has already migrated from `.codex/prompts` to `.agents/skills`.
- [ ] Confirm current implementation still matches Codex CLI skills expectations
- [ ] Confirm legacy cleanup for project and global `.codex/prompts`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy prompt output
- [x] Confirm ancestor conflict protection because Codex inherits parent-directory `.agents/skills`
- [ ] Implement/extend automated tests as needed
- [ ] Commit any follow-up fixes if required
## Cursor
Support assumption: full Agent Skills support. BMAD currently installs legacy command files to `.cursor/commands`; target should move to a native skills directory.
- [x] Confirm current Cursor skills path and that BMAD should target `.cursor/skills`
- [x] Implement installer migration to native skills output
- [x] Add legacy cleanup for `.cursor/commands`
- [x] Test fresh install
- [x] Test reinstall/upgrade from legacy command output
- [x] Confirm no ancestor conflict protection is needed because a child workspace surfaced child `.cursor/skills` entries but not a parent-only skill during manual verification
- [ ] Implement/extend automated tests
- [x] Commit
## Windsurf
Support assumption: full Agent Skills support. Windsurf docs confirm workspace skills at `.windsurf/skills` and global skills at `~/.codeium/windsurf/skills`. BMAD has now migrated from `.windsurf/workflows` to `.windsurf/skills`. Manual verification also confirmed that Windsurf custom skills are triggered via `@skill-name`, not slash commands.
- [x] Confirm Windsurf native skills directory as `.windsurf/skills`
- [x] Implement installer migration to native skills output
- [x] Add legacy cleanup for `.windsurf/workflows`
- [x] Test fresh install
- [x] Test reinstall/upgrade from legacy workflow output
- [x] Confirm no ancestor conflict protection is needed because manual Windsurf verification showed child-local `@` skills loaded while a parent-only skill was not inherited
- [x] Implement/extend automated tests
- [x] Commit
## Cline
Support assumption: full Agent Skills support. BMAD currently installs workflow files to `.clinerules/workflows`; target should move to the platform's native skills directory.
- [ ] Confirm current Cline skills path and whether `.cline/skills` is the correct BMAD target
- [ ] Implement installer migration to native skills output
- [ ] Add legacy cleanup for `.clinerules/workflows`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy workflow output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## Google Antigravity
Support assumption: full Agent Skills support. Antigravity docs confirm workspace skills at `.agent/skills/<skill-folder>/` and global skills at `~/.gemini/antigravity/skills/<skill-folder>/`. BMAD has now migrated from `.agent/workflows` to `.agent/skills`.
- [x] Confirm Antigravity native skills path and project/global precedence
- [x] Implement installer migration to native skills output
- [x] Add legacy cleanup for `.agent/workflows`
- [x] Test fresh install
- [x] Test reinstall/upgrade from legacy workflow output
- [x] Confirm no ancestor conflict protection is needed because manual Antigravity verification in `/tmp/antigravity-ancestor-repro/parent/child` showed only the child-local `child-only` skill, with no inherited parent `.agent/skills` entry
- [x] Implement/extend automated tests
- [x] Commit
## Auggie
Support assumption: full Agent Skills support. BMAD currently installs commands to `.augment/commands`; target should move to `.augment/skills`.
- [x] Confirm Auggie native skills path and compatibility loading from `.claude/skills` and `.agents/skills` via Augment docs plus local `auggie --print` repros
- [x] Implement installer migration to native skills output
- [x] Add legacy cleanup for `.augment/commands`
- [x] Test fresh install
- [x] Test reinstall/upgrade from legacy command output
- [x] Confirm no ancestor conflict protection is needed because local `auggie --workspace-root` repro showed child-local `.augment/skills` loading `child-only` but not parent `parent-only`
- [x] Implement/extend automated tests
- [ ] Commit
## CodeBuddy
Support assumption: full Agent Skills support. BMAD currently installs commands to `.codebuddy/commands`; target should move to `.codebuddy/skills`.
- [ ] Confirm CodeBuddy native skills path and any naming/frontmatter requirements
- [ ] Implement installer migration to native skills output
- [ ] Add legacy cleanup for `.codebuddy/commands`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy command output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## Crush
Support assumption: full Agent Skills support. BMAD currently installs commands to `.crush/commands`; target should move to the platform's native skills location.
- [ ] Confirm Crush project-local versus global skills path and BMAD's preferred install target
- [ ] Implement installer migration to native skills output
- [ ] Add legacy cleanup for `.crush/commands`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy command output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## Kiro
Support assumption: full Agent Skills support. Kiro docs confirm project skills at `.kiro/skills/<skill-name>/SKILL.md` and describe steering as a separate rules mechanism, not a required compatibility layer. BMAD has now migrated from `.kiro/steering` to `.kiro/skills`. Manual app verification also confirmed that Kiro can surface skills in Slash when the relevant UI setting is enabled, and that it does not inherit ancestor `.kiro/skills` directories.
- [x] Confirm Kiro skills path and verify BMAD should stop writing steering artifacts for this migration
- [x] Implement installer migration to native skills output
- [x] Add legacy cleanup for `.kiro/steering`
- [x] Test fresh install
- [x] Test reinstall/upgrade from legacy steering output
- [x] Confirm no ancestor conflict protection is needed because manual Kiro verification showed Slash-visible skills from the current workspace only, with no ancestor `.kiro/skills` inheritance
- [x] Implement/extend automated tests
- [x] Commit
## OpenCode
Support assumption: full Agent Skills support. BMAD currently splits output between `.opencode/agents` and `.opencode/commands`; target should consolidate to `.opencode/skills`.
- [x] Confirm OpenCode native skills path and compatibility loading from `.claude/skills` and `.agents/skills` in OpenCode docs and with local `opencode run` repros
- [x] Implement installer migration from multi-target legacy output to single native skills target
- [x] Add legacy cleanup for `.opencode/agents`, `.opencode/commands`, `.opencode/agent`, and `.opencode/command`
- [x] Test fresh install
- [x] Test reinstall/upgrade from split legacy output
- [x] Confirm ancestor conflict protection is required because local `opencode run` repros loaded both child-local `child-only` and ancestor `parent-only`, matching the docs that project-local skill discovery walks upward to the git worktree
- [x] Implement/extend automated tests
- [ ] Commit
## Roo Code
Support assumption: full Agent Skills support. BMAD currently installs commands to `.roo/commands`; target should move to `.roo/skills` or the correct mode-aware skill directories.
- [ ] Confirm Roo native skills path and whether BMAD should use generic or mode-specific skill directories
- [ ] Implement installer migration to native skills output
- [ ] Add legacy cleanup for `.roo/commands`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy command output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## Trae
Support assumption: full Agent Skills support. BMAD currently installs rule files to `.trae/rules`; target should move to the platform's native skills directory.
- [ ] Confirm Trae native skills path and whether the current `.trae/rules` path is still required for compatibility
- [ ] Implement installer migration to native skills output
- [ ] Add legacy cleanup for `.trae/rules`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy rules output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## GitHub Copilot
Support assumption: full Agent Skills support. BMAD currently uses a custom installer that generates `.github/agents`, `.github/prompts`, and `.github/copilot-instructions.md`; target should move to `.github/skills`.
- [ ] Confirm GitHub Copilot native skills path and whether `.github/agents` remains necessary as a compatibility layer
- [ ] Design the migration away from the custom prompt/agent installer model
- [ ] Implement native skills output, ideally with shared config-driven code where practical
- [ ] Add legacy cleanup for `.github/agents`, `.github/prompts`, and any BMAD-owned Copilot instruction file behavior that should be retired
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy custom installer output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## KiloCoder
Support assumption: full Agent Skills support. BMAD currently uses a custom installer that writes `.kilocodemodes` and `.kilocode/workflows`; target should move to native skills output.
- [ ] Confirm KiloCoder native skills path and whether `.kilocodemodes` should be removed entirely or retained temporarily for compatibility
- [ ] Design the migration away from modes plus workflow markdown
- [ ] Implement native skills output
- [ ] Add legacy cleanup for `.kilocode/workflows` and BMAD-owned entries in `.kilocodemodes`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy custom installer output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [ ] Commit
## Summary Gates
- [ ] All full-support BMAD platforms install `SKILL.md` directory-based output
- [ ] No full-support platform still emits BMAD command/workflow/rule files as its primary install format
- [ ] Legacy cleanup paths are defined for every migrated platform
- [ ] Automated coverage exists for config-driven and custom-installer migrations
- [ ] Installer docs and migration notes updated after code changes land