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