feat(skills): migrate KiloCoder to config-driven native skills

Replace 269-line custom kilo.js installer with config-driven entry in
platform-codes.yaml targeting .kilocode/skills/ with skill_format: true.

- Add installer config: target_dir, skill_format, template_type, legacy_targets
- Add cleanupKiloModes() to strip BMAD modes from .kilocodemodes on cleanup
- Remove kilo.js from manager.js customFiles and Kilo-specific result handling
- Delete tools/cli/installers/lib/ide/kilo.js
- Add test Suite 22: 11 assertions (config, install, legacy cleanup, modes, reinstall)
- Update migration checklist with verified results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alex Verkhovsky 2026-03-07 00:34:35 -07:00
parent b8bf7bb923
commit 40d3eb393f
6 changed files with 139 additions and 289 deletions

View File

@ -1134,6 +1134,87 @@ async function runTests() {
console.log('');
// ============================================================
// Suite 22: KiloCoder Native Skills
// ============================================================
console.log(`${colors.yellow}Test Suite 22: KiloCoder Native Skills${colors.reset}\n`);
try {
clearCache();
const platformCodes22 = await loadPlatformCodes();
const kiloInstaller = platformCodes22.platforms.kilo?.installer;
assert(kiloInstaller?.target_dir === '.kilocode/skills', 'KiloCoder target_dir uses native skills path');
assert(kiloInstaller?.skill_format === true, 'KiloCoder installer enables native skill output');
assert(
Array.isArray(kiloInstaller?.legacy_targets) && kiloInstaller.legacy_targets.includes('.kilocode/workflows'),
'KiloCoder installer cleans legacy workflows output',
);
// Fresh install test
const tempProjectDir22 = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-kilo-test-'));
const installedBmadDir22 = await createTestBmadFixture();
const legacyDir22 = path.join(tempProjectDir22, '.kilocode', 'workflows');
await fs.ensureDir(legacyDir22);
await fs.writeFile(path.join(legacyDir22, 'bmad-legacy.md'), 'legacy\n');
// Create a .kilocodemodes file with BMAD modes and a user mode
const kiloModesPath22 = path.join(tempProjectDir22, '.kilocodemodes');
const yaml22 = require('yaml');
const kiloModesContent = yaml22.stringify({
customModes: [
{ slug: 'bmad-bmm-architect', name: 'BMAD Architect', roleDefinition: 'test' },
{ slug: 'bmad-core-master', name: 'BMAD Master', roleDefinition: 'test' },
{ slug: 'user-custom-mode', name: 'My Custom Mode', roleDefinition: 'user mode' },
],
});
await fs.writeFile(kiloModesPath22, kiloModesContent);
const ideManager22 = new IdeManager();
await ideManager22.ensureInitialized();
const result22 = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
silent: true,
selectedModules: ['bmm'],
});
assert(result22.success === true, 'KiloCoder setup succeeds against temp project');
const skillFile22 = path.join(tempProjectDir22, '.kilocode', 'skills', 'bmad-master', 'SKILL.md');
assert(await fs.pathExists(skillFile22), 'KiloCoder install writes SKILL.md directory output');
const skillContent22 = await fs.readFile(skillFile22, 'utf8');
const nameMatch22 = skillContent22.match(/^name:\s*(.+)$/m);
assert(nameMatch22 && nameMatch22[1].trim() === 'bmad-master', 'KiloCoder skill name frontmatter matches directory name exactly');
assert(!(await fs.pathExists(path.join(tempProjectDir22, '.kilocode', 'workflows'))), 'KiloCoder setup removes legacy workflows dir');
// Verify .kilocodemodes cleanup: BMAD modes removed, user mode preserved
const cleanedModes22 = yaml22.parse(await fs.readFile(kiloModesPath22, 'utf8'));
assert(
Array.isArray(cleanedModes22.customModes) && cleanedModes22.customModes.length === 1,
'KiloCoder cleanup removes BMAD modes from .kilocodemodes',
);
assert(cleanedModes22.customModes[0].slug === 'user-custom-mode', 'KiloCoder cleanup preserves non-BMAD modes in .kilocodemodes');
// Reinstall test
const result22b = await ideManager22.setup('kilo', tempProjectDir22, installedBmadDir22, {
silent: true,
selectedModules: ['bmm'],
});
assert(result22b.success === true, 'KiloCoder reinstall/upgrade succeeds over existing skills');
assert(await fs.pathExists(skillFile22), 'KiloCoder reinstall preserves SKILL.md output');
await fs.remove(tempProjectDir22);
await fs.remove(installedBmadDir22);
} catch (error) {
assert(false, 'KiloCoder native skills migration test succeeds', error.message);
}
console.log('');
// ============================================================
// Summary
// ============================================================

View File

@ -660,6 +660,11 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
await this.cleanupCopilotInstructions(projectDir, options);
}
// Strip BMAD modes from .kilocodemodes if present
if (this.name === 'kilo') {
await this.cleanupKiloModes(projectDir, options);
}
// Clean all target directories
if (this.installerConfig?.targets) {
const parentDirs = new Set();
@ -807,6 +812,42 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
if (!options.silent) await prompts.log.message(' Cleaned BMAD markers from copilot-instructions.md');
}
/**
* Strip BMAD-owned modes from .kilocodemodes.
* The old custom kilo.js installer added modes with slug starting with 'bmad-'.
* Parses YAML, filters out BMAD modes, rewrites. Leaves file as-is on parse failure.
*/
async cleanupKiloModes(projectDir, options = {}) {
const kiloModesPath = path.join(projectDir, '.kilocodemodes');
if (!(await fs.pathExists(kiloModesPath))) return;
const content = await fs.readFile(kiloModesPath, 'utf8');
let config;
try {
config = yaml.parse(content) || {};
} catch {
if (!options.silent) await prompts.log.warn(' Warning: Could not parse .kilocodemodes for cleanup');
return;
}
if (!Array.isArray(config.customModes)) return;
const originalCount = config.customModes.length;
config.customModes = config.customModes.filter((mode) => mode && (!mode.slug || !mode.slug.startsWith('bmad-')));
const removedCount = originalCount - config.customModes.length;
if (removedCount > 0) {
try {
await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 }));
if (!options.silent) await prompts.log.message(` Removed ${removedCount} BMAD modes from .kilocodemodes`);
} catch {
if (!options.silent) await prompts.log.warn(' Warning: Could not write .kilocodemodes during cleanup');
}
}
}
/**
* Check ancestor directories for existing BMAD files in the same target_dir.
* IDEs like Claude Code inherit commands from parent directories, so an existing

View File

@ -1,269 +0,0 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
/**
* KiloCode IDE setup handler
* Creates custom modes in .kilocodemodes file (similar to Roo)
*/
class KiloSetup extends BaseIdeSetup {
constructor() {
super('kilo', 'Kilo Code');
this.configFile = '.kilocodemodes';
}
/**
* Setup KiloCode IDE configuration
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`);
// Clean up any old BMAD installation first
await this.cleanup(projectDir, options);
// Load existing config (may contain non-BMAD modes and other settings)
const kiloModesPath = path.join(projectDir, this.configFile);
let config = {};
if (await this.pathExists(kiloModesPath)) {
const existingContent = await this.readFile(kiloModesPath);
try {
config = yaml.parse(existingContent) || {};
} catch {
// If parsing fails, start fresh but warn user
await prompts.log.warn('Warning: Could not parse existing .kilocodemodes, starting fresh');
config = {};
}
}
// Ensure customModes array exists
if (!Array.isArray(config.customModes)) {
config.customModes = [];
}
// Generate agent launchers
const agentGen = new AgentCommandGenerator(this.bmadFolderName);
const { artifacts: agentArtifacts } = await agentGen.collectAgentArtifacts(bmadDir, options.selectedModules || []);
// Create mode objects and add to config
let addedCount = 0;
for (const artifact of agentArtifacts) {
const modeObject = await this.createModeObject(artifact, projectDir);
config.customModes.push(modeObject);
addedCount++;
}
// Write .kilocodemodes file with proper YAML structure
const finalContent = yaml.stringify(config, { lineWidth: 0 });
await this.writeFile(kiloModesPath, finalContent);
// Generate workflow commands
const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName);
const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir);
// Write to .kilocode/workflows/ directory
const workflowsDir = path.join(projectDir, '.kilocode', 'workflows');
await this.ensureDir(workflowsDir);
// Clear old BMAD workflows before writing new ones
await this.clearBmadWorkflows(workflowsDir);
// Write workflow files
const workflowCount = await workflowGenerator.writeDashArtifacts(workflowsDir, workflowArtifacts);
// Generate task and tool commands
const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName);
const { artifacts: taskToolArtifacts, counts: taskToolCounts } = await taskToolGen.collectTaskToolArtifacts(bmadDir);
// Write task/tool files to workflows directory (same location as workflows)
await taskToolGen.writeDashArtifacts(workflowsDir, taskToolArtifacts);
const taskCount = taskToolCounts.tasks || 0;
const toolCount = taskToolCounts.tools || 0;
if (!options.silent) {
await prompts.log.success(
`${this.name} configured: ${addedCount} modes, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools → ${this.configFile}`,
);
}
return {
success: true,
modes: addedCount,
workflows: workflowCount,
tasks: taskCount,
tools: toolCount,
};
}
/**
* Create a mode object for an agent
* @param {Object} artifact - Agent artifact
* @param {string} projectDir - Project directory
* @returns {Object} Mode object for YAML serialization
*/
async createModeObject(artifact, projectDir) {
// Extract metadata from launcher content
const titleMatch = artifact.content.match(/title="([^"]+)"/);
const title = titleMatch ? titleMatch[1] : this.formatTitle(artifact.name);
const iconMatch = artifact.content.match(/icon="([^"]+)"/);
const icon = iconMatch ? iconMatch[1] : '🤖';
const whenToUseMatch = artifact.content.match(/whenToUse="([^"]+)"/);
const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`;
// Get the activation header from central template (trim to avoid YAML formatting issues)
const activationHeader = (await this.getAgentCommandHeader()).trim();
const roleDefinitionMatch = artifact.content.match(/roleDefinition="([^"]+)"/);
const roleDefinition = roleDefinitionMatch
? roleDefinitionMatch[1]
: `You are a ${title} specializing in ${title.toLowerCase()} tasks.`;
// Get relative path
const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/');
// Build mode object (KiloCode uses same schema as Roo)
return {
slug: `bmad-${artifact.module}-${artifact.name}`,
name: `${icon} ${title}`,
roleDefinition: roleDefinition,
whenToUse: whenToUse,
customInstructions: `${activationHeader} Read the full YAML from ${relativePath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`,
groups: ['read', 'edit', 'browser', 'command', 'mcp'],
};
}
/**
* Format name as title
*/
formatTitle(name) {
return name
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
/**
* Clear old BMAD workflow files from workflows directory
* @param {string} workflowsDir - Workflows directory path
*/
async clearBmadWorkflows(workflowsDir) {
const fs = require('fs-extra');
if (!(await fs.pathExists(workflowsDir))) return;
const entries = await fs.readdir(workflowsDir);
for (const entry of entries) {
if (entry.startsWith('bmad-') && entry.endsWith('.md')) {
await fs.remove(path.join(workflowsDir, entry));
}
}
}
/**
* Cleanup KiloCode configuration
*/
async cleanup(projectDir, options = {}) {
const fs = require('fs-extra');
const kiloModesPath = path.join(projectDir, this.configFile);
if (await fs.pathExists(kiloModesPath)) {
const content = await fs.readFile(kiloModesPath, 'utf8');
try {
const config = yaml.parse(content) || {};
if (Array.isArray(config.customModes)) {
const originalCount = config.customModes.length;
// Remove BMAD modes only (keep non-BMAD modes)
config.customModes = config.customModes.filter((mode) => !mode.slug || !mode.slug.startsWith('bmad-'));
const removedCount = originalCount - config.customModes.length;
if (removedCount > 0) {
await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 }));
if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .kilocodemodes`);
}
}
} catch {
// If parsing fails, leave file as-is
if (!options.silent) await prompts.log.warn('Warning: Could not parse .kilocodemodes for cleanup');
}
}
// Clean up workflow files
const workflowsDir = path.join(projectDir, '.kilocode', 'workflows');
await this.clearBmadWorkflows(workflowsDir);
}
/**
* Install a custom agent launcher for Kilo
* @param {string} projectDir - Project directory
* @param {string} agentName - Agent name (e.g., "fred-commit-poet")
* @param {string} agentPath - Path to compiled agent (relative to project root)
* @param {Object} metadata - Agent metadata
* @returns {Object} Installation result
*/
async installCustomAgentLauncher(projectDir, agentName, agentPath, metadata) {
const kilocodemodesPath = path.join(projectDir, this.configFile);
let config = {};
// Read existing .kilocodemodes file
if (await this.pathExists(kilocodemodesPath)) {
const existingContent = await this.readFile(kilocodemodesPath);
try {
config = yaml.parse(existingContent) || {};
} catch {
config = {};
}
}
// Ensure customModes array exists
if (!Array.isArray(config.customModes)) {
config.customModes = [];
}
// Create custom agent mode object
const slug = `bmad-custom-${agentName.toLowerCase()}`;
// Check if mode already exists
if (config.customModes.some((mode) => mode.slug === slug)) {
return {
ide: 'kilo',
path: this.configFile,
command: agentName,
type: 'custom-agent-launcher',
alreadyExists: true,
};
}
// Add custom mode object
config.customModes.push({
slug: slug,
name: `BMAD Custom: ${agentName}`,
description: `Custom BMAD agent: ${agentName}\n\n**⚠️ IMPORTANT**: Run @${agentPath} first to load the complete agent!\n\nThis is a launcher for the custom BMAD agent "${agentName}". The agent will follow the persona and instructions from the main agent file.\n`,
prompt: `@${agentPath}\n`,
always: false,
permissions: 'all',
});
// Write .kilocodemodes file with proper YAML structure
await this.writeFile(kilocodemodesPath, yaml.stringify(config, { lineWidth: 0 }));
return {
ide: 'kilo',
path: this.configFile,
command: slug,
type: 'custom-agent-launcher',
};
}
}
module.exports = { KiloSetup };

View File

@ -8,7 +8,7 @@ const prompts = require('../../../lib/prompts');
* Dynamically discovers and loads IDE handlers
*
* Loading strategy:
* 1. Custom installer files (kilo.js, rovodev.js) - for platforms with unique installation logic
* 1. Custom installer files (rovodev.js) - for platforms with unique installation logic
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
*/
class IdeManager {
@ -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 and github-copilot were migrated to config-driven (platform-codes.yaml)
* Note: codex, github-copilot, and kilo were migrated to config-driven (platform-codes.yaml)
*/
async loadCustomInstallerFiles() {
const ideDir = __dirname;
const customFiles = ['kilo.js', 'rovodev.js'];
const customFiles = ['rovodev.js'];
for (const file of customFiles) {
const filePath = path.join(ideDir, file);
@ -190,14 +190,6 @@ class IdeManager {
if (r.tasks > 0) parts.push(`${r.tasks} tasks`);
if (r.tools > 0) parts.push(`${r.tools} tools`);
detail = parts.join(', ');
} else if (handlerResult && handlerResult.modes !== undefined) {
// Kilo handler returns { success, modes, workflows, tasks, tools }
const parts = [];
if (handlerResult.modes > 0) parts.push(`${handlerResult.modes} modes`);
if (handlerResult.workflows > 0) parts.push(`${handlerResult.workflows} workflows`);
if (handlerResult.tasks > 0) parts.push(`${handlerResult.tasks} tasks`);
if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`);
detail = parts.join(', ');
}
// Propagate handler's success status (default true for backward compat)
const success = handlerResult?.success !== false;

View File

@ -150,7 +150,12 @@ platforms:
preferred: false
category: ide
description: "AI coding platform"
# No installer config - uses custom kilo.js (creates .kilocodemodes file)
installer:
legacy_targets:
- .kilocode/workflows
target_dir: .kilocode/skills
template_type: default
skill_format: true
kiro:
name: "Kiro"

View File

@ -211,14 +211,14 @@ Support assumption: full Agent Skills support. BMAD currently uses a custom inst
**Install:** VS Code extension `kilocode.kilo-code` — search "Kilo Code" in Extensions or `code --install-extension kilocode.kilo-code`
- [ ] Confirm KiloCoder native skills path and whether `.kilocodemodes` should be removed entirely or retained temporarily for compatibility
- [ ] Design the migration away from modes plus workflow markdown
- [ ] Implement native skills output
- [ ] Add legacy cleanup for `.kilocode/workflows` and BMAD-owned entries in `.kilocodemodes`
- [ ] Test fresh install
- [ ] Test reinstall/upgrade from legacy custom installer output
- [ ] Confirm ancestor conflict protection where applicable
- [ ] Implement/extend automated tests
- [x] Confirm KiloCoder native skills path is `.kilocode/skills/{skill-name}/SKILL.md` (Kilo forked from Roo Code which uses `.roo/skills/`)
- [x] Design the migration away from modes plus workflow markdown — replaced 269-line custom kilo.js with config-driven installer entry in platform-codes.yaml
- [x] Implement native skills output — target_dir `.kilocode/skills`, skill_format true, template_type default
- [x] Add legacy cleanup for `.kilocode/workflows` (via legacy_targets) and BMAD-owned entries in `.kilocodemodes` (via `cleanupKiloModes()` in `_config-driven.js`, same pattern as `copilot-instructions.md` cleanup)
- [x] Test fresh install — skills written to `.kilocode/skills/bmad-master/SKILL.md` with correct frontmatter
- [x] Test reinstall/upgrade from legacy custom installer output — legacy workflows removed, skills installed
- [x] Confirm no ancestor conflict protection is needed — Kilo Code (like Cline) only scans workspace-local `.kilocode/skills/`, no ancestor directory inheritance
- [x] Implement/extend automated tests — 11 assertions in test suite 22 (config, fresh install, legacy cleanup, .kilocodemodes cleanup, reinstall)
- [ ] Commit
## Summary Gates