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>
This commit is contained in:
Alex Verkhovsky 2026-03-07 04:00:21 -07:00
parent 0969983949
commit f7d8fb6a03
6 changed files with 258 additions and 268 deletions

View File

@ -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
// ============================================================

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();
@ -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);

View File

@ -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"

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

@ -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