diff --git a/src/bmm/workflows/1-analysis/research/bmad-skill-manifest.yaml b/src/bmm/workflows/1-analysis/research/bmad-skill-manifest.yaml
index 0c815c1bd..02bf825e9 100644
--- a/src/bmm/workflows/1-analysis/research/bmad-skill-manifest.yaml
+++ b/src/bmm/workflows/1-analysis/research/bmad-skill-manifest.yaml
@@ -1,14 +1,14 @@
workflow-domain-research.md:
- canonicalId: bmad-bmm-domain-research
+ canonicalId: bmad-domain-research
type: workflow
- description: "Conduct domain and industry research"
+ description: "Conduct domain and industry research. Use when the user says 'lets create a research report on [domain or industry]'"
workflow-market-research.md:
- canonicalId: bmad-bmm-market-research
+ canonicalId: bmad-market-research
type: workflow
- description: "Conduct market research on competition and customers"
+ description: "Conduct market research on competition and customers. Use when the user says 'create a market research report about [business idea]'"
workflow-technical-research.md:
- canonicalId: bmad-bmm-technical-research
+ canonicalId: bmad-technical-research
type: workflow
- description: "Conduct technical research on technologies and architecture"
+ description: "Conduct technical research on technologies and architecture. Use when the user says 'create a technical research report on [topic]'"
diff --git a/src/bmm/workflows/2-plan-workflows/create-prd/bmad-skill-manifest.yaml b/src/bmm/workflows/2-plan-workflows/create-prd/bmad-skill-manifest.yaml
index fdbe80cd9..aea9910a2 100644
--- a/src/bmm/workflows/2-plan-workflows/create-prd/bmad-skill-manifest.yaml
+++ b/src/bmm/workflows/2-plan-workflows/create-prd/bmad-skill-manifest.yaml
@@ -1,14 +1,14 @@
workflow-create-prd.md:
- canonicalId: bmad-bmm-create-prd
+ canonicalId: bmad-create-prd
type: workflow
- description: "Create a PRD from scratch"
+ description: "Create a PRD from scratch. Use when the user says 'lets create a product requirements document' or 'I want to create a new PRD'"
workflow-edit-prd.md:
- canonicalId: bmad-bmm-edit-prd
+ canonicalId: bmad-edit-prd
type: workflow
- description: "Edit an existing PRD"
+ description: "Edit an existing PRD. Use when the user says 'edit this PRD'"
workflow-validate-prd.md:
- canonicalId: bmad-bmm-validate-prd
+ canonicalId: bmad-validate-prd
type: workflow
- description: "Validate a PRD against standards"
+ description: "Validate a PRD against standards. Use when the user says 'validate this PRD' or 'run PRD validation'"
diff --git a/test/test-installation-components.js b/test/test-installation-components.js
index 63f2567f5..56f37b365 100644
--- a/test/test-installation-components.js
+++ b/test/test-installation-components.js
@@ -457,9 +457,311 @@ async function runTests() {
console.log('');
// ============================================================
- // Test 9: OpenCode Ancestor Conflict
+ // Test 9: Claude Code Native Skills Install
// ============================================================
- console.log(`${colors.yellow}Test Suite 9: OpenCode Ancestor Conflict${colors.reset}\n`);
+ console.log(`${colors.yellow}Test Suite 9: Claude Code Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes9 = await loadPlatformCodes();
+ const claudeInstaller = platformCodes9.platforms['claude-code']?.installer;
+
+ assert(claudeInstaller?.target_dir === '.claude/skills', 'Claude Code target_dir uses native skills path');
+
+ assert(claudeInstaller?.skill_format === true, 'Claude Code installer enables native skill output');
+
+ assert(claudeInstaller?.ancestor_conflict_check === true, 'Claude Code installer enables ancestor conflict checks');
+
+ assert(
+ Array.isArray(claudeInstaller?.legacy_targets) && claudeInstaller.legacy_targets.includes('.claude/commands'),
+ 'Claude Code installer cleans legacy command output',
+ );
+
+ const tempProjectDir9 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-claude-code-test-'));
+ const installedBmadDir9 = await createTestBmadFixture();
+ const legacyDir9 = path.join(tempProjectDir9, '.claude', 'commands');
+ await fs.ensureDir(legacyDir9);
+ await fs.writeFile(path.join(legacyDir9, 'bmad-legacy.md'), 'legacy\n');
+
+ const ideManager9 = new IdeManager();
+ await ideManager9.ensureInitialized();
+ const result9 = await ideManager9.setup('claude-code', tempProjectDir9, installedBmadDir9, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result9.success === true, 'Claude Code setup succeeds against temp project');
+
+ const skillFile9 = path.join(tempProjectDir9, '.claude', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile9), 'Claude Code install writes SKILL.md directory output');
+
+ // Verify name frontmatter matches directory name
+ const skillContent9 = await fs.readFile(skillFile9, 'utf8');
+ const nameMatch9 = skillContent9.match(/^name:\s*(.+)$/m);
+ assert(nameMatch9 && nameMatch9[1].trim() === 'bmad-master', 'Claude Code skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(legacyDir9)), 'Claude Code setup removes legacy commands dir');
+
+ await fs.remove(tempProjectDir9);
+ await fs.remove(installedBmadDir9);
+ } catch (error) {
+ assert(false, 'Claude Code native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Test 10: Claude Code Ancestor Conflict
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 10: Claude Code Ancestor Conflict${colors.reset}\n`);
+
+ try {
+ const tempRoot10 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-claude-code-ancestor-test-'));
+ const parentProjectDir10 = path.join(tempRoot10, 'parent');
+ const childProjectDir10 = path.join(parentProjectDir10, 'child');
+ const installedBmadDir10 = await createTestBmadFixture();
+
+ await fs.ensureDir(path.join(parentProjectDir10, '.git'));
+ await fs.ensureDir(path.join(parentProjectDir10, '.claude', 'skills', 'bmad-existing'));
+ await fs.ensureDir(childProjectDir10);
+ await fs.writeFile(path.join(parentProjectDir10, '.claude', 'skills', 'bmad-existing', 'SKILL.md'), 'legacy\n');
+
+ const ideManager10 = new IdeManager();
+ await ideManager10.ensureInitialized();
+ const result10 = await ideManager10.setup('claude-code', childProjectDir10, installedBmadDir10, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+ const expectedConflictDir10 = await fs.realpath(path.join(parentProjectDir10, '.claude', 'skills'));
+
+ assert(result10.success === false, 'Claude Code setup refuses install when ancestor skills already exist');
+ assert(result10.handlerResult?.reason === 'ancestor-conflict', 'Claude Code ancestor rejection reports ancestor-conflict reason');
+ assert(
+ result10.handlerResult?.conflictDir === expectedConflictDir10,
+ 'Claude Code ancestor rejection points at ancestor .claude/skills dir',
+ );
+
+ await fs.remove(tempRoot10);
+ await fs.remove(installedBmadDir10);
+ } catch (error) {
+ assert(false, 'Claude Code ancestor conflict protection test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Test 11: Codex Native Skills Install
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 11: Codex Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes11 = await loadPlatformCodes();
+ const codexInstaller = platformCodes11.platforms.codex?.installer;
+
+ assert(codexInstaller?.target_dir === '.agents/skills', 'Codex target_dir uses native skills path');
+
+ assert(codexInstaller?.skill_format === true, 'Codex installer enables native skill output');
+
+ assert(codexInstaller?.ancestor_conflict_check === true, 'Codex installer enables ancestor conflict checks');
+
+ assert(
+ Array.isArray(codexInstaller?.legacy_targets) && codexInstaller.legacy_targets.includes('.codex/prompts'),
+ 'Codex installer cleans legacy prompt output',
+ );
+
+ const tempProjectDir11 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-test-'));
+ const installedBmadDir11 = await createTestBmadFixture();
+ const legacyDir11 = path.join(tempProjectDir11, '.codex', 'prompts');
+ await fs.ensureDir(legacyDir11);
+ await fs.writeFile(path.join(legacyDir11, 'bmad-legacy.md'), 'legacy\n');
+
+ const ideManager11 = new IdeManager();
+ await ideManager11.ensureInitialized();
+ const result11 = await ideManager11.setup('codex', tempProjectDir11, installedBmadDir11, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result11.success === true, 'Codex setup succeeds against temp project');
+
+ const skillFile11 = path.join(tempProjectDir11, '.agents', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile11), 'Codex install writes SKILL.md directory output');
+
+ // Verify name frontmatter matches directory name
+ const skillContent11 = await fs.readFile(skillFile11, 'utf8');
+ const nameMatch11 = skillContent11.match(/^name:\s*(.+)$/m);
+ assert(nameMatch11 && nameMatch11[1].trim() === 'bmad-master', 'Codex skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(legacyDir11)), 'Codex setup removes legacy prompts dir');
+
+ await fs.remove(tempProjectDir11);
+ await fs.remove(installedBmadDir11);
+ } catch (error) {
+ assert(false, 'Codex native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Test 12: Codex Ancestor Conflict
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 12: Codex Ancestor Conflict${colors.reset}\n`);
+
+ try {
+ const tempRoot12 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-ancestor-test-'));
+ const parentProjectDir12 = path.join(tempRoot12, 'parent');
+ const childProjectDir12 = path.join(parentProjectDir12, 'child');
+ const installedBmadDir12 = await createTestBmadFixture();
+
+ await fs.ensureDir(path.join(parentProjectDir12, '.git'));
+ await fs.ensureDir(path.join(parentProjectDir12, '.agents', 'skills', 'bmad-existing'));
+ await fs.ensureDir(childProjectDir12);
+ await fs.writeFile(path.join(parentProjectDir12, '.agents', 'skills', 'bmad-existing', 'SKILL.md'), 'legacy\n');
+
+ const ideManager12 = new IdeManager();
+ await ideManager12.ensureInitialized();
+ const result12 = await ideManager12.setup('codex', childProjectDir12, installedBmadDir12, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+ const expectedConflictDir12 = await fs.realpath(path.join(parentProjectDir12, '.agents', 'skills'));
+
+ assert(result12.success === false, 'Codex setup refuses install when ancestor skills already exist');
+ assert(result12.handlerResult?.reason === 'ancestor-conflict', 'Codex ancestor rejection reports ancestor-conflict reason');
+ assert(result12.handlerResult?.conflictDir === expectedConflictDir12, 'Codex ancestor rejection points at ancestor .agents/skills dir');
+
+ await fs.remove(tempRoot12);
+ await fs.remove(installedBmadDir12);
+ } catch (error) {
+ assert(false, 'Codex ancestor conflict protection test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Test 13: Cursor Native Skills Install
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 13: Cursor Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes13 = await loadPlatformCodes();
+ const cursorInstaller = platformCodes13.platforms.cursor?.installer;
+
+ assert(cursorInstaller?.target_dir === '.cursor/skills', 'Cursor target_dir uses native skills path');
+
+ assert(cursorInstaller?.skill_format === true, 'Cursor installer enables native skill output');
+
+ assert(
+ Array.isArray(cursorInstaller?.legacy_targets) && cursorInstaller.legacy_targets.includes('.cursor/commands'),
+ 'Cursor installer cleans legacy command output',
+ );
+
+ assert(!cursorInstaller?.ancestor_conflict_check, 'Cursor installer does not enable ancestor conflict checks');
+
+ const tempProjectDir13c = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cursor-test-'));
+ const installedBmadDir13c = await createTestBmadFixture();
+ const legacyDir13c = path.join(tempProjectDir13c, '.cursor', 'commands');
+ await fs.ensureDir(legacyDir13c);
+ await fs.writeFile(path.join(legacyDir13c, 'bmad-legacy.md'), 'legacy\n');
+
+ const ideManager13c = new IdeManager();
+ await ideManager13c.ensureInitialized();
+ const result13c = await ideManager13c.setup('cursor', tempProjectDir13c, installedBmadDir13c, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result13c.success === true, 'Cursor setup succeeds against temp project');
+
+ const skillFile13c = path.join(tempProjectDir13c, '.cursor', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile13c), 'Cursor install writes SKILL.md directory output');
+
+ // Verify name frontmatter matches directory name
+ const skillContent13c = await fs.readFile(skillFile13c, 'utf8');
+ const nameMatch13c = skillContent13c.match(/^name:\s*(.+)$/m);
+ assert(nameMatch13c && nameMatch13c[1].trim() === 'bmad-master', 'Cursor skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(legacyDir13c)), 'Cursor setup removes legacy commands dir');
+
+ await fs.remove(tempProjectDir13c);
+ await fs.remove(installedBmadDir13c);
+ } catch (error) {
+ assert(false, 'Cursor native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Test 14: Roo Code Native Skills Install
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 14: Roo Code Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes13 = await loadPlatformCodes();
+ const rooInstaller = platformCodes13.platforms.roo?.installer;
+
+ assert(rooInstaller?.target_dir === '.roo/skills', 'Roo target_dir uses native skills path');
+
+ assert(rooInstaller?.skill_format === true, 'Roo installer enables native skill output');
+
+ assert(
+ Array.isArray(rooInstaller?.legacy_targets) && rooInstaller.legacy_targets.includes('.roo/commands'),
+ 'Roo installer cleans legacy command output',
+ );
+
+ const tempProjectDir13 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-roo-test-'));
+ const installedBmadDir13 = await createTestBmadFixture();
+ const legacyDir13 = path.join(tempProjectDir13, '.roo', 'commands', 'bmad-legacy-dir');
+ await fs.ensureDir(legacyDir13);
+ await fs.writeFile(path.join(tempProjectDir13, '.roo', 'commands', 'bmad-legacy.md'), 'legacy\n');
+ await fs.writeFile(path.join(legacyDir13, 'SKILL.md'), 'legacy\n');
+
+ const ideManager13 = new IdeManager();
+ await ideManager13.ensureInitialized();
+ const result13 = await ideManager13.setup('roo', tempProjectDir13, installedBmadDir13, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result13.success === true, 'Roo setup succeeds against temp project');
+
+ const skillFile13 = path.join(tempProjectDir13, '.roo', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile13), 'Roo install writes SKILL.md directory output');
+
+ // Verify name frontmatter matches directory name (Roo constraint: lowercase alphanumeric + hyphens)
+ const skillContent13 = await fs.readFile(skillFile13, 'utf8');
+ const nameMatch13 = skillContent13.match(/^name:\s*(.+)$/m);
+ assert(
+ nameMatch13 && nameMatch13[1].trim() === 'bmad-master',
+ 'Roo skill name frontmatter matches directory name exactly (lowercase alphanumeric + hyphens)',
+ );
+
+ assert(!(await fs.pathExists(path.join(tempProjectDir13, '.roo', 'commands'))), 'Roo setup removes legacy commands dir');
+
+ // Reinstall/upgrade: run setup again over existing skills output
+ const result13b = await ideManager13.setup('roo', tempProjectDir13, installedBmadDir13, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result13b.success === true, 'Roo reinstall/upgrade succeeds over existing skills');
+ assert(await fs.pathExists(skillFile13), 'Roo reinstall preserves SKILL.md output');
+
+ await fs.remove(tempProjectDir13);
+ await fs.remove(installedBmadDir13);
+ } catch (error) {
+ assert(false, 'Roo native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Test 15: OpenCode Ancestor Conflict
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 15: OpenCode Ancestor Conflict${colors.reset}\n`);
try {
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-opencode-ancestor-test-'));
@@ -496,9 +798,9 @@ async function runTests() {
console.log('');
// ============================================================
- // Test 10: QA Agent Compilation
+ // Test 16: QA Agent Compilation
// ============================================================
- console.log(`${colors.yellow}Test Suite 10: QA Agent Compilation${colors.reset}\n`);
+ console.log(`${colors.yellow}Test Suite 16: QA Agent Compilation${colors.reset}\n`);
try {
const builder = new YamlXmlBuilder();
@@ -524,6 +826,680 @@ async function runTests() {
console.log('');
+ // ============================================================
+ // Test 17: GitHub Copilot Native Skills Install
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 17: GitHub Copilot Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes17 = await loadPlatformCodes();
+ const copilotInstaller = platformCodes17.platforms['github-copilot']?.installer;
+
+ assert(copilotInstaller?.target_dir === '.github/skills', 'GitHub Copilot target_dir uses native skills path');
+
+ assert(copilotInstaller?.skill_format === true, 'GitHub Copilot installer enables native skill output');
+
+ assert(
+ Array.isArray(copilotInstaller?.legacy_targets) && copilotInstaller.legacy_targets.includes('.github/agents'),
+ 'GitHub Copilot installer cleans legacy agents output',
+ );
+
+ assert(
+ Array.isArray(copilotInstaller?.legacy_targets) && copilotInstaller.legacy_targets.includes('.github/prompts'),
+ 'GitHub Copilot installer cleans legacy prompts output',
+ );
+
+ const tempProjectDir17 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-copilot-test-'));
+ const installedBmadDir17 = await createTestBmadFixture();
+
+ // Create legacy .github/agents/ and .github/prompts/ files
+ const legacyAgentsDir17 = path.join(tempProjectDir17, '.github', 'agents');
+ const legacyPromptsDir17 = path.join(tempProjectDir17, '.github', 'prompts');
+ await fs.ensureDir(legacyAgentsDir17);
+ await fs.ensureDir(legacyPromptsDir17);
+ await fs.writeFile(path.join(legacyAgentsDir17, 'bmad-legacy.agent.md'), 'legacy agent\n');
+ await fs.writeFile(path.join(legacyPromptsDir17, 'bmad-legacy.prompt.md'), 'legacy prompt\n');
+
+ // Create legacy copilot-instructions.md with BMAD markers
+ const copilotInstructionsPath17 = path.join(tempProjectDir17, '.github', 'copilot-instructions.md');
+ await fs.writeFile(
+ copilotInstructionsPath17,
+ 'User content before\n\nBMAD generated content\n\nUser content after\n',
+ );
+
+ const ideManager17 = new IdeManager();
+ await ideManager17.ensureInitialized();
+ const result17 = await ideManager17.setup('github-copilot', tempProjectDir17, installedBmadDir17, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result17.success === true, 'GitHub Copilot setup succeeds against temp project');
+
+ const skillFile17 = path.join(tempProjectDir17, '.github', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile17), 'GitHub Copilot install writes SKILL.md directory output');
+
+ // Verify name frontmatter matches directory name
+ const skillContent17 = await fs.readFile(skillFile17, 'utf8');
+ const nameMatch17 = skillContent17.match(/^name:\s*(.+)$/m);
+ assert(nameMatch17 && nameMatch17[1].trim() === 'bmad-master', 'GitHub Copilot skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(legacyAgentsDir17)), 'GitHub Copilot setup removes legacy agents dir');
+
+ assert(!(await fs.pathExists(legacyPromptsDir17)), 'GitHub Copilot setup removes legacy prompts dir');
+
+ // Verify copilot-instructions.md BMAD markers were stripped but user content preserved
+ const cleanedInstructions17 = await fs.readFile(copilotInstructionsPath17, 'utf8');
+ assert(
+ !cleanedInstructions17.includes('BMAD:START') && !cleanedInstructions17.includes('BMAD generated content'),
+ 'GitHub Copilot setup strips BMAD markers from copilot-instructions.md',
+ );
+ assert(
+ cleanedInstructions17.includes('User content before') && cleanedInstructions17.includes('User content after'),
+ 'GitHub Copilot setup preserves user content in copilot-instructions.md',
+ );
+
+ await fs.remove(tempProjectDir17);
+ await fs.remove(installedBmadDir17);
+ } catch (error) {
+ assert(false, 'GitHub Copilot native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Test 18: Cline Native Skills Install
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 18: Cline Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes18 = await loadPlatformCodes();
+ const clineInstaller = platformCodes18.platforms.cline?.installer;
+
+ assert(clineInstaller?.target_dir === '.cline/skills', 'Cline target_dir uses native skills path');
+
+ assert(clineInstaller?.skill_format === true, 'Cline installer enables native skill output');
+
+ assert(
+ Array.isArray(clineInstaller?.legacy_targets) && clineInstaller.legacy_targets.includes('.clinerules/workflows'),
+ 'Cline installer cleans legacy workflow output',
+ );
+
+ const tempProjectDir18 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-cline-test-'));
+ const installedBmadDir18 = await createTestBmadFixture();
+ const legacyDir18 = path.join(tempProjectDir18, '.clinerules', 'workflows', 'bmad-legacy-dir');
+ await fs.ensureDir(legacyDir18);
+ await fs.writeFile(path.join(tempProjectDir18, '.clinerules', 'workflows', 'bmad-legacy.md'), 'legacy\n');
+ await fs.writeFile(path.join(legacyDir18, 'SKILL.md'), 'legacy\n');
+
+ const ideManager18 = new IdeManager();
+ await ideManager18.ensureInitialized();
+ const result18 = await ideManager18.setup('cline', tempProjectDir18, installedBmadDir18, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result18.success === true, 'Cline setup succeeds against temp project');
+
+ const skillFile18 = path.join(tempProjectDir18, '.cline', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile18), 'Cline install writes SKILL.md directory output');
+
+ // Verify name frontmatter matches directory name
+ const skillContent18 = await fs.readFile(skillFile18, 'utf8');
+ const nameMatch18 = skillContent18.match(/^name:\s*(.+)$/m);
+ assert(nameMatch18 && nameMatch18[1].trim() === 'bmad-master', 'Cline skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(path.join(tempProjectDir18, '.clinerules', 'workflows'))), 'Cline setup removes legacy workflows dir');
+
+ // Reinstall/upgrade: run setup again over existing skills output
+ const result18b = await ideManager18.setup('cline', tempProjectDir18, installedBmadDir18, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result18b.success === true, 'Cline reinstall/upgrade succeeds over existing skills');
+ assert(await fs.pathExists(skillFile18), 'Cline reinstall preserves SKILL.md output');
+
+ await fs.remove(tempProjectDir18);
+ await fs.remove(installedBmadDir18);
+ } catch (error) {
+ assert(false, 'Cline native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Test 19: CodeBuddy Native Skills Install
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 19: CodeBuddy Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes19 = await loadPlatformCodes();
+ const codebuddyInstaller = platformCodes19.platforms.codebuddy?.installer;
+
+ assert(codebuddyInstaller?.target_dir === '.codebuddy/skills', 'CodeBuddy target_dir uses native skills path');
+
+ assert(codebuddyInstaller?.skill_format === true, 'CodeBuddy installer enables native skill output');
+
+ assert(
+ Array.isArray(codebuddyInstaller?.legacy_targets) && codebuddyInstaller.legacy_targets.includes('.codebuddy/commands'),
+ 'CodeBuddy installer cleans legacy command output',
+ );
+
+ const tempProjectDir19 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codebuddy-test-'));
+ const installedBmadDir19 = await createTestBmadFixture();
+ const legacyDir19 = path.join(tempProjectDir19, '.codebuddy', 'commands', 'bmad-legacy-dir');
+ await fs.ensureDir(legacyDir19);
+ await fs.writeFile(path.join(tempProjectDir19, '.codebuddy', 'commands', 'bmad-legacy.md'), 'legacy\n');
+ await fs.writeFile(path.join(legacyDir19, 'SKILL.md'), 'legacy\n');
+
+ const ideManager19 = new IdeManager();
+ await ideManager19.ensureInitialized();
+ const result19 = await ideManager19.setup('codebuddy', tempProjectDir19, installedBmadDir19, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result19.success === true, 'CodeBuddy setup succeeds against temp project');
+
+ const skillFile19 = path.join(tempProjectDir19, '.codebuddy', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile19), 'CodeBuddy install writes SKILL.md directory output');
+
+ const skillContent19 = await fs.readFile(skillFile19, 'utf8');
+ const nameMatch19 = skillContent19.match(/^name:\s*(.+)$/m);
+ assert(nameMatch19 && nameMatch19[1].trim() === 'bmad-master', 'CodeBuddy skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(path.join(tempProjectDir19, '.codebuddy', 'commands'))), 'CodeBuddy setup removes legacy commands dir');
+
+ const result19b = await ideManager19.setup('codebuddy', tempProjectDir19, installedBmadDir19, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result19b.success === true, 'CodeBuddy reinstall/upgrade succeeds over existing skills');
+ assert(await fs.pathExists(skillFile19), 'CodeBuddy reinstall preserves SKILL.md output');
+
+ await fs.remove(tempProjectDir19);
+ await fs.remove(installedBmadDir19);
+ } catch (error) {
+ assert(false, 'CodeBuddy native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Test 20: Crush Native Skills Install
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 20: Crush Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes20 = await loadPlatformCodes();
+ const crushInstaller = platformCodes20.platforms.crush?.installer;
+
+ assert(crushInstaller?.target_dir === '.crush/skills', 'Crush target_dir uses native skills path');
+
+ assert(crushInstaller?.skill_format === true, 'Crush installer enables native skill output');
+
+ assert(
+ Array.isArray(crushInstaller?.legacy_targets) && crushInstaller.legacy_targets.includes('.crush/commands'),
+ 'Crush installer cleans legacy command output',
+ );
+
+ const tempProjectDir20 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-crush-test-'));
+ const installedBmadDir20 = await createTestBmadFixture();
+ const legacyDir20 = path.join(tempProjectDir20, '.crush', 'commands', 'bmad-legacy-dir');
+ await fs.ensureDir(legacyDir20);
+ await fs.writeFile(path.join(tempProjectDir20, '.crush', 'commands', 'bmad-legacy.md'), 'legacy\n');
+ await fs.writeFile(path.join(legacyDir20, 'SKILL.md'), 'legacy\n');
+
+ const ideManager20 = new IdeManager();
+ await ideManager20.ensureInitialized();
+ const result20 = await ideManager20.setup('crush', tempProjectDir20, installedBmadDir20, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result20.success === true, 'Crush setup succeeds against temp project');
+
+ const skillFile20 = path.join(tempProjectDir20, '.crush', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile20), 'Crush install writes SKILL.md directory output');
+
+ const skillContent20 = await fs.readFile(skillFile20, 'utf8');
+ const nameMatch20 = skillContent20.match(/^name:\s*(.+)$/m);
+ assert(nameMatch20 && nameMatch20[1].trim() === 'bmad-master', 'Crush skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(path.join(tempProjectDir20, '.crush', 'commands'))), 'Crush setup removes legacy commands dir');
+
+ const result20b = await ideManager20.setup('crush', tempProjectDir20, installedBmadDir20, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result20b.success === true, 'Crush reinstall/upgrade succeeds over existing skills');
+ assert(await fs.pathExists(skillFile20), 'Crush reinstall preserves SKILL.md output');
+
+ await fs.remove(tempProjectDir20);
+ await fs.remove(installedBmadDir20);
+ } catch (error) {
+ assert(false, 'Crush native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Test 21: Trae Native Skills Install
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 21: Trae Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes21 = await loadPlatformCodes();
+ const traeInstaller = platformCodes21.platforms.trae?.installer;
+
+ assert(traeInstaller?.target_dir === '.trae/skills', 'Trae target_dir uses native skills path');
+
+ assert(traeInstaller?.skill_format === true, 'Trae installer enables native skill output');
+
+ assert(
+ Array.isArray(traeInstaller?.legacy_targets) && traeInstaller.legacy_targets.includes('.trae/rules'),
+ 'Trae installer cleans legacy rules output',
+ );
+
+ const tempProjectDir21 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-trae-test-'));
+ const installedBmadDir21 = await createTestBmadFixture();
+ const legacyDir21 = path.join(tempProjectDir21, '.trae', 'rules');
+ await fs.ensureDir(legacyDir21);
+ await fs.writeFile(path.join(legacyDir21, 'bmad-legacy.md'), 'legacy\n');
+
+ const ideManager21 = new IdeManager();
+ await ideManager21.ensureInitialized();
+ const result21 = await ideManager21.setup('trae', tempProjectDir21, installedBmadDir21, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result21.success === true, 'Trae setup succeeds against temp project');
+
+ const skillFile21 = path.join(tempProjectDir21, '.trae', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile21), 'Trae install writes SKILL.md directory output');
+
+ const skillContent21 = await fs.readFile(skillFile21, 'utf8');
+ const nameMatch21 = skillContent21.match(/^name:\s*(.+)$/m);
+ assert(nameMatch21 && nameMatch21[1].trim() === 'bmad-master', 'Trae skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(path.join(tempProjectDir21, '.trae', 'rules'))), 'Trae setup removes legacy rules dir');
+
+ const result21b = await ideManager21.setup('trae', tempProjectDir21, installedBmadDir21, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result21b.success === true, 'Trae reinstall/upgrade succeeds over existing skills');
+ assert(await fs.pathExists(skillFile21), 'Trae reinstall preserves SKILL.md output');
+
+ await fs.remove(tempProjectDir21);
+ await fs.remove(installedBmadDir21);
+ } catch (error) {
+ assert(false, 'Trae native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Suite 22: KiloCoder Suspended
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 22: KiloCoder Suspended${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes22 = await loadPlatformCodes();
+ const kiloConfig22 = platformCodes22.platforms.kilo;
+
+ assert(typeof kiloConfig22?.suspended === 'string', 'KiloCoder has a suspended message in platform config');
+
+ assert(kiloConfig22?.installer?.target_dir === '.kilocode/skills', 'KiloCoder retains target_dir config for future use');
+
+ const ideManager22 = new IdeManager();
+ await ideManager22.ensureInitialized();
+
+ // Should not appear in available IDEs
+ const availableIdes22 = ideManager22.getAvailableIdes();
+ assert(!availableIdes22.some((ide) => ide.value === 'kilo'), 'KiloCoder is hidden from IDE selection');
+
+ // Setup should be blocked but legacy files should be cleaned up
+ const tempProjectDir22 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kilo-test-'));
+ const installedBmadDir22 = await createTestBmadFixture();
+
+ // Pre-populate legacy Kilo artifacts that should be cleaned up
+ const legacyDir22 = path.join(tempProjectDir22, '.kilocode', 'workflows');
+ await fs.ensureDir(legacyDir22);
+ await fs.writeFile(path.join(legacyDir22, 'bmad-legacy.md'), 'legacy\n');
+
+ const result22 = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result22.success === false, 'KiloCoder setup is blocked when suspended');
+ assert(result22.error === 'suspended', 'KiloCoder setup returns suspended error');
+
+ // Should not write new skill files
+ assert(
+ !(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'skills'))),
+ 'KiloCoder does not create skills directory when suspended',
+ );
+
+ // Legacy files should be cleaned up
+ assert(
+ !(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'workflows'))),
+ 'KiloCoder legacy workflows are cleaned up even when suspended',
+ );
+
+ await fs.remove(tempProjectDir22);
+ await fs.remove(installedBmadDir22);
+ } catch (error) {
+ assert(false, 'KiloCoder suspended test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Suite 23: Gemini CLI Native Skills
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 23: Gemini CLI Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes23 = await loadPlatformCodes();
+ const geminiInstaller = platformCodes23.platforms.gemini?.installer;
+
+ assert(geminiInstaller?.target_dir === '.gemini/skills', 'Gemini target_dir uses native skills path');
+
+ assert(geminiInstaller?.skill_format === true, 'Gemini installer enables native skill output');
+
+ assert(
+ Array.isArray(geminiInstaller?.legacy_targets) && geminiInstaller.legacy_targets.includes('.gemini/commands'),
+ 'Gemini installer cleans legacy commands output',
+ );
+
+ const tempProjectDir23 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-gemini-test-'));
+ const installedBmadDir23 = await createTestBmadFixture();
+ const legacyDir23 = path.join(tempProjectDir23, '.gemini', 'commands');
+ await fs.ensureDir(legacyDir23);
+ await fs.writeFile(path.join(legacyDir23, 'bmad-legacy.toml'), 'legacy\n');
+
+ const ideManager23 = new IdeManager();
+ await ideManager23.ensureInitialized();
+ const result23 = await ideManager23.setup('gemini', tempProjectDir23, installedBmadDir23, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result23.success === true, 'Gemini setup succeeds against temp project');
+
+ const skillFile23 = path.join(tempProjectDir23, '.gemini', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile23), 'Gemini install writes SKILL.md directory output');
+
+ const skillContent23 = await fs.readFile(skillFile23, 'utf8');
+ const nameMatch23 = skillContent23.match(/^name:\s*(.+)$/m);
+ assert(nameMatch23 && nameMatch23[1].trim() === 'bmad-master', 'Gemini skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(path.join(tempProjectDir23, '.gemini', 'commands'))), 'Gemini setup removes legacy commands dir');
+
+ const result23b = await ideManager23.setup('gemini', tempProjectDir23, installedBmadDir23, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result23b.success === true, 'Gemini reinstall/upgrade succeeds over existing skills');
+ assert(await fs.pathExists(skillFile23), 'Gemini reinstall preserves SKILL.md output');
+
+ await fs.remove(tempProjectDir23);
+ await fs.remove(installedBmadDir23);
+ } catch (error) {
+ assert(false, 'Gemini native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Suite 24: iFlow Native Skills
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 24: iFlow Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes24 = await loadPlatformCodes();
+ const iflowInstaller = platformCodes24.platforms.iflow?.installer;
+
+ assert(iflowInstaller?.target_dir === '.iflow/skills', 'iFlow target_dir uses native skills path');
+ assert(iflowInstaller?.skill_format === true, 'iFlow installer enables native skill output');
+ assert(
+ Array.isArray(iflowInstaller?.legacy_targets) && iflowInstaller.legacy_targets.includes('.iflow/commands'),
+ 'iFlow installer cleans legacy commands output',
+ );
+
+ const tempProjectDir24 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-iflow-test-'));
+ const installedBmadDir24 = await createTestBmadFixture();
+ const legacyDir24 = path.join(tempProjectDir24, '.iflow', 'commands');
+ await fs.ensureDir(legacyDir24);
+ await fs.writeFile(path.join(legacyDir24, 'bmad-legacy.md'), 'legacy\n');
+
+ const ideManager24 = new IdeManager();
+ await ideManager24.ensureInitialized();
+ const result24 = await ideManager24.setup('iflow', tempProjectDir24, installedBmadDir24, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result24.success === true, 'iFlow setup succeeds against temp project');
+
+ const skillFile24 = path.join(tempProjectDir24, '.iflow', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile24), 'iFlow install writes SKILL.md directory output');
+
+ // Verify name frontmatter matches directory name
+ const skillContent24 = await fs.readFile(skillFile24, 'utf8');
+ const nameMatch24 = skillContent24.match(/^name:\s*(.+)$/m);
+ assert(nameMatch24 && nameMatch24[1].trim() === 'bmad-master', 'iFlow skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(path.join(tempProjectDir24, '.iflow', 'commands'))), 'iFlow setup removes legacy commands dir');
+
+ await fs.remove(tempProjectDir24);
+ await fs.remove(installedBmadDir24);
+ } catch (error) {
+ assert(false, 'iFlow native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Suite 25: QwenCoder Native Skills
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 25: QwenCoder Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes25 = await loadPlatformCodes();
+ const qwenInstaller = platformCodes25.platforms.qwen?.installer;
+
+ assert(qwenInstaller?.target_dir === '.qwen/skills', 'QwenCoder target_dir uses native skills path');
+ assert(qwenInstaller?.skill_format === true, 'QwenCoder installer enables native skill output');
+ assert(
+ Array.isArray(qwenInstaller?.legacy_targets) && qwenInstaller.legacy_targets.includes('.qwen/commands'),
+ 'QwenCoder installer cleans legacy commands output',
+ );
+
+ const tempProjectDir25 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-qwen-test-'));
+ const installedBmadDir25 = await createTestBmadFixture();
+ const legacyDir25 = path.join(tempProjectDir25, '.qwen', 'commands');
+ await fs.ensureDir(legacyDir25);
+ await fs.writeFile(path.join(legacyDir25, 'bmad-legacy.md'), 'legacy\n');
+
+ const ideManager25 = new IdeManager();
+ await ideManager25.ensureInitialized();
+ const result25 = await ideManager25.setup('qwen', tempProjectDir25, installedBmadDir25, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result25.success === true, 'QwenCoder setup succeeds against temp project');
+
+ const skillFile25 = path.join(tempProjectDir25, '.qwen', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile25), 'QwenCoder install writes SKILL.md directory output');
+
+ // Verify name frontmatter matches directory name
+ const skillContent25 = await fs.readFile(skillFile25, 'utf8');
+ const nameMatch25 = skillContent25.match(/^name:\s*(.+)$/m);
+ assert(nameMatch25 && nameMatch25[1].trim() === 'bmad-master', 'QwenCoder skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(path.join(tempProjectDir25, '.qwen', 'commands'))), 'QwenCoder setup removes legacy commands dir');
+
+ await fs.remove(tempProjectDir25);
+ await fs.remove(installedBmadDir25);
+ } catch (error) {
+ assert(false, 'QwenCoder native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Suite 26: Rovo Dev Native Skills
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 26: Rovo Dev Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes26 = await loadPlatformCodes();
+ const rovoInstaller = platformCodes26.platforms['rovo-dev']?.installer;
+
+ assert(rovoInstaller?.target_dir === '.rovodev/skills', 'Rovo Dev target_dir uses native skills path');
+ assert(rovoInstaller?.skill_format === true, 'Rovo Dev installer enables native skill output');
+ assert(
+ Array.isArray(rovoInstaller?.legacy_targets) && rovoInstaller.legacy_targets.includes('.rovodev/workflows'),
+ 'Rovo Dev installer cleans legacy workflows output',
+ );
+
+ const tempProjectDir26 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-rovodev-test-'));
+ const installedBmadDir26 = await createTestBmadFixture();
+ const legacyDir26 = path.join(tempProjectDir26, '.rovodev', 'workflows');
+ await fs.ensureDir(legacyDir26);
+ await fs.writeFile(path.join(legacyDir26, 'bmad-legacy.md'), 'legacy\n');
+
+ // Create a prompts.yml with BMAD entries and a user entry
+ const yaml26 = require('yaml');
+ const promptsPath26 = path.join(tempProjectDir26, '.rovodev', 'prompts.yml');
+ const promptsContent26 = yaml26.stringify({
+ prompts: [
+ { name: 'bmad-bmm-create-prd', description: 'BMAD workflow', content_file: 'workflows/bmad-bmm-create-prd.md' },
+ { name: 'my-custom-prompt', description: 'User prompt', content_file: 'custom.md' },
+ ],
+ });
+ await fs.writeFile(promptsPath26, promptsContent26);
+
+ const ideManager26 = new IdeManager();
+ await ideManager26.ensureInitialized();
+ const result26 = await ideManager26.setup('rovo-dev', tempProjectDir26, installedBmadDir26, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result26.success === true, 'Rovo Dev setup succeeds against temp project');
+
+ const skillFile26 = path.join(tempProjectDir26, '.rovodev', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile26), 'Rovo Dev install writes SKILL.md directory output');
+
+ // Verify name frontmatter matches directory name
+ const skillContent26 = await fs.readFile(skillFile26, 'utf8');
+ const nameMatch26 = skillContent26.match(/^name:\s*(.+)$/m);
+ assert(nameMatch26 && nameMatch26[1].trim() === 'bmad-master', 'Rovo Dev skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(path.join(tempProjectDir26, '.rovodev', 'workflows'))), 'Rovo Dev setup removes legacy workflows dir');
+
+ // Verify prompts.yml cleanup: BMAD entries removed, user entry preserved
+ const cleanedPrompts26 = yaml26.parse(await fs.readFile(promptsPath26, 'utf8'));
+ assert(
+ Array.isArray(cleanedPrompts26.prompts) && cleanedPrompts26.prompts.length === 1,
+ 'Rovo Dev cleanup removes BMAD entries from prompts.yml',
+ );
+ assert(cleanedPrompts26.prompts[0].name === 'my-custom-prompt', 'Rovo Dev cleanup preserves non-BMAD entries in prompts.yml');
+
+ await fs.remove(tempProjectDir26);
+ await fs.remove(installedBmadDir26);
+ } catch (error) {
+ assert(false, 'Rovo Dev native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
+ // ============================================================
+ // Suite 27: Cleanup preserves bmad-os-* skills
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 27: Cleanup preserves bmad-os-* skills${colors.reset}\n`);
+
+ try {
+ const tempProjectDir27 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-os-preserve-test-'));
+ const installedBmadDir27 = await createTestBmadFixture();
+
+ // Pre-populate .claude/skills with bmad-os-* skills (version-controlled repo skills)
+ const osSkillDir27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-os-review-pr');
+ await fs.ensureDir(osSkillDir27);
+ await fs.writeFile(
+ path.join(osSkillDir27, 'SKILL.md'),
+ '---\nname: bmad-os-review-pr\ndescription: Review PRs\n---\nOS skill content\n',
+ );
+
+ const osSkillDir27b = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-os-release-module');
+ await fs.ensureDir(osSkillDir27b);
+ await fs.writeFile(
+ path.join(osSkillDir27b, 'SKILL.md'),
+ '---\nname: bmad-os-release-module\ndescription: Release module\n---\nOS skill content\n',
+ );
+
+ // Also add a regular bmad skill that SHOULD be cleaned up
+ const regularSkillDir27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-architect');
+ await fs.ensureDir(regularSkillDir27);
+ await fs.writeFile(
+ path.join(regularSkillDir27, 'SKILL.md'),
+ '---\nname: bmad-architect\ndescription: Architect\n---\nOld skill content\n',
+ );
+
+ // Run Claude Code setup (which triggers cleanup then install)
+ const ideManager27 = new IdeManager();
+ await ideManager27.ensureInitialized();
+ const result27 = await ideManager27.setup('claude-code', tempProjectDir27, installedBmadDir27, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result27.success === true, 'Claude Code setup succeeds with bmad-os-* skills present');
+
+ // bmad-os-* skills must survive
+ assert(await fs.pathExists(osSkillDir27), 'Cleanup preserves bmad-os-review-pr skill');
+ assert(await fs.pathExists(osSkillDir27b), 'Cleanup preserves bmad-os-release-module skill');
+
+ // bmad-os skill content must be untouched
+ const osContent27 = await fs.readFile(path.join(osSkillDir27, 'SKILL.md'), 'utf8');
+ assert(osContent27.includes('OS skill content'), 'bmad-os-review-pr skill content is unchanged');
+
+ // Regular bmad skill should have been replaced by fresh install
+ const newSkillFile27 = path.join(tempProjectDir27, '.claude', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(newSkillFile27), 'Fresh bmad skills are installed alongside preserved bmad-os-* skills');
+
+ // Stale non-bmad-os skill must have been removed by cleanup
+ assert(!(await fs.pathExists(regularSkillDir27)), 'Cleanup removes stale non-bmad-os skills');
+
+ await fs.remove(tempProjectDir27);
+ await fs.remove(installedBmadDir27);
+ } catch (error) {
+ assert(false, 'bmad-os-* skill preservation test succeeds', error.message);
+ }
+
+ console.log('');
+
// ============================================================
// Summary
// ============================================================
diff --git a/tools/cli/installers/lib/core/installer.js b/tools/cli/installers/lib/core/installer.js
index fe8b88d7c..1d9868b60 100644
--- a/tools/cli/installers/lib/core/installer.js
+++ b/tools/cli/installers/lib/core/installer.js
@@ -713,10 +713,30 @@ class Installer {
}
// Merge tool selection into config (for both quick update and regular flow)
- config.ides = toolSelection.ides;
+ // Normalize IDE keys to lowercase so they match handler map keys consistently
+ config.ides = (toolSelection.ides || []).map((ide) => ide.toLowerCase());
config.skipIde = toolSelection.skipIde;
const ideConfigurations = toolSelection.configurations;
+ // Early check: fail fast if ALL selected IDEs are suspended
+ if (config.ides && config.ides.length > 0) {
+ await this.ideManager.ensureInitialized();
+ const suspendedIdes = config.ides.filter((ide) => {
+ const handler = this.ideManager.handlers.get(ide);
+ return handler?.platformConfig?.suspended;
+ });
+
+ if (suspendedIdes.length > 0 && suspendedIdes.length === config.ides.length) {
+ for (const ide of suspendedIdes) {
+ const handler = this.ideManager.handlers.get(ide);
+ await prompts.log.error(`${handler.displayName || ide}: ${handler.platformConfig.suspended}`);
+ }
+ throw new Error(
+ `All selected tool(s) are suspended: ${suspendedIdes.join(', ')}. Installation aborted to prevent upgrading _bmad/ without a working IDE configuration.`,
+ );
+ }
+ }
+
// Detect IDEs that were previously installed but are NOT in the new selection (to be removed)
if (config._isUpdate && config._existingInstall) {
const previouslyInstalledIdes = new Set(config._existingInstall.ides || []);
@@ -1335,6 +1355,19 @@ class Installer {
} catch {
// Ensure the original error is never swallowed by a logging failure
}
+
+ // Clean up any temp backup directories that were created before the failure
+ try {
+ if (config._tempBackupDir && (await fs.pathExists(config._tempBackupDir))) {
+ await fs.remove(config._tempBackupDir);
+ }
+ if (config._tempModifiedBackupDir && (await fs.pathExists(config._tempModifiedBackupDir))) {
+ await fs.remove(config._tempModifiedBackupDir);
+ }
+ } catch {
+ // Best-effort cleanup — don't mask the original error
+ }
+
throw error;
}
}
diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js
index d23d8d6d0..0a311a68d 100644
--- a/tools/cli/installers/lib/ide/_config-driven.js
+++ b/tools/cli/installers/lib/ide/_config-driven.js
@@ -655,6 +655,21 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
}
}
+ // Strip BMAD markers from copilot-instructions.md if present
+ if (this.name === 'github-copilot') {
+ await this.cleanupCopilotInstructions(projectDir, options);
+ }
+
+ // Strip BMAD modes from .kilocodemodes if present
+ if (this.name === 'kilo') {
+ await this.cleanupKiloModes(projectDir, options);
+ }
+
+ // Strip BMAD entries from .rovodev/prompts.yml if present
+ if (this.name === 'rovo-dev') {
+ await this.cleanupRovoDevPrompts(projectDir, options);
+ }
+
// Clean all target directories
if (this.installerConfig?.targets) {
const parentDirs = new Set();
@@ -741,7 +756,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
if (!entry || typeof entry !== 'string') {
continue;
}
- if (entry.startsWith('bmad')) {
+ if (entry.startsWith('bmad') && !entry.startsWith('bmad-os-')) {
const entryPath = path.join(targetPath, entry);
try {
await fs.remove(entryPath);
@@ -768,6 +783,121 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
}
}
}
+ /**
+ * Strip BMAD-owned content from .github/copilot-instructions.md.
+ * The old custom installer injected content between and markers.
+ * Deletes the file if nothing remains. Restores .bak backup if one exists.
+ */
+ async cleanupCopilotInstructions(projectDir, options = {}) {
+ const filePath = path.join(projectDir, '.github', 'copilot-instructions.md');
+
+ if (!(await fs.pathExists(filePath))) return;
+
+ try {
+ const content = await fs.readFile(filePath, 'utf8');
+ const startIdx = content.indexOf('');
+ const endIdx = content.indexOf('');
+
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) return;
+
+ const cleaned = content.slice(0, startIdx) + content.slice(endIdx + ''.length);
+
+ if (cleaned.trim().length === 0) {
+ await fs.remove(filePath);
+ const backupPath = `${filePath}.bak`;
+ if (await fs.pathExists(backupPath)) {
+ await fs.rename(backupPath, filePath);
+ if (!options.silent) await prompts.log.message(' Restored copilot-instructions.md from backup');
+ }
+ } else {
+ await fs.writeFile(filePath, cleaned, 'utf8');
+ const backupPath = `${filePath}.bak`;
+ if (await fs.pathExists(backupPath)) await fs.remove(backupPath);
+ }
+
+ if (!options.silent) await prompts.log.message(' Cleaned BMAD markers from copilot-instructions.md');
+ } catch {
+ if (!options.silent) await prompts.log.warn(' Warning: Could not clean BMAD markers from copilot-instructions.md');
+ }
+ }
+
+ /**
+ * Strip BMAD-owned modes from .kilocodemodes.
+ * The old custom kilo.js installer added modes with slug starting with 'bmad-'.
+ * Parses YAML, filters out BMAD modes, rewrites. Leaves file as-is on parse failure.
+ */
+ async cleanupKiloModes(projectDir, options = {}) {
+ const kiloModesPath = path.join(projectDir, '.kilocodemodes');
+
+ if (!(await fs.pathExists(kiloModesPath))) return;
+
+ const content = await fs.readFile(kiloModesPath, 'utf8');
+
+ let config;
+ try {
+ config = yaml.parse(content) || {};
+ } catch {
+ if (!options.silent) await prompts.log.warn(' Warning: Could not parse .kilocodemodes for cleanup');
+ return;
+ }
+
+ if (!Array.isArray(config.customModes)) return;
+
+ const originalCount = config.customModes.length;
+ config.customModes = config.customModes.filter((mode) => mode && (!mode.slug || !mode.slug.startsWith('bmad-')));
+ const removedCount = originalCount - config.customModes.length;
+
+ if (removedCount > 0) {
+ try {
+ await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 }));
+ if (!options.silent) await prompts.log.message(` Removed ${removedCount} BMAD modes from .kilocodemodes`);
+ } catch {
+ if (!options.silent) await prompts.log.warn(' Warning: Could not write .kilocodemodes during cleanup');
+ }
+ }
+ }
+
+ /**
+ * Strip BMAD-owned entries from .rovodev/prompts.yml.
+ * The old custom rovodev.js installer registered workflows in prompts.yml.
+ * Parses YAML, filters out entries with name starting with 'bmad-', rewrites.
+ * Removes the file if no entries remain.
+ */
+ async cleanupRovoDevPrompts(projectDir, options = {}) {
+ const promptsPath = path.join(projectDir, '.rovodev', 'prompts.yml');
+
+ if (!(await fs.pathExists(promptsPath))) return;
+
+ const content = await fs.readFile(promptsPath, 'utf8');
+
+ let config;
+ try {
+ config = yaml.parse(content) || {};
+ } catch {
+ if (!options.silent) await prompts.log.warn(' Warning: Could not parse prompts.yml for cleanup');
+ return;
+ }
+
+ if (!Array.isArray(config.prompts)) return;
+
+ const originalCount = config.prompts.length;
+ config.prompts = config.prompts.filter((entry) => entry && (!entry.name || !entry.name.startsWith('bmad-')));
+ const removedCount = originalCount - config.prompts.length;
+
+ if (removedCount > 0) {
+ try {
+ if (config.prompts.length === 0) {
+ await fs.remove(promptsPath);
+ } else {
+ await fs.writeFile(promptsPath, yaml.stringify(config, { lineWidth: 0 }));
+ }
+ if (!options.silent) await prompts.log.message(` Removed ${removedCount} BMAD entries from prompts.yml`);
+ } catch {
+ if (!options.silent) await prompts.log.warn(' Warning: Could not write prompts.yml during cleanup');
+ }
+ }
+ }
+
/**
* Check ancestor directories for existing BMAD files in the same target_dir.
* IDEs like Claude Code inherit commands from parent directories, so an existing
@@ -788,7 +918,9 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
try {
if (await fs.pathExists(candidatePath)) {
const entries = await fs.readdir(candidatePath);
- const hasBmad = entries.some((e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad'));
+ const hasBmad = entries.some(
+ (e) => typeof e === 'string' && e.toLowerCase().startsWith('bmad') && !e.toLowerCase().startsWith('bmad-os-'),
+ );
if (hasBmad) {
return candidatePath;
}
diff --git a/tools/cli/installers/lib/ide/github-copilot.js b/tools/cli/installers/lib/ide/github-copilot.js
deleted file mode 100644
index 059127f81..000000000
--- a/tools/cli/installers/lib/ide/github-copilot.js
+++ /dev/null
@@ -1,699 +0,0 @@
-const path = require('node:path');
-const { BaseIdeSetup } = require('./_base-ide');
-const prompts = require('../../../lib/prompts');
-const { AgentCommandGenerator } = require('./shared/agent-command-generator');
-const { BMAD_FOLDER_NAME, toDashPath } = require('./shared/path-utils');
-const fs = require('fs-extra');
-const csv = require('csv-parse/sync');
-const yaml = require('yaml');
-
-/**
- * GitHub Copilot setup handler
- * Creates agents in .github/agents/, prompts in .github/prompts/,
- * copilot-instructions.md, and configures VS Code settings
- */
-class GitHubCopilotSetup extends BaseIdeSetup {
- constructor() {
- super('github-copilot', 'GitHub Copilot', false);
- // Don't set configDir to '.github' — nearly every GitHub repo has that directory,
- // which would cause the base detect() to false-positive. Use detectionPaths instead.
- this.configDir = null;
- this.githubDir = '.github';
- this.agentsDir = 'agents';
- this.promptsDir = 'prompts';
- this.detectionPaths = ['.github/copilot-instructions.md', '.github/agents'];
- }
-
- /**
- * Setup GitHub Copilot 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}...`);
-
- // Create .github/agents and .github/prompts directories
- const githubDir = path.join(projectDir, this.githubDir);
- const agentsDir = path.join(githubDir, this.agentsDir);
- const promptsDir = path.join(githubDir, this.promptsDir);
- await this.ensureDir(agentsDir);
- await this.ensureDir(promptsDir);
-
- // Preserve any customised tool permissions from existing files before cleanup
- this.existingToolPermissions = await this.collectExistingToolPermissions(projectDir);
-
- // Clean up any existing BMAD files before reinstalling
- await this.cleanup(projectDir);
-
- // Load agent manifest for enriched descriptions
- const agentManifest = await this.loadAgentManifest(bmadDir);
-
- // Generate agent launchers
- const agentGen = new AgentCommandGenerator(this.bmadFolderName);
- const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
-
- // Create agent .agent.md files
- let agentCount = 0;
- for (const artifact of agentArtifacts) {
- const agentMeta = agentManifest.get(artifact.name);
-
- // Compute fileName first so we can look up any existing tool permissions
- const dashName = toDashPath(artifact.relativePath);
- const fileName = dashName.replace(/\.md$/, '.agent.md');
- const toolsStr = this.getToolsForFile(fileName);
- const agentContent = this.createAgentContent(artifact, agentMeta, toolsStr);
- const targetPath = path.join(agentsDir, fileName);
- await this.writeFile(targetPath, agentContent);
- agentCount++;
- }
-
- // Generate prompt files from bmad-help.csv
- const promptCount = await this.generatePromptFiles(projectDir, bmadDir, agentArtifacts, agentManifest);
-
- // Generate copilot-instructions.md
- await this.generateCopilotInstructions(projectDir, bmadDir, agentManifest, options);
-
- if (!options.silent) await prompts.log.success(`${this.name} configured: ${agentCount} agents, ${promptCount} prompts → .github/`);
-
- return {
- success: true,
- results: {
- agents: agentCount,
- workflows: promptCount,
- tasks: 0,
- tools: 0,
- },
- };
- }
-
- /**
- * Load agent manifest CSV into a Map keyed by agent name
- * @param {string} bmadDir - BMAD installation directory
- * @returns {Map} Agent metadata keyed by name
- */
- async loadAgentManifest(bmadDir) {
- const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
- const agents = new Map();
-
- if (!(await fs.pathExists(manifestPath))) {
- return agents;
- }
-
- try {
- const csvContent = await fs.readFile(manifestPath, 'utf8');
- const records = csv.parse(csvContent, {
- columns: true,
- skip_empty_lines: true,
- });
-
- for (const record of records) {
- agents.set(record.name, record);
- }
- } catch {
- // Gracefully degrade if manifest is unreadable/malformed
- }
-
- return agents;
- }
-
- /**
- * Load bmad-help.csv to drive prompt generation
- * @param {string} bmadDir - BMAD installation directory
- * @returns {Array|null} Parsed CSV rows
- */
- async loadBmadHelp(bmadDir) {
- const helpPath = path.join(bmadDir, '_config', 'bmad-help.csv');
-
- if (!(await fs.pathExists(helpPath))) {
- return null;
- }
-
- try {
- const csvContent = await fs.readFile(helpPath, 'utf8');
- return csv.parse(csvContent, {
- columns: true,
- skip_empty_lines: true,
- });
- } catch {
- // Gracefully degrade if help CSV is unreadable/malformed
- return null;
- }
- }
-
- /**
- * Create agent .agent.md content with enriched description
- * @param {Object} artifact - Agent artifact from AgentCommandGenerator
- * @param {Object|undefined} manifestEntry - Agent manifest entry with metadata
- * @returns {string} Agent file content
- */
- createAgentContent(artifact, manifestEntry, toolsStr) {
- // Build enriched description from manifest metadata
- let description;
- if (manifestEntry) {
- const persona = manifestEntry.displayName || artifact.name;
- const title = manifestEntry.title || this.formatTitle(artifact.name);
- const capabilities = manifestEntry.capabilities || 'agent capabilities';
- description = `${persona} — ${title}: ${capabilities}`;
- } else {
- description = `Activates the ${this.formatTitle(artifact.name)} agent persona.`;
- }
-
- // Build the agent file path for the activation block
- const agentPath = artifact.agentPath || artifact.relativePath;
- const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`;
-
- return `---
-description: '${description.replaceAll("'", "''")}'
-tools: ${toolsStr}
----
-
-You must fully embody this agent's persona and follow all activation instructions exactly as specified.
-
-
-1. LOAD the FULL agent file from ${agentFilePath}
-2. READ its entire contents - this contains the complete agent persona, menu, and instructions
-3. FOLLOW every step in the section precisely
-4. DISPLAY the welcome/greeting as instructed
-5. PRESENT the numbered menu
-6. WAIT for user input before proceeding
-
-`;
- }
-
- /**
- * Generate .prompt.md files for workflows, tasks, tech-writer commands, and agent activators
- * @param {string} projectDir - Project directory
- * @param {string} bmadDir - BMAD installation directory
- * @param {Array} agentArtifacts - Agent artifacts for activator generation
- * @param {Map} agentManifest - Agent manifest data
- * @returns {number} Count of prompts generated
- */
- async generatePromptFiles(projectDir, bmadDir, agentArtifacts, agentManifest) {
- const promptsDir = path.join(projectDir, this.githubDir, this.promptsDir);
- let promptCount = 0;
-
- // Load bmad-help.csv to drive workflow/task prompt generation
- const helpEntries = await this.loadBmadHelp(bmadDir);
-
- if (helpEntries) {
- for (const entry of helpEntries) {
- const command = entry.command;
- if (!command) continue; // Skip entries without a command (tech-writer commands have no command column)
-
- const workflowFile = entry['workflow-file'];
- if (!workflowFile) continue; // Skip entries with no workflow file path
- const promptFileName = `${command}.prompt.md`;
- const toolsStr = this.getToolsForFile(promptFileName);
- const promptContent = this.createWorkflowPromptContent(entry, workflowFile, toolsStr);
- const promptPath = path.join(promptsDir, promptFileName);
- await this.writeFile(promptPath, promptContent);
- promptCount++;
- }
-
- // Generate tech-writer command prompts (entries with no command column)
- for (const entry of helpEntries) {
- if (entry.command) continue; // Already handled above
- const techWriterPrompt = this.createTechWriterPromptContent(entry);
- if (techWriterPrompt) {
- const promptFileName = `${techWriterPrompt.fileName}.prompt.md`;
- const promptPath = path.join(promptsDir, promptFileName);
- await this.writeFile(promptPath, techWriterPrompt.content);
- promptCount++;
- }
- }
- }
-
- // Generate agent activator prompts (Pattern D)
- for (const artifact of agentArtifacts) {
- const agentMeta = agentManifest.get(artifact.name);
- const fileName = `bmad-${artifact.name}.prompt.md`;
- const toolsStr = this.getToolsForFile(fileName);
- const promptContent = this.createAgentActivatorPromptContent(artifact, agentMeta, toolsStr);
- const promptPath = path.join(promptsDir, fileName);
- await this.writeFile(promptPath, promptContent);
- promptCount++;
- }
-
- return promptCount;
- }
-
- /**
- * Create prompt content for a workflow/task entry from bmad-help.csv
- * Determines the pattern (A, B, or A for .xml tasks) based on file extension
- * @param {Object} entry - bmad-help.csv row
- * @param {string} workflowFile - Workflow file path
- * @returns {string} Prompt file content
- */
- createWorkflowPromptContent(entry, workflowFile, toolsStr) {
- const description = this.escapeYamlSingleQuote(this.createPromptDescription(entry.name));
- // bmm/config.yaml is safe to hardcode here: these prompts are only generated when
- // bmad-help.csv exists (bmm module data), so bmm is guaranteed to be installed.
- const configLine = `1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables`;
-
- let body;
- if (workflowFile.endsWith('.yaml')) {
- // Pattern B: YAML-based workflows — use workflow engine
- body = `${configLine}
-2. Load the workflow engine at {project-root}/${this.bmadFolderName}/core/tasks/workflow.xml
-3. Load and execute the workflow configuration at {project-root}/${workflowFile} using the engine from step 2`;
- } else if (workflowFile.endsWith('.xml')) {
- // Pattern A variant: XML tasks — load and execute directly
- body = `${configLine}
-2. Load and execute the task at {project-root}/${workflowFile}`;
- } else {
- // Pattern A: MD workflows — load and follow directly
- body = `${configLine}
-2. Load and follow the workflow at {project-root}/${workflowFile}`;
- }
-
- return `---
-description: '${description}'
-agent: 'agent'
-tools: ${toolsStr}
----
-
-${body}
-`;
- }
-
- /**
- * Create a short 2-5 word description for a prompt from the entry name
- * @param {string} name - Entry name from bmad-help.csv
- * @returns {string} Short description
- */
- createPromptDescription(name) {
- const descriptionMap = {
- 'Brainstorm Project': 'Brainstorm ideas',
- 'Market Research': 'Market research',
- 'Domain Research': 'Domain research',
- 'Technical Research': 'Technical research',
- 'Create Brief': 'Create product brief',
- 'Create PRD': 'Create PRD',
- 'Validate PRD': 'Validate PRD',
- 'Edit PRD': 'Edit PRD',
- 'Create UX': 'Create UX design',
- 'Create Architecture': 'Create architecture',
- 'Create Epics and Stories': 'Create epics and stories',
- 'Check Implementation Readiness': 'Check implementation readiness',
- 'Sprint Planning': 'Sprint planning',
- 'Sprint Status': 'Sprint status',
- 'Create Story': 'Create story',
- 'Validate Story': 'Validate story',
- 'Dev Story': 'Dev story',
- 'QA Automation Test': 'QA automation',
- 'Code Review': 'Code review',
- Retrospective: 'Retrospective',
- 'Document Project': 'Document project',
- 'Generate Project Context': 'Generate project context',
- 'Quick Spec': 'Quick spec',
- 'Quick Dev': 'Quick dev',
- 'Correct Course': 'Correct course',
- Brainstorming: 'Brainstorm ideas',
- 'Party Mode': 'Party mode',
- 'bmad-help': 'BMAD help',
- 'Index Docs': 'Index documents',
- 'Shard Document': 'Shard document',
- 'Editorial Review - Prose': 'Editorial review prose',
- 'Editorial Review - Structure': 'Editorial review structure',
- 'Adversarial Review (General)': 'Adversarial review',
- };
-
- return descriptionMap[name] || name;
- }
-
- /**
- * Create prompt content for tech-writer agent-only commands (Pattern C)
- * @param {Object} entry - bmad-help.csv row
- * @returns {Object|null} { fileName, content } or null if not a tech-writer command
- */
- createTechWriterPromptContent(entry) {
- if (entry['agent-name'] !== 'tech-writer') return null;
-
- const techWriterCommands = {
- 'Write Document': { code: 'WD', file: 'bmad-bmm-write-document', description: 'Write document' },
- 'Update Standards': { code: 'US', file: 'bmad-bmm-update-standards', description: 'Update standards' },
- 'Mermaid Generate': { code: 'MG', file: 'bmad-bmm-mermaid-generate', description: 'Mermaid generate' },
- 'Validate Document': { code: 'VD', file: 'bmad-bmm-validate-document', description: 'Validate document' },
- 'Explain Concept': { code: 'EC', file: 'bmad-bmm-explain-concept', description: 'Explain concept' },
- };
-
- const cmd = techWriterCommands[entry.name];
- if (!cmd) return null;
-
- const safeDescription = this.escapeYamlSingleQuote(cmd.description);
- const toolsStr = this.getToolsForFile(`${cmd.file}.prompt.md`);
-
- const content = `---
-description: '${safeDescription}'
-agent: 'agent'
-tools: ${toolsStr}
----
-
-1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables
-2. Load the full agent file from {project-root}/${this.bmadFolderName}/bmm/agents/tech-writer/tech-writer.md and activate the Paige (Technical Writer) persona
-3. Execute the ${entry.name} menu command (${cmd.code})
-`;
-
- return { fileName: cmd.file, content };
- }
-
- /**
- * Create agent activator prompt content (Pattern D)
- * @param {Object} artifact - Agent artifact
- * @param {Object|undefined} manifestEntry - Agent manifest entry
- * @returns {string} Prompt file content
- */
- createAgentActivatorPromptContent(artifact, manifestEntry, toolsStr) {
- let description;
- if (manifestEntry) {
- description = manifestEntry.title || this.formatTitle(artifact.name);
- } else {
- description = this.formatTitle(artifact.name);
- }
-
- const safeDescription = this.escapeYamlSingleQuote(description);
- const agentPath = artifact.agentPath || artifact.relativePath;
- const agentFilePath = `{project-root}/${this.bmadFolderName}/${agentPath}`;
-
- // bmm/config.yaml is safe to hardcode: agent activators are only generated from
- // bmm agent artifacts, so bmm is guaranteed to be installed.
- return `---
-description: '${safeDescription}'
-agent: 'agent'
-tools: ${toolsStr}
----
-
-1. Load {project-root}/${this.bmadFolderName}/bmm/config.yaml and store ALL fields as session variables
-2. Load the full agent file from ${agentFilePath}
-3. Follow ALL activation instructions in the agent file
-4. Display the welcome/greeting as instructed
-5. Present the numbered menu
-6. Wait for user input before proceeding
-`;
- }
-
- /**
- * Generate copilot-instructions.md from module config
- * @param {string} projectDir - Project directory
- * @param {string} bmadDir - BMAD installation directory
- * @param {Map} agentManifest - Agent manifest data
- */
- async generateCopilotInstructions(projectDir, bmadDir, agentManifest, options = {}) {
- const configVars = await this.loadModuleConfig(bmadDir);
-
- // Build the agents table from the manifest
- let agentsTable = '| Agent | Persona | Title | Capabilities |\n|---|---|---|---|\n';
- const agentOrder = [
- 'bmad-master',
- 'analyst',
- 'architect',
- 'dev',
- 'pm',
- 'qa',
- 'quick-flow-solo-dev',
- 'sm',
- 'tech-writer',
- 'ux-designer',
- ];
-
- for (const agentName of agentOrder) {
- const meta = agentManifest.get(agentName);
- if (meta) {
- const capabilities = meta.capabilities || 'agent capabilities';
- const cleanTitle = (meta.title || '').replaceAll('""', '"');
- agentsTable += `| ${agentName} | ${meta.displayName} | ${cleanTitle} | ${capabilities} |\n`;
- }
- }
-
- const bmad = this.bmadFolderName;
- const bmadSection = `# BMAD Method — Project Instructions
-
-## Project Configuration
-
-- **Project**: ${configVars.project_name || '{{project_name}}'}
-- **User**: ${configVars.user_name || '{{user_name}}'}
-- **Communication Language**: ${configVars.communication_language || '{{communication_language}}'}
-- **Document Output Language**: ${configVars.document_output_language || '{{document_output_language}}'}
-- **User Skill Level**: ${configVars.user_skill_level || '{{user_skill_level}}'}
-- **Output Folder**: ${configVars.output_folder || '{{output_folder}}'}
-- **Planning Artifacts**: ${configVars.planning_artifacts || '{{planning_artifacts}}'}
-- **Implementation Artifacts**: ${configVars.implementation_artifacts || '{{implementation_artifacts}}'}
-- **Project Knowledge**: ${configVars.project_knowledge || '{{project_knowledge}}'}
-
-## BMAD Runtime Structure
-
-- **Agent definitions**: \`${bmad}/bmm/agents/\` (BMM module) and \`${bmad}/core/agents/\` (core)
-- **Workflow definitions**: \`${bmad}/bmm/workflows/\` (organized by phase)
-- **Core tasks**: \`${bmad}/core/tasks/\` (help, editorial review, indexing, sharding, adversarial review)
-- **Core workflows**: \`${bmad}/core/workflows/\` (brainstorming, party-mode, advanced-elicitation)
-- **Workflow engine**: \`${bmad}/core/tasks/workflow.xml\` (executes YAML-based workflows)
-- **Module configuration**: \`${bmad}/bmm/config.yaml\`
-- **Core configuration**: \`${bmad}/core/config.yaml\`
-- **Agent manifest**: \`${bmad}/_config/agent-manifest.csv\`
-- **Workflow manifest**: \`${bmad}/_config/workflow-manifest.csv\`
-- **Help manifest**: \`${bmad}/_config/bmad-help.csv\`
-- **Agent memory**: \`${bmad}/_memory/\`
-
-## Key Conventions
-
-- Always load \`${bmad}/bmm/config.yaml\` before any agent activation or workflow execution
-- Store all config fields as session variables: \`{user_name}\`, \`{communication_language}\`, \`{output_folder}\`, \`{planning_artifacts}\`, \`{implementation_artifacts}\`, \`{project_knowledge}\`
-- MD-based workflows execute directly — load and follow the \`.md\` file
-- YAML-based workflows require the workflow engine — load \`workflow.xml\` first, then pass the \`.yaml\` config
-- Follow step-based workflow execution: load steps JIT, never multiple at once
-- Save outputs after EACH step when using the workflow engine
-- The \`{project-root}\` variable resolves to the workspace root at runtime
-
-## Available Agents
-
-${agentsTable}
-## Slash Commands
-
-Type \`/bmad-\` in Copilot Chat to see all available BMAD workflows and agent activators. Agents are also available in the agents dropdown.`;
-
- const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md');
- const markerStart = '';
- const markerEnd = '';
- const markedContent = `${markerStart}\n${bmadSection}\n${markerEnd}`;
-
- if (await fs.pathExists(instructionsPath)) {
- const existing = await fs.readFile(instructionsPath, 'utf8');
- const startIdx = existing.indexOf(markerStart);
- const endIdx = existing.indexOf(markerEnd);
-
- if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
- // Replace only the BMAD section between markers
- const before = existing.slice(0, startIdx);
- const after = existing.slice(endIdx + markerEnd.length);
- const merged = `${before}${markedContent}${after}`;
- await this.writeFile(instructionsPath, merged);
- } else {
- // Existing file without markers — back it up before overwriting
- const backupPath = `${instructionsPath}.bak`;
- await fs.copy(instructionsPath, backupPath);
- if (!options.silent) await prompts.log.warn(` Backed up copilot-instructions.md → .bak`);
- await this.writeFile(instructionsPath, `${markedContent}\n`);
- }
- } else {
- // No existing file — create fresh with markers
- await this.writeFile(instructionsPath, `${markedContent}\n`);
- }
- }
-
- /**
- * Load module config.yaml for template variables
- * @param {string} bmadDir - BMAD installation directory
- * @returns {Object} Config variables
- */
- async loadModuleConfig(bmadDir) {
- const bmmConfigPath = path.join(bmadDir, 'bmm', 'config.yaml');
- const coreConfigPath = path.join(bmadDir, 'core', 'config.yaml');
-
- for (const configPath of [bmmConfigPath, coreConfigPath]) {
- if (await fs.pathExists(configPath)) {
- try {
- const content = await fs.readFile(configPath, 'utf8');
- return yaml.parse(content) || {};
- } catch {
- // Fall through to next config
- }
- }
- }
-
- return {};
- }
-
- /**
- * Escape a string for use inside YAML single-quoted values.
- * In YAML, the only escape inside single quotes is '' for a literal '.
- * @param {string} value - Raw string
- * @returns {string} Escaped string safe for YAML single-quoted context
- */
- escapeYamlSingleQuote(value) {
- return (value || '').replaceAll("'", "''");
- }
-
- /**
- * Scan existing agent and prompt files for customised tool permissions before cleanup.
- * Returns a Map so permissions can be preserved across reinstalls.
- * @param {string} projectDir - Project directory
- * @returns {Map} Existing tool permissions keyed by filename
- */
- async collectExistingToolPermissions(projectDir) {
- const permissions = new Map();
- const dirs = [
- [path.join(projectDir, this.githubDir, this.agentsDir), /^bmad.*\.agent\.md$/],
- [path.join(projectDir, this.githubDir, this.promptsDir), /^bmad-.*\.prompt\.md$/],
- ];
-
- for (const [dir, pattern] of dirs) {
- if (!(await fs.pathExists(dir))) continue;
- const files = await fs.readdir(dir);
-
- for (const file of files) {
- if (!pattern.test(file)) continue;
-
- try {
- const content = await fs.readFile(path.join(dir, file), 'utf8');
- const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
- if (!fmMatch) continue;
-
- const frontmatter = yaml.parse(fmMatch[1]);
- if (frontmatter && Array.isArray(frontmatter.tools)) {
- permissions.set(file, frontmatter.tools);
- }
- } catch {
- // Skip unreadable files
- }
- }
- }
-
- return permissions;
- }
-
- /**
- * Get the tools array string for a file, preserving any existing customisation.
- * Falls back to the default tools if no prior customisation exists.
- * @param {string} fileName - Target filename (e.g. 'bmad-agent-bmm-pm.agent.md')
- * @returns {string} YAML inline array string
- */
- getToolsForFile(fileName) {
- const defaultTools = ['read', 'edit', 'search', 'execute'];
- const tools = (this.existingToolPermissions && this.existingToolPermissions.get(fileName)) || defaultTools;
- return '[' + tools.map((t) => `'${t}'`).join(', ') + ']';
- }
-
- /**
- * Format name as title
- */
- formatTitle(name) {
- return name
- .split('-')
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
- .join(' ');
- }
-
- /**
- * Cleanup GitHub Copilot configuration - surgically remove only BMAD files
- */
- async cleanup(projectDir, options = {}) {
- // Clean up agents directory
- const agentsDir = path.join(projectDir, this.githubDir, this.agentsDir);
- if (await fs.pathExists(agentsDir)) {
- const files = await fs.readdir(agentsDir);
- let removed = 0;
-
- for (const file of files) {
- if (file.startsWith('bmad') && (file.endsWith('.agent.md') || file.endsWith('.md'))) {
- await fs.remove(path.join(agentsDir, file));
- removed++;
- }
- }
-
- if (removed > 0 && !options.silent) {
- await prompts.log.message(` Cleaned up ${removed} existing BMAD agents`);
- }
- }
-
- // Clean up prompts directory
- const promptsDir = path.join(projectDir, this.githubDir, this.promptsDir);
- if (await fs.pathExists(promptsDir)) {
- const files = await fs.readdir(promptsDir);
- let removed = 0;
-
- for (const file of files) {
- if (file.startsWith('bmad-') && file.endsWith('.prompt.md')) {
- await fs.remove(path.join(promptsDir, file));
- removed++;
- }
- }
-
- if (removed > 0 && !options.silent) {
- await prompts.log.message(` Cleaned up ${removed} existing BMAD prompts`);
- }
- }
-
- // During uninstall, also strip BMAD markers from copilot-instructions.md.
- // During reinstall (default), this is skipped because generateCopilotInstructions()
- // handles marker-based replacement in a single read-modify-write pass,
- // which correctly preserves user content outside the markers.
- if (options.isUninstall) {
- await this.cleanupCopilotInstructions(projectDir, options);
- }
- }
-
- /**
- * Strip BMAD marker section from copilot-instructions.md
- * If file becomes empty after stripping, delete it.
- * If a .bak backup exists and the main file was deleted, restore the backup.
- * @param {string} projectDir - Project directory
- * @param {Object} [options] - Options (e.g. { silent: true })
- */
- async cleanupCopilotInstructions(projectDir, options = {}) {
- const instructionsPath = path.join(projectDir, this.githubDir, 'copilot-instructions.md');
- const backupPath = `${instructionsPath}.bak`;
-
- if (!(await fs.pathExists(instructionsPath))) {
- return;
- }
-
- const content = await fs.readFile(instructionsPath, 'utf8');
- const markerStart = '';
- const markerEnd = '';
- const startIdx = content.indexOf(markerStart);
- const endIdx = content.indexOf(markerEnd);
-
- if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
- return; // No valid markers found
- }
-
- // Strip the marker section (including markers)
- const before = content.slice(0, startIdx);
- const after = content.slice(endIdx + markerEnd.length);
- const cleaned = before + after;
-
- if (cleaned.trim().length === 0) {
- // File is empty after stripping — delete it
- await fs.remove(instructionsPath);
-
- // If backup exists, restore it
- if (await fs.pathExists(backupPath)) {
- await fs.rename(backupPath, instructionsPath);
- if (!options.silent) {
- await prompts.log.message(' Restored copilot-instructions.md from backup');
- }
- }
- } else {
- // Write cleaned content back (preserve original whitespace)
- await fs.writeFile(instructionsPath, cleaned, 'utf8');
-
- // If backup exists, it's stale now — remove it
- if (await fs.pathExists(backupPath)) {
- await fs.remove(backupPath);
- }
- }
- }
-}
-
-module.exports = { GitHubCopilotSetup };
diff --git a/tools/cli/installers/lib/ide/kilo.js b/tools/cli/installers/lib/ide/kilo.js
deleted file mode 100644
index 2e5734391..000000000
--- a/tools/cli/installers/lib/ide/kilo.js
+++ /dev/null
@@ -1,269 +0,0 @@
-const path = require('node:path');
-const { BaseIdeSetup } = require('./_base-ide');
-const yaml = require('yaml');
-const prompts = require('../../../lib/prompts');
-const { AgentCommandGenerator } = require('./shared/agent-command-generator');
-const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
-const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
-
-/**
- * KiloCode IDE setup handler
- * Creates custom modes in .kilocodemodes file (similar to Roo)
- */
-class KiloSetup extends BaseIdeSetup {
- constructor() {
- super('kilo', 'Kilo Code');
- this.configFile = '.kilocodemodes';
- }
-
- /**
- * Setup KiloCode IDE 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}...`);
-
- // Clean up any old BMAD installation first
- await this.cleanup(projectDir, options);
-
- // Load existing config (may contain non-BMAD modes and other settings)
- const kiloModesPath = path.join(projectDir, this.configFile);
- let config = {};
-
- if (await this.pathExists(kiloModesPath)) {
- const existingContent = await this.readFile(kiloModesPath);
- try {
- config = yaml.parse(existingContent) || {};
- } catch {
- // If parsing fails, start fresh but warn user
- await prompts.log.warn('Warning: Could not parse existing .kilocodemodes, starting fresh');
- config = {};
- }
- }
-
- // Ensure customModes array exists
- if (!Array.isArray(config.customModes)) {
- config.customModes = [];
- }
-
- // Generate agent launchers
- const agentGen = new AgentCommandGenerator(this.bmadFolderName);
- const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
-
- // Create mode objects and add to config
- let addedCount = 0;
-
- for (const artifact of agentArtifacts) {
- const modeObject = await this.createModeObject(artifact, projectDir);
- config.customModes.push(modeObject);
- addedCount++;
- }
-
- // Write .kilocodemodes file with proper YAML structure
- const finalContent = yaml.stringify(config, { lineWidth: 0 });
- await this.writeFile(kiloModesPath, finalContent);
-
- // Generate workflow commands
- const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
- const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
-
- // Write to .kilocode/workflows/ directory
- const workflowsDir = path.join(projectDir, '.kilocode', 'workflows');
- await this.ensureDir(workflowsDir);
-
- // Clear old BMAD workflows before writing new ones
- await this.clearBmadWorkflows(workflowsDir);
-
- // Write workflow files
- const workflowCount = await workflowGenerator.writeDashArtifacts(workflowsDir, workflowArtifacts);
-
- // Generate task and tool commands
- const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName);
- const { artifacts: taskToolArtifacts, counts: taskToolCounts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
-
- // Write task/tool files to workflows directory (same location as workflows)
- await taskToolGen.writeDashArtifacts(workflowsDir, taskToolArtifacts);
- const taskCount = taskToolCounts.tasks || 0;
- const toolCount = taskToolCounts.tools || 0;
-
- if (!options.silent) {
- await prompts.log.success(
- `${this.name} configured: ${addedCount} modes, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools → ${this.configFile}`,
- );
- }
-
- return {
- success: true,
- modes: addedCount,
- workflows: workflowCount,
- tasks: taskCount,
- tools: toolCount,
- };
- }
-
- /**
- * Create a mode object for an agent
- * @param {Object} artifact - Agent artifact
- * @param {string} projectDir - Project directory
- * @returns {Object} Mode object for YAML serialization
- */
- async createModeObject(artifact, projectDir) {
- // Extract metadata from launcher content
- const titleMatch = artifact.content.match(/title="([^"]+)"/);
- const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name);
-
- const iconMatch = artifact.content.match(/icon="([^"]+)"/);
- const icon = iconMatch ? iconMatch[1] : '🤖';
-
- const whenToUseMatch = artifact.content.match(/whenToUse="([^"]+)"/);
- const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
-
- // Get the activation header from central template (trim to avoid YAML formatting issues)
- const activationHeader = (await this.getAgentCommandHeader()).trim();
-
- const roleDefinitionMatch = artifact.content.match(/roleDefinition="([^"]+)"/);
- const roleDefinition = roleDefinitionMatch
- ? roleDefinitionMatch[1]
- : `You are a ${title} specializing in ${title.toLowerCase()} tasks.`;
-
- // Get relative path
- const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/');
-
- // Build mode object (KiloCode uses same schema as Roo)
- return {
- slug: `bmad-${artifact.module}-${artifact.name}`,
- name: `${icon} ${title}`,
- roleDefinition: roleDefinition,
- whenToUse: whenToUse,
- customInstructions: `${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`,
- groups: ['read', 'edit', 'browser', 'command', 'mcp'],
- };
- }
-
- /**
- * Format name as title
- */
- formatTitle(name) {
- return name
- .split('-')
- .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
- .join(' ');
- }
-
- /**
- * Clear old BMAD workflow files from workflows directory
- * @param {string} workflowsDir - Workflows directory path
- */
- async clearBmadWorkflows(workflowsDir) {
- const fs = require('fs-extra');
- if (!(await fs.pathExists(workflowsDir))) return;
-
- const entries = await fs.readdir(workflowsDir);
- for (const entry of entries) {
- if (entry.startsWith('bmad-') && entry.endsWith('.md')) {
- await fs.remove(path.join(workflowsDir, entry));
- }
- }
- }
-
- /**
- * Cleanup KiloCode configuration
- */
- async cleanup(projectDir, options = {}) {
- const fs = require('fs-extra');
- const kiloModesPath = path.join(projectDir, this.configFile);
-
- if (await fs.pathExists(kiloModesPath)) {
- const content = await fs.readFile(kiloModesPath, 'utf8');
-
- try {
- const config = yaml.parse(content) || {};
-
- if (Array.isArray(config.customModes)) {
- const originalCount = config.customModes.length;
- // Remove BMAD modes only (keep non-BMAD modes)
- config.customModes = config.customModes.filter((mode) => !mode.slug || !mode.slug.startsWith('bmad-'));
- const removedCount = originalCount - config.customModes.length;
-
- if (removedCount > 0) {
- await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 }));
- if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .kilocodemodes`);
- }
- }
- } catch {
- // If parsing fails, leave file as-is
- if (!options.silent) await prompts.log.warn('Warning: Could not parse .kilocodemodes for cleanup');
- }
- }
-
- // Clean up workflow files
- const workflowsDir = path.join(projectDir, '.kilocode', 'workflows');
- await this.clearBmadWorkflows(workflowsDir);
- }
-
- /**
- * Install a custom agent launcher for Kilo
- * @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} Installation result
- */
- async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
- const kilocodemodesPath = path.join(projectDir, this.configFile);
- let config = {};
-
- // Read existing .kilocodemodes file
- if (await this.pathExists(kilocodemodesPath)) {
- const existingContent = await this.readFile(kilocodemodesPath);
- try {
- config = yaml.parse(existingContent) || {};
- } catch {
- config = {};
- }
- }
-
- // Ensure customModes array exists
- if (!Array.isArray(config.customModes)) {
- config.customModes = [];
- }
-
- // Create custom agent mode object
- const slug = `bmad-custom-${agentName.toLowerCase()}`;
-
- // Check if mode already exists
- if (config.customModes.some((mode) => mode.slug === slug)) {
- return {
- ide: 'kilo',
- path: this.configFile,
- command: agentName,
- type: 'custom-agent-launcher',
- alreadyExists: true,
- };
- }
-
- // Add custom mode object
- config.customModes.push({
- slug: slug,
- name: `BMAD Custom: ${agentName}`,
- description: `Custom BMAD agent: ${agentName}\n\n**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!\n\nThis is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file.\n`,
- prompt: `@${agentPath}\n`,
- always: false,
- permissions: 'all',
- });
-
- // Write .kilocodemodes file with proper YAML structure
- await this.writeFile(kilocodemodesPath, yaml.stringify(config, { lineWidth: 0 }));
-
- return {
- ide: 'kilo',
- path: this.configFile,
- command: slug,
- type: 'custom-agent-launcher',
- };
- }
-}
-
-module.exports = { KiloSetup };
diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js
index 654574a25..2381bddfa 100644
--- a/tools/cli/installers/lib/ide/manager.js
+++ b/tools/cli/installers/lib/ide/manager.js
@@ -1,5 +1,3 @@
-const fs = require('fs-extra');
-const path = require('node:path');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
const prompts = require('../../../lib/prompts');
@@ -8,8 +6,7 @@ const prompts = require('../../../lib/prompts');
* Dynamically discovers and loads IDE handlers
*
* Loading strategy:
- * 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
+ * All platforms are config-driven from platform-codes.yaml.
*/
class IdeManager {
constructor() {
@@ -43,50 +40,12 @@ class IdeManager {
}
/**
- * Dynamically load all IDE handlers
- * 1. Load custom installer files first (github-copilot.js, kilo.js, rovodev.js)
- * 2. Load config-driven handlers from platform-codes.yaml
+ * Dynamically load all IDE handlers from platform-codes.yaml
*/
async loadHandlers() {
- // Load custom installer files
- await this.loadCustomInstallerFiles();
-
- // Load config-driven handlers from platform-codes.yaml
await this.loadConfigDrivenHandlers();
}
- /**
- * Load custom installer files (unique installation logic)
- * 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() {
- const ideDir = __dirname;
- const customFiles = ['github-copilot.js', 'kilo.js', 'rovodev.js'];
-
- for (const file of customFiles) {
- const filePath = path.join(ideDir, file);
- if (!fs.existsSync(filePath)) continue;
-
- try {
- const HandlerModule = require(filePath);
- const HandlerClass = HandlerModule.default || Object.values(HandlerModule)[0];
-
- if (HandlerClass) {
- const instance = new HandlerClass();
- if (instance.name && typeof instance.name === 'string') {
- if (typeof instance.setBmadFolderName === 'function') {
- instance.setBmadFolderName(this.bmadFolderName);
- }
- this.handlers.set(instance.name, instance);
- }
- }
- } catch (error) {
- await prompts.log.warn(`Warning: Could not load ${file}: ${error.message}`);
- }
- }
- }
-
/**
* Load config-driven handlers from platform-codes.yaml
* This creates ConfigDrivenIdeSetup instances for platforms with installer config
@@ -98,9 +57,6 @@ class IdeManager {
const { ConfigDrivenIdeSetup } = require('./_config-driven');
for (const [platformCode, platformInfo] of Object.entries(platformConfig.platforms)) {
- // Skip if already loaded by custom installer
- if (this.handlers.has(platformCode)) continue;
-
// Skip if no installer config (platform may not need installation)
if (!platformInfo.installer) continue;
@@ -128,6 +84,11 @@ class IdeManager {
continue;
}
+ // Skip suspended platforms (e.g., IDE doesn't support skills yet)
+ if (handler.platformConfig?.suspended) {
+ continue;
+ }
+
ides.push({
value: key,
name: name,
@@ -177,6 +138,22 @@ class IdeManager {
return { success: false, ide: ideName, error: 'unsupported IDE' };
}
+ // Block suspended platforms — clean up legacy files but don't install
+ if (handler.platformConfig?.suspended) {
+ if (!options.silent) {
+ await prompts.log.warn(`${handler.displayName || ideName}: ${handler.platformConfig.suspended}`);
+ }
+ // Still clean up legacy artifacts so old broken configs don't linger
+ if (typeof handler.cleanup === 'function') {
+ try {
+ await handler.cleanup(projectDir, { silent: true });
+ } catch {
+ // Best-effort cleanup — don't let stale files block the suspended result
+ }
+ }
+ return { success: false, ide: ideName, error: 'suspended' };
+ }
+
try {
const handlerResult = await handler.setup(projectDir, bmadDir, options);
// Build detail string from handler-returned data
@@ -190,14 +167,6 @@ class IdeManager {
if (r.tasks > 0) parts.push(`${r.tasks} tasks`);
if (r.tools > 0) parts.push(`${r.tools} tools`);
detail = parts.join(', ');
- } else if (handlerResult && handlerResult.modes !== undefined) {
- // Kilo handler returns { success, modes, workflows, tasks, tools }
- const parts = [];
- if (handlerResult.modes > 0) parts.push(`${handlerResult.modes} modes`);
- if (handlerResult.workflows > 0) parts.push(`${handlerResult.workflows} workflows`);
- if (handlerResult.tasks > 0) parts.push(`${handlerResult.tasks} tasks`);
- if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`);
- detail = parts.join(', ');
}
// Propagate handler's success status (default true for backward compat)
const success = handlerResult?.success !== false;
diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml
index 99269552f..82d45f562 100644
--- a/tools/cli/installers/lib/ide/platform-codes.yaml
+++ b/tools/cli/installers/lib/ide/platform-codes.yaml
@@ -57,8 +57,11 @@ platforms:
category: ide
description: "AI coding assistant"
installer:
- target_dir: .clinerules/workflows
- template_type: windsurf
+ legacy_targets:
+ - .clinerules/workflows
+ target_dir: .cline/skills
+ template_type: default
+ skill_format: true
codex:
name: "Codex"
@@ -81,8 +84,11 @@ platforms:
category: ide
description: "Tencent Cloud Code Assistant - AI-powered coding companion"
installer:
- target_dir: .codebuddy/commands
+ legacy_targets:
+ - .codebuddy/commands
+ target_dir: .codebuddy/skills
template_type: default
+ skill_format: true
crush:
name: "Crush"
@@ -90,8 +96,11 @@ platforms:
category: ide
description: "AI development assistant"
installer:
- target_dir: .crush/commands
+ legacy_targets:
+ - .crush/commands
+ target_dir: .crush/skills
template_type: default
+ skill_format: true
cursor:
name: "Cursor"
@@ -111,15 +120,24 @@ platforms:
category: cli
description: "Google's CLI for Gemini"
installer:
- target_dir: .gemini/commands
- template_type: gemini
+ legacy_targets:
+ - .gemini/commands
+ target_dir: .gemini/skills
+ template_type: default
+ skill_format: true
github-copilot:
name: "GitHub Copilot"
preferred: false
category: ide
description: "GitHub's AI pair programmer"
- # No installer config - uses custom github-copilot.js
+ installer:
+ legacy_targets:
+ - .github/agents
+ - .github/prompts
+ target_dir: .github/skills
+ template_type: default
+ skill_format: true
iflow:
name: "iFlow"
@@ -127,15 +145,24 @@ platforms:
category: ide
description: "AI workflow automation"
installer:
- target_dir: .iflow/commands
+ legacy_targets:
+ - .iflow/commands
+ target_dir: .iflow/skills
template_type: default
+ skill_format: true
kilo:
name: "KiloCoder"
preferred: false
category: ide
description: "AI coding platform"
- # No installer config - uses custom kilo.js (creates .kilocodemodes file)
+ suspended: "Kilo Code does not yet support the Agent Skills standard. Support is paused until they implement it. See https://github.com/kilocode/kilo-code/issues for updates."
+ installer:
+ legacy_targets:
+ - .kilocode/workflows
+ target_dir: .kilocode/skills
+ template_type: default
+ skill_format: true
kiro:
name: "Kiro"
@@ -171,24 +198,35 @@ platforms:
category: ide
description: "Qwen AI coding assistant"
installer:
- target_dir: .qwen/commands
+ legacy_targets:
+ - .qwen/commands
+ target_dir: .qwen/skills
template_type: default
+ skill_format: true
roo:
- name: "Roo Cline"
+ name: "Roo Code"
preferred: false
category: ide
description: "Enhanced Cline fork"
installer:
- target_dir: .roo/commands
+ legacy_targets:
+ - .roo/commands
+ target_dir: .roo/skills
template_type: default
+ skill_format: true
rovo-dev:
name: "Rovo Dev"
preferred: false
category: ide
description: "Atlassian's Rovo development environment"
- # No installer config - uses custom rovodev.js (generates prompts.yml manifest)
+ installer:
+ legacy_targets:
+ - .rovodev/workflows
+ target_dir: .rovodev/skills
+ template_type: default
+ skill_format: true
trae:
name: "Trae"
@@ -196,8 +234,11 @@ platforms:
category: ide
description: "AI coding tool"
installer:
- target_dir: .trae/rules
- template_type: trae
+ legacy_targets:
+ - .trae/rules
+ target_dir: .trae/skills
+ template_type: default
+ skill_format: true
windsurf:
name: "Windsurf"
diff --git a/tools/cli/installers/lib/ide/rovodev.js b/tools/cli/installers/lib/ide/rovodev.js
deleted file mode 100644
index da3c4809d..000000000
--- a/tools/cli/installers/lib/ide/rovodev.js
+++ /dev/null
@@ -1,257 +0,0 @@
-const path = require('node:path');
-const fs = require('fs-extra');
-const yaml = require('yaml');
-const { BaseIdeSetup } = require('./_base-ide');
-const prompts = require('../../../lib/prompts');
-const { AgentCommandGenerator } = require('./shared/agent-command-generator');
-const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
-const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
-const { toDashPath } = require('./shared/path-utils');
-
-/**
- * Rovo Dev IDE setup handler
- *
- * Custom installer that writes .md workflow files to .rovodev/workflows/
- * and generates .rovodev/prompts.yml to register them with Rovo Dev's /prompts feature.
- *
- * prompts.yml format (per Rovo Dev docs):
- * prompts:
- * - name: bmad-bmm-create-prd
- * description: "PRD workflow..."
- * content_file: workflows/bmad-bmm-create-prd.md
- */
-class RovoDevSetup extends BaseIdeSetup {
- constructor() {
- super('rovo-dev', 'Rovo Dev', false);
- this.rovoDir = '.rovodev';
- this.workflowsDir = 'workflows';
- this.promptsFile = 'prompts.yml';
- }
-
- /**
- * Setup Rovo Dev configuration
- * @param {string} projectDir - Project directory
- * @param {string} bmadDir - BMAD installation directory
- * @param {Object} options - Setup options
- * @returns {Promise