diff --git a/test/test-installation-components.js b/test/test-installation-components.js
index 63f2567f5..23c8f6382 100644
--- a/test/test-installation-components.js
+++ b/test/test-installation-components.js
@@ -457,9 +457,296 @@ 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');
+
+ 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');
+
+ 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');
+
+ 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 +783,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 +811,410 @@ 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 Native Skills
+ // ============================================================
+ console.log(`${colors.yellow}Test Suite 22: KiloCoder Native Skills${colors.reset}\n`);
+
+ try {
+ clearCache();
+ const platformCodes22 = await loadPlatformCodes();
+ const kiloInstaller = platformCodes22.platforms.kilo?.installer;
+
+ assert(kiloInstaller?.target_dir === '.kilocode/skills', 'KiloCoder target_dir uses native skills path');
+
+ assert(kiloInstaller?.skill_format === true, 'KiloCoder installer enables native skill output');
+
+ assert(
+ Array.isArray(kiloInstaller?.legacy_targets) && kiloInstaller.legacy_targets.includes('.kilocode/workflows'),
+ 'KiloCoder installer cleans legacy workflows output',
+ );
+
+ // Fresh install test
+ const tempProjectDir22 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kilo-test-'));
+ const installedBmadDir22 = await createTestBmadFixture();
+ const legacyDir22 = path.join(tempProjectDir22, '.kilocode', 'workflows');
+ await fs.ensureDir(legacyDir22);
+ await fs.writeFile(path.join(legacyDir22, 'bmad-legacy.md'), 'legacy\n');
+
+ // Create a .kilocodemodes file with BMAD modes and a user mode
+ const kiloModesPath22 = path.join(tempProjectDir22, '.kilocodemodes');
+ const yaml22 = require('yaml');
+ const kiloModesContent = yaml22.stringify({
+ customModes: [
+ { slug: 'bmad-bmm-architect', name: 'BMAD Architect', roleDefinition: 'test' },
+ { slug: 'bmad-core-master', name: 'BMAD Master', roleDefinition: 'test' },
+ { slug: 'user-custom-mode', name: 'My Custom Mode', roleDefinition: 'user mode' },
+ ],
+ });
+ await fs.writeFile(kiloModesPath22, kiloModesContent);
+
+ const ideManager22 = new IdeManager();
+ await ideManager22.ensureInitialized();
+ const result22 = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result22.success === true, 'KiloCoder setup succeeds against temp project');
+
+ const skillFile22 = path.join(tempProjectDir22, '.kilocode', 'skills', 'bmad-master', 'SKILL.md');
+ assert(await fs.pathExists(skillFile22), 'KiloCoder install writes SKILL.md directory output');
+
+ const skillContent22 = await fs.readFile(skillFile22, 'utf8');
+ const nameMatch22 = skillContent22.match(/^name:\s*(.+)$/m);
+ assert(nameMatch22 && nameMatch22[1].trim() === 'bmad-master', 'KiloCoder skill name frontmatter matches directory name exactly');
+
+ assert(!(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'workflows'))), 'KiloCoder setup removes legacy workflows dir');
+
+ // Verify .kilocodemodes cleanup: BMAD modes removed, user mode preserved
+ const cleanedModes22 = yaml22.parse(await fs.readFile(kiloModesPath22, 'utf8'));
+ assert(
+ Array.isArray(cleanedModes22.customModes) && cleanedModes22.customModes.length === 1,
+ 'KiloCoder cleanup removes BMAD modes from .kilocodemodes',
+ );
+ assert(cleanedModes22.customModes[0].slug === 'user-custom-mode', 'KiloCoder cleanup preserves non-BMAD modes in .kilocodemodes');
+
+ // Reinstall test
+ const result22b = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
+ silent: true,
+ selectedModules: ['bmm'],
+ });
+
+ assert(result22b.success === true, 'KiloCoder reinstall/upgrade succeeds over existing skills');
+ assert(await fs.pathExists(skillFile22), 'KiloCoder reinstall preserves SKILL.md output');
+
+ await fs.remove(tempProjectDir22);
+ await fs.remove(installedBmadDir22);
+ } catch (error) {
+ assert(false, 'KiloCoder native skills migration test succeeds', error.message);
+ }
+
+ console.log('');
+
// ============================================================
// Summary
// ============================================================
diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js
index d23d8d6d0..3ade16b47 100644
--- a/tools/cli/installers/lib/ide/_config-driven.js
+++ b/tools/cli/installers/lib/ide/_config-driven.js
@@ -655,6 +655,16 @@ 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);
+ }
+
// Clean all target directories
if (this.installerConfig?.targets) {
const parentDirs = new Set();
@@ -768,6 +778,76 @@ 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;
+
+ 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');
+ }
+
+ /**
+ * 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');
+ }
+ }
+ }
+
/**
* Check ancestor directories for existing BMAD files in the same target_dir.
* IDEs like Claude Code inherit commands from parent directories, so an existing
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..908a094a3 100644
--- a/tools/cli/installers/lib/ide/manager.js
+++ b/tools/cli/installers/lib/ide/manager.js
@@ -8,7 +8,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
+ * 1. Custom installer files (rovodev.js) - for platforms with unique installation logic
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
*/
class IdeManager {
@@ -44,7 +44,7 @@ class IdeManager {
/**
* Dynamically load all IDE handlers
- * 1. Load custom installer files first (github-copilot.js, kilo.js, rovodev.js)
+ * 1. Load custom installer files first (kilo.js, rovodev.js)
* 2. Load config-driven handlers from platform-codes.yaml
*/
async loadHandlers() {
@@ -58,11 +58,11 @@ class IdeManager {
/**
* 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
+ * Note: codex, github-copilot, and kilo were migrated to config-driven (platform-codes.yaml)
*/
async loadCustomInstallerFiles() {
const ideDir = __dirname;
- const customFiles = ['github-copilot.js', 'kilo.js', 'rovodev.js'];
+ const customFiles = ['rovodev.js'];
for (const file of customFiles) {
const filePath = path.join(ideDir, file);
@@ -190,14 +190,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..37497c86b 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"
@@ -119,7 +128,13 @@ platforms:
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"
@@ -135,7 +150,12 @@ platforms:
preferred: false
category: ide
description: "AI coding platform"
- # No installer config - uses custom kilo.js (creates .kilocodemodes file)
+ installer:
+ legacy_targets:
+ - .kilocode/workflows
+ target_dir: .kilocode/skills
+ template_type: default
+ skill_format: true
kiro:
name: "Kiro"
@@ -180,8 +200,11 @@ platforms:
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"
@@ -196,8 +219,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/docs/native-skills-migration-checklist.md b/tools/docs/native-skills-migration-checklist.md
index ba8f412ed..614871f99 100644
--- a/tools/docs/native-skills-migration-checklist.md
+++ b/tools/docs/native-skills-migration-checklist.md
@@ -15,25 +15,27 @@ This checklist now includes those completed platforms plus the remaining full-su
Support assumption: full Agent Skills support. BMAD has already migrated from `.claude/commands` to `.claude/skills`.
-- [ ] Confirm current implementation still matches Claude Code skills expectations
-- [ ] Confirm legacy cleanup for `.claude/commands`
-- [ ] Test fresh install
-- [ ] Test reinstall/upgrade from legacy command output
-- [ ] Confirm ancestor conflict protection
-- [ ] Implement/extend automated tests as needed
-- [ ] Commit any follow-up fixes if required
+**Install:** `npm install -g @anthropic-ai/claude-code` or `brew install claude-code`
+
+- [x] Confirm current implementation still matches Claude Code skills expectations
+- [x] Confirm legacy cleanup for `.claude/commands`
+- [x] Test fresh install
+- [x] Test reinstall/upgrade from legacy command output
+- [x] Confirm ancestor conflict protection because Claude Code inherits skills from parent directories and `ancestor_conflict_check: true` is set in platform-codes.yaml
+- [x] Implement/extend automated tests as needed
## Codex CLI
Support assumption: full Agent Skills support. BMAD has already migrated from `.codex/prompts` to `.agents/skills`.
-- [ ] Confirm current implementation still matches Codex CLI skills expectations
-- [ ] Confirm legacy cleanup for project and global `.codex/prompts`
-- [ ] Test fresh install
-- [ ] Test reinstall/upgrade from legacy prompt output
+**Install:** `npm install -g @openai/codex`
+
+- [x] Confirm current implementation still matches Codex CLI skills expectations
+- [x] Confirm legacy cleanup for project and global `.codex/prompts`
+- [x] Test fresh install
+- [x] Test reinstall/upgrade from legacy prompt output
- [x] Confirm ancestor conflict protection because Codex inherits parent-directory `.agents/skills`
-- [ ] Implement/extend automated tests as needed
-- [ ] Commit any follow-up fixes if required
+- [x] Implement/extend automated tests as needed
## Cursor
@@ -45,7 +47,7 @@ Support assumption: full Agent Skills support. BMAD currently installs legacy co
- [x] Test fresh install
- [x] Test reinstall/upgrade from legacy command output
- [x] Confirm no ancestor conflict protection is needed because a child workspace surfaced child `.cursor/skills` entries but not a parent-only skill during manual verification
-- [ ] Implement/extend automated tests
+- [x] Implement/extend automated tests
- [x] Commit
## Windsurf
@@ -59,20 +61,21 @@ Support assumption: full Agent Skills support. Windsurf docs confirm workspace s
- [x] Test reinstall/upgrade from legacy workflow output
- [x] Confirm no ancestor conflict protection is needed because manual Windsurf verification showed child-local `@` skills loaded while a parent-only skill was not inherited
- [x] Implement/extend automated tests
-- [x] Commit
## Cline
-Support assumption: full Agent Skills support. BMAD currently installs workflow files to `.clinerules/workflows`; target should move to the platform's native skills directory.
+Support assumption: full Agent Skills support. Cline docs confirm workspace skills at `.cline/skills//SKILL.md` and global skills at `~/.cline/skills/`. BMAD has now migrated from `.clinerules/workflows` to `.cline/skills`.
-- [ ] Confirm current Cline skills path and whether `.cline/skills` is the correct BMAD target
-- [ ] Implement installer migration to native skills output
-- [ ] Add legacy cleanup for `.clinerules/workflows`
-- [ ] Test fresh install
-- [ ] Test reinstall/upgrade from legacy workflow output
-- [ ] Confirm ancestor conflict protection where applicable
-- [ ] Implement/extend automated tests
-- [ ] Commit
+**Install:** VS Code extension `saoudrizwan.claude-dev` — search "Cline" in Extensions or `code --install-extension saoudrizwan.claude-dev`
+
+- [x] Confirm current Cline skills path is `.cline/skills/{skill-name}/SKILL.md` with YAML frontmatter (name + description)
+- [x] Implement installer migration to native skills output
+- [x] Add legacy cleanup for `.clinerules/workflows`
+- [x] Test fresh install — 43 skills installed to `.cline/skills/`
+- [x] Test reinstall/upgrade from legacy workflow output
+- [x] Confirm no ancestor conflict protection is needed because Cline only scans workspace-local `.cline/skills/` and global `~/.cline/skills/`, with no ancestor directory inheritance
+- [x] Implement/extend automated tests — 9 assertions in test suite 18
+- [x] Commit
## Google Antigravity
@@ -85,7 +88,6 @@ Support assumption: full Agent Skills support. Antigravity docs confirm workspac
- [x] Test reinstall/upgrade from legacy workflow output
- [x] Confirm no ancestor conflict protection is needed because manual Antigravity verification in `/tmp/antigravity-ancestor-repro/parent/child` showed only the child-local `child-only` skill, with no inherited parent `.agent/skills` entry
- [x] Implement/extend automated tests
-- [x] Commit
## Auggie
@@ -98,33 +100,38 @@ Support assumption: full Agent Skills support. BMAD currently installs commands
- [x] Test reinstall/upgrade from legacy command output
- [x] Confirm no ancestor conflict protection is needed because local `auggie --workspace-root` repro showed child-local `.augment/skills` loading `child-only` but not parent `parent-only`
- [x] Implement/extend automated tests
-- [ ] Commit
+- [x] Commit
## CodeBuddy
-Support assumption: full Agent Skills support. BMAD currently installs commands to `.codebuddy/commands`; target should move to `.codebuddy/skills`.
+Support assumption: full Agent Skills support. CodeBuddy docs confirm workspace skills at `.codebuddy/skills//SKILL.md` and global skills at `~/.codebuddy/commands/`. BMAD has now migrated from `.codebuddy/commands` to `.codebuddy/skills`.
-- [ ] Confirm CodeBuddy native skills path and any naming/frontmatter requirements
-- [ ] Implement installer migration to native skills output
-- [ ] Add legacy cleanup for `.codebuddy/commands`
-- [ ] Test fresh install
-- [ ] Test reinstall/upgrade from legacy command output
-- [ ] Confirm ancestor conflict protection where applicable
-- [ ] Implement/extend automated tests
-- [ ] Commit
+**Install:** Download [Tencent CodeBuddy IDE](https://codebuddyide.net/) or install as VS Code extension `CodebuddyAI.codebuddy-ai`
+
+- [x] Confirm CodeBuddy native skills path is `.codebuddy/skills/{skill-name}/SKILL.md` with YAML frontmatter (name + description) — per docs, not IDE-verified
+- [x] Implement installer migration to native skills output
+- [x] Add legacy cleanup for `.codebuddy/commands`
+- [x] Test fresh install — 43 skills installed to `.codebuddy/skills/` (installer output only)
+- [x] Test reinstall/upgrade from legacy command output
+- [ ] **NEEDS MANUAL IDE VERIFICATION** — requires Tencent Cloud account; confirm skills appear in UI and test ancestor inheritance
+- [x] Implement/extend automated tests — 9 assertions in test suite 19
+- [x] Commit
## Crush
-Support assumption: full Agent Skills support. BMAD currently installs commands to `.crush/commands`; target should move to the platform's native skills location.
+Support assumption: full Agent Skills support. Crush scans project-local `.crush/skills/` exclusively ([GitHub issue #2072](https://github.com/charmbracelet/crush/issues/2072) confirms this and requests adding `~/.agents/skills/`). BMAD has now migrated from `.crush/commands` to `.crush/skills`.
-- [ ] Confirm Crush project-local versus global skills path and BMAD's preferred install target
-- [ ] Implement installer migration to native skills output
-- [ ] Add legacy cleanup for `.crush/commands`
-- [ ] Test fresh install
-- [ ] Test reinstall/upgrade from legacy command output
-- [ ] Confirm ancestor conflict protection where applicable
-- [ ] Implement/extend automated tests
-- [ ] Commit
+**Install:** `brew install charmbracelet/tap/crush` (macOS/Linux) or `winget install charmbracelet.crush` (Windows)
+
+- [x] Confirm Crush project-local skills path is `.crush/skills/{skill-name}/SKILL.md` — per GitHub issue #2072 confirming `.crush/skills/` is the only scan path
+- [x] Implement installer migration to native skills output
+- [x] Add legacy cleanup for `.crush/commands`
+- [x] Test fresh install — 43 skills installed to `.crush/skills/`
+- [x] Test reinstall/upgrade from legacy command output
+- [x] Confirm no ancestor conflict protection is needed because Crush only scans project-local `.crush/skills/`, no ancestor inheritance
+- [ ] **NEEDS MANUAL IDE VERIFICATION** — install Crush via brew and confirm skills appear in UI
+- [x] Implement/extend automated tests — 9 assertions in test suite 20
+- [x] Commit
## Kiro
@@ -137,7 +144,6 @@ Support assumption: full Agent Skills support. Kiro docs confirm project skills
- [x] Test reinstall/upgrade from legacy steering output
- [x] Confirm no ancestor conflict protection is needed because manual Kiro verification showed Slash-visible skills from the current workspace only, with no ancestor `.kiro/skills` inheritance
- [x] Implement/extend automated tests
-- [x] Commit
## OpenCode
@@ -150,60 +156,69 @@ Support assumption: full Agent Skills support. BMAD currently splits output betw
- [x] Test reinstall/upgrade from split legacy output
- [x] Confirm ancestor conflict protection is required because local `opencode run` repros loaded both child-local `child-only` and ancestor `parent-only`, matching the docs that project-local skill discovery walks upward to the git worktree
- [x] Implement/extend automated tests
-- [ ] Commit
+- [x] Commit
## Roo Code
Support assumption: full Agent Skills support. BMAD currently installs commands to `.roo/commands`; target should move to `.roo/skills` or the correct mode-aware skill directories.
-- [ ] Confirm Roo native skills path and whether BMAD should use generic or mode-specific skill directories
-- [ ] Implement installer migration to native skills output
-- [ ] Add legacy cleanup for `.roo/commands`
-- [ ] Test fresh install
-- [ ] Test reinstall/upgrade from legacy command output
-- [ ] Confirm ancestor conflict protection where applicable
-- [ ] Implement/extend automated tests
-- [ ] Commit
+**Install:** VS Code extension `RooVeterinaryInc.roo-cline` — search "Roo Code" in Extensions or `code --install-extension RooVeterinaryInc.roo-cline`
+
+- [x] Confirm Roo native skills path is `.roo/skills/{skill-name}/SKILL.md` with `name` frontmatter matching directory exactly (lowercase, alphanumeric + hyphens only)
+- [x] Implement installer migration to native skills output
+- [x] Add legacy cleanup for `.roo/commands`
+- [x] Test fresh install — 43 skills installed, verified in Roo Code v3.51
+- [x] Test reinstall/upgrade from legacy command output
+- [x] Confirm no ancestor conflict protection is needed because manual Roo Code v3.51 verification showed child-local `child-only` skill loaded while parent-only skill was not inherited
+- [x] Implement/extend automated tests — 7 assertions in test suite 13
+- [x] Commit
## Trae
-Support assumption: full Agent Skills support. BMAD currently installs rule files to `.trae/rules`; target should move to the platform's native skills directory.
+Support assumption: full Agent Skills support. [Trae docs](https://docs.trae.ai/ide/skills) confirm workspace skills at `.trae/skills//SKILL.md`. BMAD has now migrated from `.trae/rules` to `.trae/skills`.
-- [ ] Confirm Trae native skills path and whether the current `.trae/rules` path is still required for compatibility
-- [ ] Implement installer migration to native skills output
-- [ ] Add legacy cleanup for `.trae/rules`
-- [ ] Test fresh install
-- [ ] Test reinstall/upgrade from legacy rules output
-- [ ] Confirm ancestor conflict protection where applicable
-- [ ] Implement/extend automated tests
-- [ ] Commit
+**Install:** Download [standalone IDE](https://www.trae.ai/download) (macOS/Windows/Linux) or `winget install -e --id ByteDance.Trae`
+
+- [x] Confirm Trae native skills path is `.trae/skills/{skill-name}/SKILL.md` — per official docs
+- [x] Implement installer migration to native skills output
+- [x] Add legacy cleanup for `.trae/rules`
+- [x] Test fresh install — 43 skills installed to `.trae/skills/`
+- [x] Test reinstall/upgrade from legacy rules output
+- [x] Confirm no ancestor conflict protection is needed — Trae docs describe project-local `.trae/skills/` only
+- [ ] **NEEDS MANUAL IDE VERIFICATION** — download Trae IDE and confirm skills appear in UI
+- [x] Implement/extend automated tests — 9 assertions in test suite 21
+- [x] Commit
## GitHub Copilot
Support assumption: full Agent Skills support. BMAD currently uses a custom installer that generates `.github/agents`, `.github/prompts`, and `.github/copilot-instructions.md`; target should move to `.github/skills`.
-- [ ] Confirm GitHub Copilot native skills path and whether `.github/agents` remains necessary as a compatibility layer
-- [ ] Design the migration away from the custom prompt/agent installer model
-- [ ] Implement native skills output, ideally with shared config-driven code where practical
-- [ ] Add legacy cleanup for `.github/agents`, `.github/prompts`, and any BMAD-owned Copilot instruction file behavior that should be retired
-- [ ] Test fresh install
-- [ ] Test reinstall/upgrade from legacy custom installer output
-- [ ] Confirm ancestor conflict protection where applicable
-- [ ] Implement/extend automated tests
-- [ ] Commit
+**Install:** VS Code extension `GitHub.copilot` — search "GitHub Copilot" in Extensions or `code --install-extension GitHub.copilot`
+
+- [x] Confirm GitHub Copilot native skills path is `.github/skills/{skill-name}/SKILL.md` — also reads `.claude/skills/` automatically
+- [x] Design the migration away from the custom prompt/agent installer model — replaced 699-line custom installer with config-driven `skill_format: true`
+- [x] Implement native skills output, ideally with shared config-driven code where practical
+- [x] Add legacy cleanup for `.github/agents`, `.github/prompts`, and BMAD markers in `copilot-instructions.md`
+- [x] Test fresh install — 43 skills installed to `.github/skills/`
+- [x] Test reinstall/upgrade from legacy custom installer output — legacy dirs removed, BMAD markers stripped, user content preserved
+- [x] Confirm no ancestor conflict protection is needed because manual Copilot verification showed child-local `child-only` skill loaded while parent-only skill was not inherited
+- [x] Implement/extend automated tests — 11 assertions in test suite 17 including marker cleanup
+- [x] Commit
## KiloCoder
Support assumption: full Agent Skills support. BMAD currently uses a custom installer that writes `.kilocodemodes` and `.kilocode/workflows`; target should move to native skills output.
-- [ ] Confirm KiloCoder native skills path and whether `.kilocodemodes` should be removed entirely or retained temporarily for compatibility
-- [ ] Design the migration away from modes plus workflow markdown
-- [ ] Implement native skills output
-- [ ] Add legacy cleanup for `.kilocode/workflows` and BMAD-owned entries in `.kilocodemodes`
-- [ ] Test fresh install
-- [ ] Test reinstall/upgrade from legacy custom installer output
-- [ ] Confirm ancestor conflict protection where applicable
-- [ ] Implement/extend automated tests
+**Install:** VS Code extension `kilocode.kilo-code` — search "Kilo Code" in Extensions or `code --install-extension kilocode.kilo-code`
+
+- [x] Confirm KiloCoder native skills path is `.kilocode/skills/{skill-name}/SKILL.md` (Kilo forked from Roo Code which uses `.roo/skills/`)
+- [x] Design the migration away from modes plus workflow markdown — replaced 269-line custom kilo.js with config-driven installer entry in platform-codes.yaml
+- [x] Implement native skills output — target_dir `.kilocode/skills`, skill_format true, template_type default
+- [x] Add legacy cleanup for `.kilocode/workflows` (via legacy_targets) and BMAD-owned entries in `.kilocodemodes` (via `cleanupKiloModes()` in `_config-driven.js`, same pattern as `copilot-instructions.md` cleanup)
+- [x] Test fresh install — skills written to `.kilocode/skills/bmad-master/SKILL.md` with correct frontmatter
+- [x] Test reinstall/upgrade from legacy custom installer output — legacy workflows removed, skills installed
+- [x] Confirm no ancestor conflict protection is needed — Kilo Code (like Cline) only scans workspace-local `.kilocode/skills/`, no ancestor directory inheritance
+- [x] Implement/extend automated tests — 11 assertions in test suite 22 (config, fresh install, legacy cleanup, .kilocodemodes cleanup, reinstall)
- [ ] Commit
## Summary Gates