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