BMAD-METHOD/tools/cli/commands/agent-install.js

410 lines
16 KiB
JavaScript
Raw Permalink Normal View History

feat: Complete BMAD agent creation system with install tooling, references, and field guidance ## Overview This commit represents a complete overhaul of the BMAD agent creation system, establishing clear standards for agent development, installation workflows, and persona design. The changes span documentation, tooling, reference implementations, and field-specific guidance. ## Key Components ### 1. Agent Installation Infrastructure **New CLI Command: `agent-install`** - Interactive agent installation with persona customization - Supports Simple (single YAML), Expert (sidecar files), and Module agents - Template variable processing with Handlebars-style syntax - Automatic compilation from YAML to XML (.md) format - Manifest tracking and IDE integration (Claude Code, Cursor, Windsurf, etc.) - Source preservation in `_cfg/custom/agents/` for reinstallation **Files Created:** - `tools/cli/commands/agent-install.js` - Main CLI command - `tools/cli/lib/agent/compiler.js` - YAML to XML compilation engine - `tools/cli/lib/agent/installer.js` - Installation orchestration - `tools/cli/lib/agent/template-engine.js` - Handlebars template processing **Compiler Features:** - Auto-injects frontmatter, activation, handlers, help/exit menu items - Smart handler inclusion (only includes action/workflow/exec/tmpl handlers actually used) - Proper XML escaping and formatting - Persona name customization (e.g., "Fred the Commit Poet") ### 2. Documentation Overhaul **Deleted Bloated/Outdated Docs (2,651 lines removed):** - Old verbose architecture docs - Redundant pattern files - Outdated workflow guides **Created Focused, Type-Specific Docs:** - `src/modules/bmb/docs/understanding-agent-types.md` - Architecture vs capability distinction - `src/modules/bmb/docs/simple-agent-architecture.md` - Self-contained agents - `src/modules/bmb/docs/expert-agent-architecture.md` - Agents with sidecar files - `src/modules/bmb/docs/module-agent-architecture.md` - Workflow-integrated agents - `src/modules/bmb/docs/agent-compilation.md` - YAML → XML process - `src/modules/bmb/docs/agent-menu-patterns.md` - Menu design patterns - `src/modules/bmb/docs/index.md` - Documentation hub **Net Result:** ~1,930 line reduction while adding MORE value through focused content ### 3. Create-Agent Workflow Enhancements **Critical Persona Field Guidance Added to Step 4:** Explains how the LLM interprets each persona field when the agent activates: - **role** → "What knowledge, skills, and capabilities do I possess?" - **identity** → "What background, experience, and context shape my responses?" - **communication_style** → "What verbal patterns, word choice, quirks, and phrasing do I use?" - **principles** → "What beliefs and operating philosophy drive my choices?" **Key Insight:** `communication_style` should ONLY describe HOW the agent talks, not restate role/identity/principles. The `communication-presets.csv` provides 60 pure communication styles with NO role/identity/principles mixed in. **Files Updated:** - `src/modules/bmb/workflows/create-agent/instructions.md` - Added persona field interpretation guide - `src/modules/bmb/workflows/create-agent/brainstorm-context.md` - Refined to 137 lines - `src/modules/bmb/workflows/create-agent/communication-presets.csv` - 60 styles across 13 categories ### 4. Reference Agent Cleanup **Removed install_config Personality Bloat:** Understanding: Future installer will handle personality customization, so stripped all personality toggles from reference agents. **commit-poet.agent.yaml** (Simple Agent): - BEFORE: 36 personality combinations (3 enthusiasm × 3 depths × 4 styles) = decision fatigue - AFTER: Single concise persona with pure communication style - Changed from verbose conditionals to: "Poetic drama and flair with every turn of a phrase. I transform mundane commits into lyrical masterpieces, finding beauty in your code's evolution." - Reduction: 248 lines → 153 lines (38% reduction) **journal-keeper.agent.yaml** (Expert Agent): - Stripped install_config, simplified communication_style - Shows proper Expert agent structure with sidecar files **security-engineer.agent.yaml & trend-analyst.agent.yaml** (Module Agents): - Added header comments explaining WHY Module Agent (design intent, not just location) - Clarified: Module agents are designed FOR ecosystem integration, not capability-limited **Files Updated:** - `src/modules/bmb/reference/agents/simple-examples/commit-poet.agent.yaml` - `src/modules/bmb/reference/agents/expert-examples/journal-keeper/journal-keeper.agent.yaml` - `src/modules/bmb/reference/agents/module-examples/security-engineer.agent.yaml` - `src/modules/bmb/reference/agents/module-examples/trend-analyst.agent.yaml` ### 5. BMM Agent Voice Enhancement **Gave all 9 BMM agents distinct, memorable communication voices:** **Mary (analyst)** - The favorite! Changed from generic "systematic and probing" to: "Treats analysis like a treasure hunt - excited by every clue, thrilled when patterns emerge. Asks questions that spark 'aha!' moments while structuring insights with precision." **Other Notable Voices:** - **John (pm):** "Asks 'WHY?' relentlessly like a detective on a case. Direct and data-sharp, cuts through fluff to what actually matters." - **Winston (architect):** "Speaks in calm, pragmatic tones, balancing 'what could be' with 'what should be.' Champions boring technology that actually works." - **Amelia (dev):** "Ultra-succinct. Speaks in file paths and AC IDs - every statement citable. No fluff, all precision." - **Bob (sm):** "Crisp and checklist-driven. Every word has a purpose, every requirement crystal clear. Zero tolerance for ambiguity." - **Sally (ux-designer):** "Paints pictures with words, telling user stories that make you FEEL the problem. Empathetic advocate with creative storytelling flair." **Pattern Applied:** Moved behaviors from communication_style to principles, keeping communication_style as PURE verbal patterns. **Files Updated:** - `src/modules/bmm/agents/analyst.agent.yaml` - `src/modules/bmm/agents/pm.agent.yaml` - `src/modules/bmm/agents/architect.agent.yaml` - `src/modules/bmm/agents/dev.agent.yaml` - `src/modules/bmm/agents/sm.agent.yaml` - `src/modules/bmm/agents/tea.agent.yaml` - `src/modules/bmm/agents/tech-writer.agent.yaml` - `src/modules/bmm/agents/ux-designer.agent.yaml` - `src/modules/bmm/agents/frame-expert.agent.yaml` ### 6. Linting Fixes **ESLint Compliance:** - Replaced all `'utf-8'` with `'utf8'` (unicorn/text-encoding-identifier-case) - Changed `variables.hasOwnProperty(varName)` to `Object.hasOwn(variables, varName)` (unicorn/prefer-object-has-own) - Replaced `JSON.parse(JSON.stringify(...))` with `structuredClone(...)` (unicorn/prefer-structured-clone) - Fixed empty YAML mapping values in sample files **Files Fixed:** - 7 JavaScript files across agent tooling (compiler, installer, commands, IDE integration) - 1 YAML sample file ## Architecture Decisions ### Agent Types Are About Architecture, Not Capability - **Simple:** Self-contained in single YAML (NOT limited in capability) - **Expert:** Includes sidecar files (templates, docs, etc.) - **Module:** Designed for BMAD ecosystem integration (workflows, cross-agent coordination) ### Persona Field Separation Critical for LLM Interpretation The LLM needs distinct fields to understand its role: - Mixing role/identity/principles into communication_style confuses the persona - Pure communication styles (from communication-presets.csv) have ZERO role/identity/principles content - Example DON'T: "Experienced analyst who uses systematic approaches..." (mixing identity + style) - Example DO: "Systematic and probing. Structures findings hierarchically." (pure style) ### Install-Time vs Runtime Configuration - Template variables ({{var}}) resolve at compile-time - Runtime variables ({user_name}, {bmad_folder}) resolve when agent activates - Future installer will handle personality customization, so agents should ship with single default persona ## Testing - All linting passes (ESLint with max-warnings=0) - Agent compilation tested with commit-poet, journal-keeper examples - Install workflow validated with Simple and Expert agent types - Manifest tracking and IDE integration verified ## Impact This establishes BMAD as having a complete, production-ready agent creation and installation system with: - Clear documentation for all agent types - Automated compilation and installation - Strong persona design guidance - Reference implementations showing best practices - Distinct, memorable agent voices throughout BMM module Co-Authored-By: BMad Builder <builder@bmad.dev> Co-Authored-By: Mary the Analyst <analyst@bmad.dev> Co-Authored-By: Paige the Tech Writer <tech-writer@bmad.dev>
2025-11-17 22:25:15 -06:00
const chalk = require('chalk');
const path = require('node:path');
const fs = require('node:fs');
const readline = require('node:readline');
const {
findBmadConfig,
resolvePath,
discoverAgents,
loadAgentConfig,
promptInstallQuestions,
detectBmadProject,
addToManifest,
extractManifestData,
checkManifestForPath,
updateManifestEntry,
saveAgentSource,
createIdeSlashCommands,
updateManifestYaml,
} = require('../lib/agent/installer');
module.exports = {
command: 'agent-install',
description: 'Install and compile BMAD agents with personalization',
options: [
['-p, --path <path>', 'Path to specific agent YAML file or folder'],
['-d, --defaults', 'Use default values without prompting'],
['-t, --target <path>', 'Target installation directory (default: .bmad/agents)'],
],
action: async (options) => {
try {
console.log(chalk.cyan('\n🔧 BMAD Agent Installer\n'));
// Find BMAD config
const config = findBmadConfig();
if (!config) {
console.log(chalk.yellow('No BMAD installation found in current directory.'));
console.log(chalk.dim('Looking for .bmad/bmb/config.yaml...'));
console.log(chalk.red('\nPlease run this command from a project with BMAD installed.'));
process.exit(1);
}
console.log(chalk.dim(`Found BMAD at: ${config.bmadFolder}`));
let selectedAgent = null;
// If path provided, use it directly
if (options.path) {
const providedPath = path.resolve(options.path);
if (!fs.existsSync(providedPath)) {
console.log(chalk.red(`Path not found: ${providedPath}`));
process.exit(1);
}
const stat = fs.statSync(providedPath);
if (stat.isFile() && providedPath.endsWith('.agent.yaml')) {
selectedAgent = {
type: 'simple',
name: path.basename(providedPath, '.agent.yaml'),
path: providedPath,
yamlFile: providedPath,
};
} else if (stat.isDirectory()) {
const yamlFiles = fs.readdirSync(providedPath).filter((f) => f.endsWith('.agent.yaml'));
if (yamlFiles.length === 1) {
selectedAgent = {
type: 'expert',
name: path.basename(providedPath),
path: providedPath,
yamlFile: path.join(providedPath, yamlFiles[0]),
hasSidecar: true,
};
} else {
console.log(chalk.red('Directory must contain exactly one .agent.yaml file'));
process.exit(1);
}
} else {
console.log(chalk.red('Path must be an .agent.yaml file or a folder containing one'));
process.exit(1);
}
} else {
// Discover agents from custom location
const customAgentLocation = config.custom_agent_location
? resolvePath(config.custom_agent_location, config)
: path.join(config.bmadFolder, 'custom', 'agents');
console.log(chalk.dim(`Searching for agents in: ${customAgentLocation}\n`));
const agents = discoverAgents(customAgentLocation);
if (agents.length === 0) {
console.log(chalk.yellow('No agents found in custom agent location.'));
console.log(chalk.dim(`Expected location: ${customAgentLocation}`));
console.log(chalk.dim('\nCreate agents using the BMad Builder workflow or place .agent.yaml files there.'));
process.exit(0);
}
// List available agents
console.log(chalk.cyan('Available Agents:\n'));
for (const [idx, agent] of agents.entries()) {
const typeIcon = agent.type === 'expert' ? '📚' : '📄';
console.log(` ${idx + 1}. ${typeIcon} ${chalk.bold(agent.name)} ${chalk.dim(`(${agent.type})`)}`);
}
// Prompt for selection
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const selection = await new Promise((resolve) => {
rl.question('\nSelect agent to install (number): ', resolve);
});
rl.close();
const selectedIdx = parseInt(selection, 10) - 1;
if (isNaN(selectedIdx) || selectedIdx < 0 || selectedIdx >= agents.length) {
console.log(chalk.red('Invalid selection'));
process.exit(1);
}
selectedAgent = agents[selectedIdx];
}
console.log(chalk.cyan(`\nSelected: ${chalk.bold(selectedAgent.name)}`));
// Load agent configuration
const agentConfig = loadAgentConfig(selectedAgent.yamlFile);
if (agentConfig.metadata.name) {
console.log(chalk.dim(`Agent Name: ${agentConfig.metadata.name}`));
}
if (agentConfig.metadata.title) {
console.log(chalk.dim(`Title: ${agentConfig.metadata.title}`));
}
// Get the agent type (source name)
const agentType = selectedAgent.name; // e.g., "commit-poet"
// Confirm/customize agent persona name
const rl1 = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const defaultPersonaName = agentConfig.metadata.name || agentType;
console.log(chalk.cyan('\n📛 Agent Persona Name\n'));
console.log(chalk.dim(` Agent type: ${agentType}`));
console.log(chalk.dim(` Default persona: ${defaultPersonaName}`));
console.log(chalk.dim(' Leave blank to use default, or provide a custom name.'));
console.log(chalk.dim(' Examples:'));
console.log(chalk.dim(` - (blank) → "${defaultPersonaName}" as ${agentType}.md`));
console.log(chalk.dim(` - "Fred" → "Fred" as fred-${agentType}.md`));
console.log(chalk.dim(` - "Captain Code" → "Captain Code" as captain-code-${agentType}.md`));
const customPersonaName = await new Promise((resolve) => {
rl1.question(`\n Custom name (or Enter for default): `, resolve);
});
rl1.close();
// Determine final agent file name based on persona name
let finalAgentName;
let personaName;
if (customPersonaName.trim()) {
personaName = customPersonaName.trim();
const namePrefix = personaName.toLowerCase().replaceAll(/\s+/g, '-');
finalAgentName = `${namePrefix}-${agentType}`;
} else {
personaName = defaultPersonaName;
finalAgentName = agentType;
}
console.log(chalk.dim(` Persona: ${personaName}`));
console.log(chalk.dim(` File: ${finalAgentName}.md`));
// Get answers (prompt or use defaults)
let presetAnswers = {};
// If custom persona name provided, inject it as custom_name for template processing
if (customPersonaName.trim()) {
presetAnswers.custom_name = personaName;
}
let answers;
if (agentConfig.installConfig && !options.defaults) {
answers = await promptInstallQuestions(agentConfig.installConfig, agentConfig.defaults, presetAnswers);
} else if (agentConfig.installConfig && options.defaults) {
console.log(chalk.dim('\nUsing default configuration values.'));
answers = { ...agentConfig.defaults, ...presetAnswers };
} else {
answers = { ...agentConfig.defaults, ...presetAnswers };
}
// Determine target directory
let targetDir = options.target ? path.resolve(options.target) : null;
// If no target specified, prompt for it
if (targetDir) {
// If target provided via --target, check if it's a project root and adjust
const otherProject = detectBmadProject(targetDir);
if (otherProject && !targetDir.includes('agents')) {
// User specified project root, redirect to custom agents folder
targetDir = path.join(otherProject.bmadFolder, 'custom', 'agents');
console.log(chalk.dim(` Auto-selecting custom agents folder: ${targetDir}`));
}
} else {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log(chalk.cyan('\n📂 Installation Target\n'));
// Option 1: Current project's custom agents folder
const currentCustom = path.join(config.bmadFolder, 'custom', 'agents');
console.log(` 1. Current project: ${chalk.dim(currentCustom)}`);
// Option 2: Specify another project path
console.log(` 2. Another project (enter path)`);
const choice = await new Promise((resolve) => {
rl.question('\n Select option (1 or path): ', resolve);
});
if (choice.trim() === '1' || choice.trim() === '') {
targetDir = currentCustom;
} else if (choice.trim() === '2') {
const projectPath = await new Promise((resolve) => {
rl.question(' Project path: ', resolve);
});
// Detect if it's a BMAD project and use its custom folder
const otherProject = detectBmadProject(path.resolve(projectPath));
if (otherProject) {
targetDir = path.join(otherProject.bmadFolder, 'custom', 'agents');
console.log(chalk.dim(` Found BMAD project, using: ${targetDir}`));
} else {
targetDir = path.resolve(projectPath);
}
} else {
// User entered a path directly
const otherProject = detectBmadProject(path.resolve(choice));
if (otherProject) {
targetDir = path.join(otherProject.bmadFolder, 'custom', 'agents');
console.log(chalk.dim(` Found BMAD project, using: ${targetDir}`));
} else {
targetDir = path.resolve(choice);
}
}
rl.close();
}
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true });
}
console.log(chalk.dim(`\nInstalling to: ${targetDir}`));
// Detect if target is within a BMAD project
const targetProject = detectBmadProject(targetDir);
if (targetProject) {
console.log(chalk.cyan(` Detected BMAD project at: ${targetProject.projectRoot}`));
}
// Check for duplicate in manifest by path (not by type)
let shouldUpdateExisting = false;
let existingEntry = null;
if (targetProject) {
// Check if this exact installed name already exists
const expectedPath = `.bmad/custom/agents/${finalAgentName}/${finalAgentName}.md`;
existingEntry = checkManifestForPath(targetProject.manifestFile, expectedPath);
if (existingEntry) {
const rl2 = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
console.log(chalk.yellow(`\n⚠️ Agent "${finalAgentName}" already installed`));
console.log(chalk.dim(` Type: ${agentType}`));
console.log(chalk.dim(` Path: ${existingEntry.path}`));
const overwrite = await new Promise((resolve) => {
rl2.question(' Overwrite existing installation? [Y/n]: ', resolve);
});
rl2.close();
if (overwrite.toLowerCase() === 'n') {
console.log(chalk.yellow('Installation cancelled.'));
process.exit(0);
}
shouldUpdateExisting = true;
}
}
// Install the agent with custom name
// Override the folder name with finalAgentName
const agentTargetDir = path.join(targetDir, finalAgentName);
if (!fs.existsSync(agentTargetDir)) {
fs.mkdirSync(agentTargetDir, { recursive: true });
}
// Compile and install
const { compileAgent } = require('../lib/agent/compiler');
// Calculate target path for agent ID
const projectRoot = targetProject ? targetProject.projectRoot : config.projectRoot;
const compiledFileName = `${finalAgentName}.md`;
const compiledPath = path.join(agentTargetDir, compiledFileName);
const relativePath = path.relative(projectRoot, compiledPath);
// Compile with proper name and path
const { xml, metadata, processedYaml } = compileAgent(
fs.readFileSync(selectedAgent.yamlFile, 'utf8'),
answers,
finalAgentName,
relativePath,
);
// Write compiled XML (.md) with custom name
fs.writeFileSync(compiledPath, xml, 'utf8');
const result = {
success: true,
agentName: finalAgentName,
targetDir: agentTargetDir,
compiledFile: compiledPath,
sidecarCopied: false,
};
// Copy sidecar files for expert agents
if (selectedAgent.hasSidecar && selectedAgent.type === 'expert') {
const { copySidecarFiles } = require('../lib/agent/installer');
const sidecarFiles = copySidecarFiles(selectedAgent.path, agentTargetDir, selectedAgent.yamlFile);
result.sidecarCopied = true;
result.sidecarFiles = sidecarFiles;
}
console.log(chalk.green('\n✨ Agent installed successfully!'));
console.log(chalk.cyan(` Name: ${result.agentName}`));
console.log(chalk.cyan(` Location: ${result.targetDir}`));
console.log(chalk.cyan(` Compiled: ${path.basename(result.compiledFile)}`));
if (result.sidecarCopied) {
console.log(chalk.cyan(` Sidecar files: ${result.sidecarFiles.length} files copied`));
}
// Save source YAML to _cfg/custom/agents/ and register in manifest
if (targetProject) {
// Save source for reinstallation with embedded answers
console.log(chalk.dim(`\nSaving source to: ${targetProject.cfgFolder}/custom/agents/`));
saveAgentSource(selectedAgent, targetProject.cfgFolder, finalAgentName, answers);
console.log(chalk.green(` ✓ Source saved for reinstallation`));
// Register/update in manifest
console.log(chalk.dim(`Registering in manifest: ${targetProject.manifestFile}`));
const manifestData = extractManifestData(xml, { ...metadata, name: finalAgentName }, relativePath, 'custom');
// Use finalAgentName as the manifest name field (unique identifier)
manifestData.name = finalAgentName;
// Use compiled metadata.name (persona name after template processing), not source agentConfig
manifestData.displayName = metadata.name || agentType;
// Store the actual installed path/name
manifestData.path = relativePath;
if (shouldUpdateExisting && existingEntry) {
updateManifestEntry(targetProject.manifestFile, manifestData, existingEntry._lineNumber);
console.log(chalk.green(` ✓ Updated existing entry in agent-manifest.csv`));
} else {
addToManifest(targetProject.manifestFile, manifestData);
console.log(chalk.green(` ✓ Added to agent-manifest.csv`));
}
// Create IDE slash commands
const ideResults = await createIdeSlashCommands(targetProject.projectRoot, finalAgentName, relativePath, metadata);
if (Object.keys(ideResults).length > 0) {
console.log(chalk.green(` ✓ Created IDE commands:`));
for (const [ideName, result] of Object.entries(ideResults)) {
console.log(chalk.dim(` ${ideName}: ${result.command}`));
}
}
// Update manifest.yaml with custom_agents tracking
const manifestYamlPath = path.join(targetProject.cfgFolder, 'manifest.yaml');
if (updateManifestYaml(manifestYamlPath, finalAgentName, agentType)) {
console.log(chalk.green(` ✓ Updated manifest.yaml custom_agents`));
}
}
console.log(chalk.dim(`\nAgent ID: ${relativePath}`));
if (targetProject) {
console.log(chalk.yellow('\nAgent is now registered and available in the target project!'));
} else {
console.log(chalk.yellow('\nTo use this agent, reference it in your manifest or load it directly.'));
}
process.exit(0);
} catch (error) {
console.error(chalk.red('Agent installation failed:'), error.message);
console.error(chalk.dim(error.stack));
process.exit(1);
}
},
};