From ab41b2375129768231ac5ede8fc308dd30c89f4b Mon Sep 17 00:00:00 2001 From: Tim Graepel Date: Thu, 26 Feb 2026 00:45:01 +0100 Subject: [PATCH 1/3] feat: add IBM Bob platform support (#1768) Add IBM Bob (agentic IDE) as a supported IDE platform with custom installer: - Create custom Bob installer (tools/cli/installers/lib/ide/bob.js) - Add .bobmodes to .gitignore - Add platform config to tools/platform-codes.yaml - Register Bob in IDE manager's custom installer list Bob uses a custom installer (like Kilo) that creates a .bobmodes file to register custom modes and writes workflows to .bob/workflows/ Fixes #1768 --- .gitignore | 2 + tools/cli/installers/lib/ide/bob.js | 271 ++++++++++++++++++++++++ tools/cli/installers/lib/ide/manager.js | 2 +- tools/platform-codes.yaml | 6 + 4 files changed, 280 insertions(+), 1 deletion(-) create mode 100644 tools/cli/installers/lib/ide/bob.js diff --git a/.gitignore b/.gitignore index a1229c93d..dcdf6b7d0 100644 --- a/.gitignore +++ b/.gitignore @@ -54,6 +54,8 @@ _bmad-output .opencode .qwen .rovodev +.bobmodes +.bob .kilocodemodes .claude/commands .codex diff --git a/tools/cli/installers/lib/ide/bob.js b/tools/cli/installers/lib/ide/bob.js new file mode 100644 index 000000000..3eabbfbe5 --- /dev/null +++ b/tools/cli/installers/lib/ide/bob.js @@ -0,0 +1,271 @@ +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'); + +/** + * IBM Bob IDE setup handler + * Creates custom modes in .bobmodes file (similar to Kilo) + */ +class BobSetup extends BaseIdeSetup { + constructor() { + super('bob', 'IBM Bob'); + this.configFile = '.bobmodes'; + } + + /** + * Setup IBM Bob 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 bobModesPath = path.join(projectDir, this.configFile); + let config = {}; + + if (await this.pathExists(bobModesPath)) { + const existingContent = await this.readFile(bobModesPath); + try { + config = yaml.parse(existingContent) || {}; + } catch { + // If parsing fails, start fresh but warn user + await prompts.log.warn('Warning: Could not parse existing .bobmodes, 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 .bobmodes file with proper YAML structure + const finalContent = yaml.stringify(config, { lineWidth: 0 }); + await this.writeFile(bobModesPath, finalContent); + + // Generate workflow commands + const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); + const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); + + // Write to .bob/workflows/ directory + const workflowsDir = path.join(projectDir, '.bob', '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 (Bob uses same schema as Kilo/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 IBM Bob configuration + */ + async cleanup(projectDir, options = {}) { + const fs = require('fs-extra'); + const bobModesPath = path.join(projectDir, this.configFile); + + if (await fs.pathExists(bobModesPath)) { + const content = await fs.readFile(bobModesPath, '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(bobModesPath, yaml.stringify(config, { lineWidth: 0 })); + if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .bobmodes`); + } + } + } catch { + // If parsing fails, leave file as-is + if (!options.silent) await prompts.log.warn('Warning: Could not parse .bobmodes for cleanup'); + } + } + + // Clean up workflow files + const workflowsDir = path.join(projectDir, '.bob', 'workflows'); + await this.clearBmadWorkflows(workflowsDir); + } + + /** + * Install a custom agent launcher for Bob + * @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 bobmodesPath = path.join(projectDir, this.configFile); + let config = {}; + + // Read existing .bobmodes file + if (await this.pathExists(bobmodesPath)) { + const existingContent = await this.readFile(bobmodesPath); + 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: 'bob', + 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 .bobmodes file with proper YAML structure + await this.writeFile(bobmodesPath, yaml.stringify(config, { lineWidth: 0 })); + + return { + ide: 'bob', + path: this.configFile, + command: slug, + type: 'custom-agent-launcher', + }; + } +} + +module.exports = { BobSetup }; + +// Made with Bob diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 9e286fdd3..94b74ba9c 100644 --- a/tools/cli/installers/lib/ide/manager.js +++ b/tools/cli/installers/lib/ide/manager.js @@ -61,7 +61,7 @@ class IdeManager { */ async loadCustomInstallerFiles() { const ideDir = __dirname; - const customFiles = ['codex.js', 'github-copilot.js', 'kilo.js', 'rovodev.js']; + const customFiles = ['bob.js', 'codex.js', 'github-copilot.js', 'kilo.js', 'rovodev.js']; for (const file of customFiles) { const filePath = path.join(ideDir, file); diff --git a/tools/platform-codes.yaml b/tools/platform-codes.yaml index 97846a9bd..da4ad3126 100644 --- a/tools/platform-codes.yaml +++ b/tools/platform-codes.yaml @@ -49,6 +49,12 @@ platforms: category: cli description: "AI development tool" + bob: + name: "IBM Bob" + preferred: false + category: ide + description: "IBM's agentic IDE for AI-powered development" + roo: name: "Roo Cline" preferred: false From 740662350a8bba60bb5448e64ed715fadfd70977 Mon Sep 17 00:00:00 2001 From: Tim Graepel Date: Fri, 27 Feb 2026 22:36:52 +0100 Subject: [PATCH 2/3] feat(ide): add Bob IDE installer support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - bob.js: fix installCustomAgentLauncher using wrong mode schema (description/prompt/always/permissions → roleDefinition/whenToUse/ customInstructions/groups) - bob.js: add detectionPaths for .bob directory - manager.js: update comments to include bob.js in custom installer list - platform-codes.yaml: move bob entry to correct alphabetical position - eslint.config.mjs: add .bob/** to eslint ignores (like .claude, .roo, etc.) --- eslint.config.mjs | 1 + tools/cli/installers/lib/ide/bob.js | 53 ++++++++++--------- tools/cli/installers/lib/ide/manager.js | 4 +- .../installers/lib/ide/platform-codes.yaml | 7 +++ 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 23bf73aa5..95ceee0f4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -20,6 +20,7 @@ export default [ 'website/**', // Gitignored patterns 'z*/**', // z-samples, z1, z2, etc. + '.bob/**', '.claude/**', '.codex/**', '.github/chatmodes/**', diff --git a/tools/cli/installers/lib/ide/bob.js b/tools/cli/installers/lib/ide/bob.js index 3eabbfbe5..cb2e111db 100644 --- a/tools/cli/installers/lib/ide/bob.js +++ b/tools/cli/installers/lib/ide/bob.js @@ -8,12 +8,13 @@ const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generat /** * IBM Bob IDE setup handler - * Creates custom modes in .bobmodes file (similar to Kilo) + * Creates custom modes in .bob/custom_modes.yaml file */ class BobSetup extends BaseIdeSetup { constructor() { super('bob', 'IBM Bob'); - this.configFile = '.bobmodes'; + this.configFile = '.bob/custom_modes.yaml'; + this.detectionPaths = ['.bob']; } /** @@ -38,7 +39,7 @@ class BobSetup extends BaseIdeSetup { config = yaml.parse(existingContent) || {}; } catch { // If parsing fails, start fresh but warn user - await prompts.log.warn('Warning: Could not parse existing .bobmodes, starting fresh'); + await prompts.log.warn('Warning: Could not parse existing .bob/custom_modes.yaml, starting fresh'); config = {}; } } @@ -61,7 +62,7 @@ class BobSetup extends BaseIdeSetup { addedCount++; } - // Write .bobmodes file with proper YAML structure + // Write .bob/custom_modes.yaml file with proper YAML structure const finalContent = yaml.stringify(config, { lineWidth: 0 }); await this.writeFile(bobModesPath, finalContent); @@ -110,23 +111,25 @@ class BobSetup extends BaseIdeSetup { * @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); + // Extract title and icon from the compiled agent file's XML tag + // artifact.content is the launcher template which does NOT contain these attributes + let title = this.formatTitle(artifact.name); + let icon = '🤖'; - const iconMatch = artifact.content.match(/icon="([^"]+)"/); - const icon = iconMatch ? iconMatch[1] : '🤖'; + if (artifact.sourcePath && (await this.pathExists(artifact.sourcePath))) { + const agentContent = await this.readFile(artifact.sourcePath); + const titleMatch = agentContent.match(/]*\stitle="([^"]+)"/); + if (titleMatch) title = titleMatch[1]; + const iconMatch = agentContent.match(/]*\sicon="([^"]+)"/); + if (iconMatch) icon = iconMatch[1]; + } - const whenToUseMatch = artifact.content.match(/whenToUse="([^"]+)"/); - const whenToUse = whenToUseMatch ? whenToUseMatch[1] : `Use for ${title} tasks`; + const whenToUse = `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.`; + const roleDefinition = `You are a ${title} specializing in ${title.toLowerCase()} tasks.`; // Get relative path const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/'); @@ -189,12 +192,12 @@ class BobSetup extends BaseIdeSetup { if (removedCount > 0) { await fs.writeFile(bobModesPath, yaml.stringify(config, { lineWidth: 0 })); - if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .bobmodes`); + if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .bob/custom_modes.yaml`); } } } catch { // If parsing fails, leave file as-is - if (!options.silent) await prompts.log.warn('Warning: Could not parse .bobmodes for cleanup'); + if (!options.silent) await prompts.log.warn('Warning: Could not parse .bob/custom_modes.yaml for cleanup'); } } @@ -215,7 +218,7 @@ class BobSetup extends BaseIdeSetup { const bobmodesPath = path.join(projectDir, this.configFile); let config = {}; - // Read existing .bobmodes file + // Read existing .bob/custom_modes.yaml file if (await this.pathExists(bobmodesPath)) { const existingContent = await this.readFile(bobmodesPath); try { @@ -245,16 +248,18 @@ class BobSetup extends BaseIdeSetup { } // Add custom mode object + const title = `BMAD Custom: ${agentName}`; + const activationHeader = (await this.getAgentCommandHeader()).trim(); 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', + name: title, + roleDefinition: `You are a custom BMAD agent "${agentName}". Follow the persona and instructions from the agent file.`, + whenToUse: `Use for custom BMAD agent "${agentName}" tasks`, + customInstructions: `${activationHeader} Read the full agent from ${agentPath} 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'], }); - // Write .bobmodes file with proper YAML structure + // Write .bob/custom_modes.yaml file with proper YAML structure await this.writeFile(bobmodesPath, yaml.stringify(config, { lineWidth: 0 })); return { diff --git a/tools/cli/installers/lib/ide/manager.js b/tools/cli/installers/lib/ide/manager.js index 94b74ba9c..a03d6f487 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 (codex.js, github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic + * 1. Custom installer files (bob.js, codex.js, github-copilot.js, kilo.js, rovodev.js) - for platforms with unique installation logic * 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns */ class IdeManager { @@ -44,7 +44,7 @@ class IdeManager { /** * Dynamically load all IDE handlers - * 1. Load custom installer files first (codex.js, github-copilot.js, kilo.js, rovodev.js) + * 1. Load custom installer files first (bob.js, codex.js, github-copilot.js, kilo.js, rovodev.js) * 2. Load config-driven handlers from platform-codes.yaml */ async loadHandlers() { diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index 4e6ca8070..5285c2570 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -32,6 +32,13 @@ platforms: target_dir: .augment/commands template_type: default + bob: + name: "IBM Bob" + preferred: false + category: ide + description: "IBM's AI development environment" + # No installer config - uses custom bob.js (creates .bob/custom_modes.yaml) + claude-code: name: "Claude Code" preferred: true From 82411feef30b94fec1b24bbbe83ec253a68c4941 Mon Sep 17 00:00:00 2001 From: Tim Graepel Date: Fri, 27 Feb 2026 23:19:58 +0100 Subject: [PATCH 3/3] fix: address CodeRabbit PR review comments for Bob IDE support - Remove leftover '// Made with Bob' debug comment - Guard artifact.sourcePath before calling path.relative() to prevent TypeError - Fix run-on customInstructions string with proper punctuation in createModeObject - Move fs-extra require to top of file; remove inline requires from clearBmadWorkflows/cleanup - Wrap write sequence in try/catch with rollback to prevent partial state on failure - Use metadata.title and metadata.icon in installCustomAgentLauncher instead of ignoring param - Align installCustomAgentLauncher mode object with createModeObject (icon in name, fixed instructions) - Remove unused .bobmodes entry from .gitignore (.bob already covers the directory) - Sync bob description in tools/cli/installers/lib/ide/platform-codes.yaml to match tools/platform-codes.yaml --- .gitignore | 1 - tools/cli/installers/lib/ide/bob.js | 80 ++++++++++++------- .../installers/lib/ide/platform-codes.yaml | 2 +- 3 files changed, 50 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index dcdf6b7d0..4e5a70bca 100644 --- a/.gitignore +++ b/.gitignore @@ -54,7 +54,6 @@ _bmad-output .opencode .qwen .rovodev -.bobmodes .bob .kilocodemodes .claude/commands diff --git a/tools/cli/installers/lib/ide/bob.js b/tools/cli/installers/lib/ide/bob.js index cb2e111db..d9cbab54c 100644 --- a/tools/cli/installers/lib/ide/bob.js +++ b/tools/cli/installers/lib/ide/bob.js @@ -1,4 +1,5 @@ const path = require('node:path'); +const fs = require('fs-extra'); const { BaseIdeSetup } = require('./_base-ide'); const yaml = require('yaml'); const prompts = require('../../../lib/prompts'); @@ -64,30 +65,46 @@ class BobSetup extends BaseIdeSetup { // Write .bob/custom_modes.yaml file with proper YAML structure const finalContent = yaml.stringify(config, { lineWidth: 0 }); - await this.writeFile(bobModesPath, finalContent); - - // Generate workflow commands - const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); - const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(bmadDir); - - // Write to .bob/workflows/ directory const workflowsDir = path.join(projectDir, '.bob', 'workflows'); - await this.ensureDir(workflowsDir); - // Clear old BMAD workflows before writing new ones - await this.clearBmadWorkflows(workflowsDir); + let workflowCount = 0; + let taskCount = 0; + let toolCount = 0; - // Write workflow files - const workflowCount = await workflowGenerator.writeDashArtifacts(workflowsDir, workflowArtifacts); + try { + await this.writeFile(bobModesPath, finalContent); - // Generate task and tool commands - const taskToolGen = new TaskToolCommandGenerator(this.bmadFolderName); - const { artifacts: taskToolArtifacts, counts: taskToolCounts } = await taskToolGen.collectTaskToolArtifacts(bmadDir); + // Generate workflow commands + const workflowGenerator = new WorkflowCommandGenerator(this.bmadFolderName); + const { artifacts: workflowArtifacts } = await workflowGenerator.collectWorkflowArtifacts(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; + // Write to .bob/workflows/ directory + await this.ensureDir(workflowsDir); + + // Clear old BMAD workflows before writing new ones + await this.clearBmadWorkflows(workflowsDir); + + // Write workflow files + 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); + taskCount = taskToolCounts.tasks || 0; + toolCount = taskToolCounts.tools || 0; + } catch (error) { + // Roll back partial writes to avoid inconsistent state + try { + await fs.remove(bobModesPath); + } catch { + // Ignore cleanup errors + } + await this.clearBmadWorkflows(workflowsDir); + throw new Error(`Failed to write Bob configuration: ${error.message}`); + } if (!options.silent) { await prompts.log.success( @@ -131,8 +148,10 @@ class BobSetup extends BaseIdeSetup { const roleDefinition = `You are a ${title} specializing in ${title.toLowerCase()} tasks.`; - // Get relative path - const relativePath = path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/'); + // Get relative path (fall back to artifact name if sourcePath unavailable) + const relativePath = artifact.sourcePath + ? path.relative(projectDir, artifact.sourcePath).replaceAll('\\', '/') + : `${this.bmadFolderName}/agents/${artifact.name}.md`; // Build mode object (Bob uses same schema as Kilo/Roo) return { @@ -140,7 +159,7 @@ class BobSetup extends BaseIdeSetup { 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`, + customInstructions: `${activationHeader} Read the full agent definition from ${relativePath}. Start activation to assume this persona. Follow the startup section instructions. Stay in this mode until told to exit.\n`, groups: ['read', 'edit', 'browser', 'command', 'mcp'], }; } @@ -160,8 +179,7 @@ class BobSetup extends BaseIdeSetup { * @param {string} workflowsDir - Workflows directory path */ async clearBmadWorkflows(workflowsDir) { - const fs = require('fs-extra'); - if (!(await fs.pathExists(workflowsDir))) return; + if (!(await this.pathExists(workflowsDir))) return; const entries = await fs.readdir(workflowsDir); for (const entry of entries) { @@ -175,11 +193,10 @@ class BobSetup extends BaseIdeSetup { * Cleanup IBM Bob configuration */ async cleanup(projectDir, options = {}) { - const fs = require('fs-extra'); const bobModesPath = path.join(projectDir, this.configFile); - if (await fs.pathExists(bobModesPath)) { - const content = await fs.readFile(bobModesPath, 'utf8'); + if (await this.pathExists(bobModesPath)) { + const content = await this.readFile(bobModesPath); try { const config = yaml.parse(content) || {}; @@ -191,7 +208,7 @@ class BobSetup extends BaseIdeSetup { const removedCount = originalCount - config.customModes.length; if (removedCount > 0) { - await fs.writeFile(bobModesPath, yaml.stringify(config, { lineWidth: 0 })); + await this.writeFile(bobModesPath, yaml.stringify(config, { lineWidth: 0 })); if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .bob/custom_modes.yaml`); } } @@ -248,14 +265,15 @@ class BobSetup extends BaseIdeSetup { } // Add custom mode object - const title = `BMAD Custom: ${agentName}`; + const title = metadata?.title || `BMAD Custom: ${agentName}`; + const icon = metadata?.icon || '🤖'; const activationHeader = (await this.getAgentCommandHeader()).trim(); config.customModes.push({ slug: slug, - name: title, + name: `${icon} ${title}`, roleDefinition: `You are a custom BMAD agent "${agentName}". Follow the persona and instructions from the agent file.`, whenToUse: `Use for custom BMAD agent "${agentName}" tasks`, - customInstructions: `${activationHeader} Read the full agent from ${agentPath} start activation to alter your state of being follow startup section instructions stay in this being until told to exit this mode\n`, + customInstructions: `${activationHeader} Read the full agent definition from ${agentPath}. Start activation to assume this persona. Follow the startup section instructions. Stay in this mode until told to exit.\n`, groups: ['read', 'edit', 'browser', 'command', 'mcp'], }); diff --git a/tools/cli/installers/lib/ide/platform-codes.yaml b/tools/cli/installers/lib/ide/platform-codes.yaml index 5285c2570..933864f0b 100644 --- a/tools/cli/installers/lib/ide/platform-codes.yaml +++ b/tools/cli/installers/lib/ide/platform-codes.yaml @@ -36,7 +36,7 @@ platforms: name: "IBM Bob" preferred: false category: ide - description: "IBM's AI development environment" + description: "IBM's agentic IDE for AI-powered development" # No installer config - uses custom bob.js (creates .bob/custom_modes.yaml) claude-code: