From f7d8fb6a0378a954c971aea77b68a96403dcbd8c Mon Sep 17 00:00:00 2001 From: Alex Verkhovsky Date: Sat, 7 Mar 2026 04:00:21 -0700 Subject: [PATCH] feat(skills): migrate iFlow, QwenCoder, and Rovo Dev to native skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- test/test-installation-components.js | 154 +++++++++++ .../cli/installers/lib/ide/_config-driven.js | 46 ++++ tools/cli/installers/lib/ide/manager.js | 8 +- .../installers/lib/ide/platform-codes.yaml | 17 +- tools/cli/installers/lib/ide/rovodev.js | 257 ------------------ .../docs/native-skills-migration-checklist.md | 44 ++- 6 files changed, 258 insertions(+), 268 deletions(-) delete mode 100644 tools/cli/installers/lib/ide/rovodev.js diff --git a/test/test-installation-components.js b/test/test-installation-components.js index de702ad43..eac17e72e 100644 --- a/test/test-installation-components.js +++ b/test/test-installation-components.js @@ -1274,6 +1274,160 @@ async function runTests() { 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(''); + // ============================================================ // Summary // ============================================================ diff --git a/tools/cli/installers/lib/ide/_config-driven.js b/tools/cli/installers/lib/ide/_config-driven.js index 3ade16b47..e4c29402f 100644 --- a/tools/cli/installers/lib/ide/_config-driven.js +++ b/tools/cli/installers/lib/ide/_config-driven.js @@ -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(); @@ -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 diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 908a094a3..8ae376710 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -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); diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index bcaf60f7f..8d82badeb 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -145,8 +145,11 @@ 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" @@ -194,8 +197,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" @@ -214,7 +220,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" 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 ea8c45971..94d92d6ad 100644 --- a/tools/docs/native-skills-migration-checklist.md +++ b/tools/docs/native-skills-migration-checklist.md @@ -236,10 +236,46 @@ Support assumption: full Agent Skills support. Gemini CLI docs confirm workspace - [x] Implement/extend automated tests — 9 assertions in test suite 23 (config, fresh install, legacy cleanup, reinstall) - [ ] 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 +- [ ] 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 +- [ ] 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 +- [ ] 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