Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ _bmad-output
.opencode
.qwen
.rovodev
.bob
.kilocodemodes
.claude/commands
.codex
Expand Down
1 change: 1 addition & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export default [
'website/**',
// Gitignored patterns
'z*/**', // z-samples, z1, z2, etc.
'.bob/**',
'.claude/**',
'.codex/**',
'.github/chatmodes/**',
Expand Down
294 changes: 294 additions & 0 deletions tools/cli/installers/lib/ide/bob.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,294 @@
const path = require('node:path');
const fs = require('fs-extra');
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 .bob/custom_modes.yaml file
*/
class BobSetup extends BaseIdeSetup {
constructor() {
super('bob', 'IBM Bob');
this.configFile = '.bob/custom_modes.yaml';
this.detectionPaths = ['.bob'];
}

/**
* 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 .bob/custom_modes.yaml, 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 .bob/custom_modes.yaml file with proper YAML structure
const finalContent = yaml.stringify(config, { lineWidth: 0 });
const workflowsDir = path.join(projectDir, '.bob', 'workflows');

let workflowCount = 0;
let taskCount = 0;
let toolCount = 0;

try {
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
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(
`${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 title and icon from the compiled agent file's <agent> XML tag
// artifact.content is the launcher template which does NOT contain these attributes
let title = this.formatTitle(artifact.name);
let icon = '🤖';

if (artifact.sourcePath && (await this.pathExists(artifact.sourcePath))) {
const agentContent = await this.readFile(artifact.sourcePath);
const titleMatch = agentContent.match(/<agent[^>]*\stitle="([^"]+)"/);
if (titleMatch) title = titleMatch[1];
const iconMatch = agentContent.match(/<agent[^>]*\sicon="([^"]+)"/);
if (iconMatch) icon = iconMatch[1];
}

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 roleDefinition = `You are a ${title} specializing in ${title.toLowerCase()} tasks.`;

// 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 {
slug: `bmad-${artifact.module}-${artifact.name}`,
name: `${icon} ${title}`,
roleDefinition: roleDefinition,
whenToUse: whenToUse,
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'],
};
}

/**
* 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) {
if (!(await this.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 bobModesPath = path.join(projectDir, this.configFile);

if (await this.pathExists(bobModesPath)) {
const content = await this.readFile(bobModesPath);

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 this.writeFile(bobModesPath, yaml.stringify(config, { lineWidth: 0 }));
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 .bob/custom_modes.yaml 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 .bob/custom_modes.yaml 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
const title = metadata?.title || `BMAD Custom: ${agentName}`;
const icon = metadata?.icon || '🤖';
const activationHeader = (await this.getAgentCommandHeader()).trim();
config.customModes.push({
slug: slug,
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 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'],
});
Comment on lines +271 to +278
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent mode object format vs createModeObject().

createModeObject() at line 140 includes an icon in the name (${icon} ${title}), but installCustomAgentLauncher() at line 254 does not. This creates inconsistent display in the Bob UI. Also, line 258 has the same malformed customInstructions issue noted earlier.

✏️ Align with createModeObject format
     config.customModes.push({
       slug: slug,
-      name: title,
+      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`,
+      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'],
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/cli/installers/lib/ide/bob.js` around lines 253 - 260, The custom mode
added in installCustomAgentLauncher (config.customModes.push) is inconsistent
with createModeObject: include the icon prefix in the name (use `${icon}
${title}` like createModeObject) and fix the malformed customInstructions string
by ensuring activationHeader, agentPath and instructions are concatenated into a
single well-formed sentence (no stray "start activation" fragment) — update the
object created in installCustomAgentLauncher to match createModeObject’s shape
(fields: slug, name (with icon), roleDefinition, whenToUse, customInstructions,
groups) and reuse the same template/variable order (icon, title, agentName,
activationHeader, agentPath) so display and instructions are consistent with
createModeObject.


// Write .bob/custom_modes.yaml 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
6 changes: 3 additions & 3 deletions tools/cli/installers/lib/ide/manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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() {
Expand All @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions tools/cli/installers/lib/ide/platform-codes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ platforms:
target_dir: .augment/commands
template_type: default

bob:
name: "IBM Bob"
preferred: false
category: ide
description: "IBM's agentic IDE for AI-powered development"
# No installer config - uses custom bob.js (creates .bob/custom_modes.yaml)

claude-code:
name: "Claude Code"
preferred: true
Expand Down
6 changes: 6 additions & 0 deletions tools/platform-codes.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Comment on lines +52 to +57
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Description mismatch with tools/cli/installers/lib/ide/platform-codes.yaml.

This file says "IBM's agentic IDE for AI-powered development" but the installer's platform-codes.yaml (line 39) says "IBM's AI development environment". Pick one and use it consistently across both files.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tools/platform-codes.yaml` around lines 52 - 57, Pick one canonical
description for the bob entry and make both platform-codes.yaml files
consistent: update the bob key's description field (the "description" value
under the bob entry) in both the main platform-codes.yaml and the installer's
platform-codes.yaml so they match exactly (preserve quoting/formatting), e.g.,
change both to either "IBM's AI development environment" or "IBM's agentic IDE
for AI-powered development".

roo:
name: "Roo Cline"
preferred: false
Expand Down