Compare commits

...

7 Commits

Author SHA1 Message Date
Alex Verkhovsky ef3106461a 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.
2026-03-07 11:11:47 -07:00
Alex Verkhovsky e530a94d43 docs: update KiloCoder checklist to reflect suspended status 2026-03-07 05:59:31 -07:00
Alex Verkhovsky bf0b248a62 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)
2026-03-07 05:56:38 -07:00
Alex Verkhovsky 5113e29fc4 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.
2026-03-07 05:05:19 -07:00
Alex Verkhovsky 140cbb7b99 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.
2026-03-07 04:18:37 -07:00
Alex Verkhovsky cd3432c099 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 <noreply@anthropic.com>
2026-03-07 04:00:21 -07:00
Alex Verkhovsky b7381d283e 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 <noreply@anthropic.com>
2026-03-07 03:56:39 -07:00
9 changed files with 501 additions and 330 deletions

View File

@ -0,0 +1,14 @@
workflow-domain-research.md:
canonicalId: bmad-domain-research
type: workflow
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-market-research
type: workflow
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-technical-research
type: workflow
description: "Conduct technical research on technologies and architecture. Use when the user says 'create a technical research report on [topic]'"

View File

@ -0,0 +1,14 @@
workflow-create-prd.md:
canonicalId: bmad-create-prd
type: workflow
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-edit-prd
type: workflow
description: "Edit an existing PRD. Use when the user says 'edit this PRD'"
workflow-validate-prd.md:
canonicalId: bmad-validate-prd
type: workflow
description: "Validate a PRD against standards. Use when the user says 'validate this PRD' or 'run PRD validation'"

View File

@ -1135,82 +1135,334 @@ async function runTests() {
console.log('');
// ============================================================
// Suite 22: KiloCoder Native Skills
// Suite 22: KiloCoder Suspended
// ============================================================
console.log(`${colors.yellow}Test Suite 22: KiloCoder Native Skills${colors.reset}\n`);
console.log(`${colors.yellow}Test Suite 22: KiloCoder Suspended${colors.reset}\n`);
try {
clearCache();
const platformCodes22 = await loadPlatformCodes();
const kiloInstaller = platformCodes22.platforms.kilo?.installer;
const kiloConfig22 = platformCodes22.platforms.kilo;
assert(kiloInstaller?.target_dir === '.kilocode/skills', 'KiloCoder target_dir uses native skills path');
assert(typeof kiloConfig22?.suspended === 'string', 'KiloCoder has a suspended message in platform config');
assert(kiloInstaller?.skill_format === true, 'KiloCoder installer enables native skill output');
assert(kiloConfig22?.installer?.target_dir === '.kilocode/skills', 'KiloCoder retains target_dir config for future use');
assert(
Array.isArray(kiloInstaller?.legacy_targets) && kiloInstaller.legacy_targets.includes('.kilocode/workflows'),
'KiloCoder installer cleans legacy workflows output',
);
const ideManager22 = new IdeManager();
await ideManager22.ensureInitialized();
// Fresh install test
// 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');
// Create a .kilocodemodes file with BMAD modes and a user mode
const kiloModesPath22 = path.join(tempProjectDir22, '.kilocodemodes');
const yaml22 = require('yaml');
const kiloModesContent = yaml22.stringify({
customModes: [
{ slug: 'bmad-bmm-architect', name: 'BMAD Architect', roleDefinition: 'test' },
{ slug: 'bmad-core-master', name: 'BMAD Master', roleDefinition: 'test' },
{ slug: 'user-custom-mode', name: 'My Custom Mode', roleDefinition: 'user mode' },
],
});
await fs.writeFile(kiloModesPath22, kiloModesContent);
const ideManager22 = new IdeManager();
await ideManager22.ensureInitialized();
const result22 = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
silent: true,
selectedModules: ['bmm'],
});
assert(result22.success === true, 'KiloCoder setup succeeds against temp project');
assert(result22.success === false, 'KiloCoder setup is blocked when suspended');
assert(result22.error === 'suspended', 'KiloCoder setup returns suspended error');
const skillFile22 = path.join(tempProjectDir22, '.kilocode', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile22), 'KiloCoder install writes SKILL.md directory output');
const skillContent22 = await fs.readFile(skillFile22, 'utf8');
const nameMatch22 = skillContent22.match(/^name:\s*(.+)$/m);
assert(nameMatch22 && nameMatch22[1].trim() === 'bmad-master', 'KiloCoder skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'workflows'))), 'KiloCoder setup removes legacy workflows dir');
// Verify .kilocodemodes cleanup: BMAD modes removed, user mode preserved
const cleanedModes22 = yaml22.parse(await fs.readFile(kiloModesPath22, 'utf8'));
// Should not write new skill files
assert(
Array.isArray(cleanedModes22.customModes) && cleanedModes22.customModes.length === 1,
'KiloCoder cleanup removes BMAD modes from .kilocodemodes',
!(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'skills'))),
'KiloCoder does not create skills directory when suspended',
);
assert(cleanedModes22.customModes[0].slug === 'user-custom-mode', 'KiloCoder cleanup preserves non-BMAD modes in .kilocodemodes');
// Reinstall test
const result22b = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
silent: true,
selectedModules: ['bmm'],
});
assert(result22b.success === true, 'KiloCoder reinstall/upgrade succeeds over existing skills');
assert(await fs.pathExists(skillFile22), 'KiloCoder reinstall preserves SKILL.md output');
// 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 native skills migration test succeeds', error.message);
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');
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');
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');
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');
await fs.remove(tempProjectDir27);
await fs.remove(installedBmadDir27);
} catch (error) {
assert(false, 'bmad-os-* skill preservation test succeeds', error.message);
}
console.log('');

View File

@ -717,6 +717,25 @@ class Installer {
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 || []);

View File

@ -665,6 +665,11 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
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();
@ -751,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);
@ -848,6 +853,47 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
}
}
/**
* 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

View File

@ -8,8 +8,8 @@ const prompts = require('../../../lib/prompts');
* Dynamically discovers and loads IDE handlers
*
* Loading strategy:
* 1. Custom installer files (rovodev.js) - for platforms with unique installation logic
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
* All platforms are now config-driven from platform-codes.yaml.
* The custom installer file mechanism is retained for future use but currently has no entries.
*/
class IdeManager {
constructor() {
@ -58,11 +58,11 @@ class IdeManager {
/**
* Load custom installer files (unique installation logic)
* These files have special installation patterns that don't fit the config-driven model
* Note: codex, github-copilot, and kilo were migrated to config-driven (platform-codes.yaml)
* Note: All custom installers (codex, github-copilot, kilo, rovodev) have been migrated to config-driven (platform-codes.yaml)
*/
async loadCustomInstallerFiles() {
const ideDir = __dirname;
const customFiles = ['rovodev.js'];
const customFiles = [];
for (const file of customFiles) {
const filePath = path.join(ideDir, file);
@ -128,6 +128,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 +182,18 @@ 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') {
await handler.cleanup(projectDir, { silent: true });
}
return { success: false, ide: ideName, error: 'suspended' };
}
try {
const handlerResult = await handler.setup(projectDir, bmadDir, options);
// Build detail string from handler-returned data

View File

@ -120,8 +120,11 @@ 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"
@ -142,14 +145,18 @@ 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"
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
@ -191,8 +198,11 @@ 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"
@ -211,7 +221,12 @@ platforms:
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"

View File

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

View File

@ -129,7 +129,7 @@ Support assumption: full Agent Skills support. Crush scans project-local `.crush
- [x] Test fresh install — 43 skills installed to `.crush/skills/`
- [x] Test reinstall/upgrade from legacy command output
- [x] Confirm no ancestor conflict protection is needed because Crush only scans project-local `.crush/skills/`, no ancestor inheritance
- [ ] **NEEDS MANUAL IDE VERIFICATION** — install Crush via brew and confirm skills appear in UI
- [x] 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
@ -205,26 +205,77 @@ Support assumption: full Agent Skills support. BMAD currently uses a custom inst
- [x] Implement/extend automated tests — 11 assertions in test suite 17 including marker cleanup
- [x] Commit
## KiloCoder
## KiloCoder — SUSPENDED
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.
**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 is `.kilocode/skills/{skill-name}/SKILL.md` (Kilo forked from Roo Code which uses `.roo/skills/`)
- [x] Design the migration away from modes plus workflow markdown — replaced 269-line custom kilo.js with config-driven installer entry in platform-codes.yaml
- [x] Implement native skills output — target_dir `.kilocode/skills`, skill_format true, template_type default
- [x] Add legacy cleanup for `.kilocode/workflows` (via legacy_targets) and BMAD-owned entries in `.kilocodemodes` (via `cleanupKiloModes()` in `_config-driven.js`, same pattern as `copilot-instructions.md` cleanup)
- [x] Test fresh install — skills written to `.kilocode/skills/bmad-master/SKILL.md` with correct frontmatter
- [x] Test reinstall/upgrade from legacy custom installer output — legacy workflows removed, skills installed
- [x] Confirm no ancestor conflict protection is needed — Kilo Code (like Cline) only scans workspace-local `.kilocode/skills/`, no ancestor directory inheritance
- [x] Implement/extend automated tests — 11 assertions in test suite 22 (config, fresh install, legacy cleanup, .kilocodemodes cleanup, reinstall)
- [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 @anthropic-ai/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
## iFlow
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`.
- [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