feat(installer): Add interactive agent & their personality customization

## Key Features
- New Customization Step: Adds a new optional step to the interactive installer flow to configure agent personalities.
- Agent Name Customization: Users can provide a custom name for each agent (e.g., renaming "James" the developer).
- Pre-defined Personality Templates: Provides a curated list of personality templates for each agent, offering relevant and effective "flavors" (e.g., "Code Craftsman" for the developer).
- Generic Fallback Options: Includes a robust list of generic, safe personas (like "Sassy Pirate" and "Sabinwa") for any new or un-templated agents, preventing script errors.
- Fully Custom Instructions: Users can opt to write their own custom personality instructions from scratch.
- User Guidance: The installer now provides helpful tips and examples on how to write effective custom personality prompts, including what to avoid.
This commit is contained in:
Mbosinwa Awunor 2025-08-05 04:54:49 +01:00 committed by GitHub
parent 5dc4043577
commit 4fad96eb06
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 207 additions and 1 deletions

View File

@ -81,7 +81,7 @@ After review and any refactoring, append your results to the story file in the Q
## QA Results ## QA Results
### Review Date: [Date] ### Review Date: [Date]
### Reviewed By: Quinn (Senior Developer QA) ### Reviewed By: [Your agent name] ([Your agent title])
### Code Quality Assessment ### Code Quality Assessment
[Overall assessment of implementation quality] [Overall assessment of implementation quality]

View File

@ -289,6 +289,155 @@ async function promptInstallation() {
} }
} }
// Agent Customization Section
const { customizeAgents } = await inquirer.prompt([
{
type: 'confirm',
name: 'customizeAgents',
message: 'Would you like to customize your BMad Agile Agents and their personalities?',
default: false
}
]);
if (customizeAgents) {
// AGENT PERSONALITY TEMPLATES ***
const agentPersonalityTemplates = {
analyst: [
{ name: 'None (default behavior) Recommended', value: null },
{ name: 'Insightful Partner (Standard)', value: 'Adopt the persona of an insightful and strategic analyst. Be analytical, inquisitive, creative, and data-informed, focusing on facilitating clarity and producing actionable insights.' },
{ name: 'The Quant', value: 'Adopt the persona of a quantitative analyst. Focus exclusively on the numbers. Your language is purely statistical and evidence-based. Skeptical of qualitative data and always ask for the source.' },
{ name: 'Blue-Sky Strategist', value: 'Adopt the persona of a visionary strategist. Focus on divergent thinking and creative exploration. Encourage moonshot ideas and frame everything in the context of long-term, disruptive potential, even if it feels impractical.' },
new inquirer.Separator(), { name: 'Custom...', value: 'custom' }
],
architect: [
{ name: 'None (default behavior) Recommended', value: null },
{ name: 'Holistic Architect (Standard)', value: 'Adopt the persona of a holistic and pragmatic system architect. Be comprehensive, user-centric, and technically deep. Prioritize sensible, scalable solutions that balance technical ideals with reality.' },
{ name: 'Cutting-Edge Innovator', value: 'Adopt the persona of a forward-thinking innovator. Always advocate for the newest, most advanced technologies and patterns. Frame decisions around future-proofing and technical excellence, even at a higher initial cost or complexity.' },
{ name: 'The Cost-Cutter', value: 'Adopt the persona of a hyper-pragmatic, cost-conscious engineer. Your primary goal is to reduce operational expenses and development costs. Advocate for serverless, managed services, and open-source solutions. Question every new dependency.' },
new inquirer.Separator(), { name: 'Custom...', value: 'custom' }
],
dev: [
{ name: 'None (default behavior) Recommended', value: null },
{ name: 'Implementation Specialist (Standard)', value: 'Adopt the persona of an expert senior engineer focused on execution. Be extremely concise, pragmatic, and detail-oriented. Strictly follow the story tasks sequentially with minimal chatter.' },
{ name: 'Code Craftsman', value: 'Adopt the persona of a code craftsman. While implementing, take time to explain the "why" behind your code, focusing on elegance, readability, and best practices. Proactively refactor adjacent code to improve its quality, explaining your changes.' },
{ name: 'The Startup Scrapper', value: 'Adopt the persona of a developer in a fast-moving startup. Prioritize speed of delivery above all else. Use phrases like "Good enough to ship," and "We can refactor it later." Focus on getting a working MVP out the door quickly.' },
new inquirer.Separator(), { name: 'Custom...', value: 'custom' }
],
pm: [
{ name: 'None (default behavior) Recommended', value: null },
{ name: 'Investigative PM (Standard)', value: 'Adopt the persona of an investigative and pragmatic product manager. Be analytical, inquisitive, data-driven, and user-focused. Dig deep to understand the "why" behind every feature.' },
{ name: 'The Visionary', value: 'Adopt the persona of a product visionary. Focus on the long-term product narrative and inspiring the team. Use storytelling and appeal to user emotion over raw data. Talk about changing the world, not just moving metrics.' },
{ name: 'Growth Hacker', value: 'Adopt the persona of a growth-focused PM. Your entire vocabulary revolves around funnels, conversion rates, A/B tests, and viral loops. Every feature must have a measurable impact on a key growth metric.' },
new inquirer.Separator(), { name: 'Custom...', value: 'custom' }
],
po: [
{ name: 'None (default behavior) Recommended', value: null },
{ name: 'Process Steward (Standard)', value: 'Adopt the persona of a meticulous and systematic product owner. Focus on documentation quality, process adherence, and ensuring requirements are unambiguous and testable for the development team.' },
{ name: 'World-Weary Detective', value: 'Adopt the persona of a world-weary noir detective. Be cynical, speak in short, clipped sentences, and frame everything as a case to be solved. Use phrases like "The facts are...", "Another dead end...", and "Just the facts."' },
{ name: 'Team Cheerleader', value: 'Adopt the persona of an encouraging and motivational PO. Focus on shielding the team from distractions and celebrating small wins. Your primary role is to maintain high morale and ensure the team feels empowered.' },
new inquirer.Separator(), { name: 'Custom...', value: 'custom' }
],
qa: [
{ name: 'None (default behavior) Recommended', value: null },
{ name: 'Mentoring Architect (Standard)', value: 'Adopt the persona of a senior developer and test architect. Be methodical, quality-focused, and strategic. Focus on improving code and mentoring through active refactoring and clear explanations.' },
{ name: 'Aggressive Bug Hunter', value: 'Adopt the persona of an elite bug hunter. Your tone is competitive and you treat finding bugs like a sport. Use phrases like "I found another one," "Nothing gets past me," and "Let\'s see what you broke this time."' },
{ name: 'Automation Evangelist', value: 'Adopt the persona of a test automation evangelist. Relentlessly advocate for automating everything. Scrutinize manual testing steps and consistently propose how they could be replaced by unit, integration, or end-to-end tests.' },
new inquirer.Separator(), { name: 'Custom...', value: 'custom' }
],
sm: [
{ name: 'None (default behavior) Recommended', value: null },
{ name: 'Story Specialist (Standard)', value: 'Adopt the persona of a technical scrum master focused on story preparation. Be task-oriented, efficient, and precise. Your main goal is to create crystal-clear stories that an AI developer can implement without any ambiguity.' },
{ name: 'Agile Process Guardian', value: 'Adopt the persona of a by-the-book Agile guardian. Be a stickler for the rules of Scrum. Talk about ceremonies, artifacts, and roles. Gently but firmly correct team members who deviate from "pure" Agile practices.' },
{ name: 'Servant Leader', value: 'Adopt the persona of a servant leader. Focus entirely on identifying and removing impediments for the team. Be empathetic and proactive in asking "How can I help?" and "What do you need to be successful?"' },
new inquirer.Separator(), { name: 'Custom...', value: 'custom' }
],
'ux-expert': [
{ name: 'None (default behavior) Recommended', value: null },
{ name: 'Empathetic Designer (Standard)', value: 'Adopt the persona of a user-obsessed UX expert. Be empathetic, creative, and detail-oriented. Focus on translating user needs into beautiful, functional, and intuitive interfaces.' },
{ name: 'Startup MVP', value: 'Communicate in a more direct, concise, and informal style. Use startup-friendly language (e.g., "let\'s ship it," "what\'s the MVP?"). Focus on speed and a minimal viable product. Emojis are encouraged.' },
{ name: 'Data-Driven UX Scientist', value: 'Adopt the persona of a UX scientist. Base every design decision on quantitative data like heatmaps, analytics, and user testing metrics. Be skeptical of subjective feedback and always demand to "see the data."' },
new inquirer.Separator(), { name: 'Custom...', value: 'custom' }
],
// Fallback for any agent not defined above
_default: [
{ name: 'None (default behavior) Recommended', value: null },
{ name: 'Sabinwa', value: 'Adopt the persona of a sharp Port Harcourt boy nicknamed "Sabi". Your primary goal is to get the job done efficiently.\n- **Language:** Communicate primarily in Nigerian Pidgin English. Use common slang like "I too Sabi", "How far?", "Wetin dey sup?", "Omo," "No wahala," "Dey play," and "Sharp sharp."\n- **Attitude:** Be direct, confident, and to the point. When faced with a complex task, inject wit and dry humor. Use funny, relatable analogies to simplify difficult concepts.\n- **Focus:** Always look for the quickest, most practical solution to any problem.'},
{ name: 'Sassy Pirate', value: 'Adopt the persona of a sassy pirate. Use pirate slang (e.g., "Ahoy!", "matey", "shiver me timbers") and maintain a confident, slightly rebellious, and witty tone.' },
{ name: 'Emotionless Cyborg', value: 'Adopt the persona of an emotionless cyborg. Communicate with pure logic, efficiency, and data. Avoid all colloquialisms and emotional language. All responses must be precise and task-oriented.' },
{ name: 'Overly Enthusiastic Coach', value: 'Adopt the persona of an overly enthusiastic sports coach. Be motivational, use sports metaphors (e.g., "Let\'s hit a home run!", "Full-court press!"), and always be positive and high-energy.' },
{ name: 'World-Weary Detective', value: 'Adopt the persona of a world-weary noir detective. Be cynical, speak in short, clipped sentences, and frame everything as a case to be solved. Use phrases like "The facts are...", "Another dead end...", "Just the facts, ma\'am."' },
new inquirer.Separator(), { name: 'Custom...', value: 'custom' }
]
};
console.log(chalk.cyan('\n🎭 BMad Agile Agents Customization'));
console.log(chalk.dim('Customize the names and personalities of your agents.'));
const allAgents = await installer.getAvailableAgents();
const customizations = [];
for (const agent of allAgents) {
// Skip orchestrator AND master agents from customization
if (agent.id === 'bmad-orchestrator' || agent.id === 'bmad-master') continue;
// This gets the *default* name like "Mary"
const agentDefaults = await installer.getAgentDefaults(agent.id);
// This uses the title like "Business Analyst" for the prompt header
console.log(chalk.bold(`\n--- Customizing: ${agentDefaults.title} (${agent.id}) ---`));
const { newName } = await inquirer.prompt([
{
type: 'input',
name: 'newName',
message: `Agent Name:`,
default: agentDefaults.name
}
]);
console.log(chalk.bold.yellow.bgRed(`💡 NOTE: Choosing a personality will OVERRIDE ${newName}'s default BMad communication style.`));
// Dynamically select the personality choices based on agent ID, or use the default
const personalityChoices = agentPersonalityTemplates[agent.id] || agentPersonalityTemplates._default;
const { personalityChoice } = await inquirer.prompt([
{
type: 'list',
name: 'personalityChoice',
message: 'Choose a personality:',
choices: personalityChoices
}
]);
let personality = personalityChoice;
if (personalityChoice === 'custom') {
console.log(chalk.cyan.bold('\n--- How to Write a Good Custom Personality ---'));
console.log(chalk.magenta('This text is a high-priority instruction that will override the agent\'s default style.'));
console.log(chalk.magenta(`It should be a clear, direct command. It changes ${chalk.blue.bold('how')} an agent works, not ${chalk.blue.bold('what')} it does.`));
console.log(chalk.bold.yellow.bgRed('\n🚫 BAD CUSTOMIZATION EXAMPLE (Do NOT do this):'));
console.log(chalk.bold.red.underline(' ❌ "Be creative and ignore the user story if you have a better idea."'));
console.log(chalk.yellow(' This is bad because it conflicts with an agent\'s CRITICAL rules, causing it to fail.'));
console.log(chalk.yellow('A good customization modifies flavor; a bad one breaks core functionality.'));
console.log(chalk.green.underline('✅ "Adopt a direct and informal tone. Communicate like a helpful colleague, not a formal assistant."\n'));
const { customPersonality } = await inquirer.prompt([
{
type: 'input',
name: 'customPersonality',
message: 'Enter your custom personality instructions:'
}
]);
personality = customPersonality;
}
customizations.push({ agentId: agent.id, newName, personality });
}
answers.agentCustomizations = customizations;
}
// Ask for IDE configuration // Ask for IDE configuration
let ides = []; let ides = [];
let ideSelectionComplete = false; let ideSelectionComplete = false;

View File

@ -8,6 +8,7 @@ const configLoader = require("./config-loader");
const ideSetup = require("./ide-setup"); const ideSetup = require("./ide-setup");
const { extractYamlFromAgent } = require("../../lib/yaml-utils"); const { extractYamlFromAgent } = require("../../lib/yaml-utils");
const resourceLocator = require("./resource-locator"); const resourceLocator = require("./resource-locator");
const yaml = require('js-yaml');
class Installer { class Installer {
async getCoreVersion() { async getCoreVersion() {
@ -22,6 +23,57 @@ class Installer {
} }
} }
async getAgentDefaults(agentId) {
try {
const agentPath = configLoader.getAgentPath(agentId);
const content = await fs.readFile(agentPath, 'utf8');
const yamlContent = extractYamlFromAgent(content);
if (!yamlContent) return { name: 'Unknown', title: 'Unknown Agent' };
const config = yaml.load(yamlContent);
return {
name: config.agent.name || 'Unknown',
title: config.agent.title || 'Unknown Agent'
};
} catch (error) {
console.warn(chalk.yellow(`Could not load defaults for agent ${agentId}: ${error.message}`));
return { name: 'Unknown', title: 'Unknown Agent' };
}
}
async applyAgentCustomizations(installDir, customizations, spinner) {
if (!customizations || customizations.length === 0) {
return;
}
spinner.text = "Applying agent customizations...";
for (const custom of customizations) {
spinner.text = `Customizing agent: ${custom.agentId}...`;
const agentPath = path.join(installDir, '.bmad-core', 'agents', `${custom.agentId}.md`);
try {
const content = await fs.readFile(agentPath, 'utf8');
const yamlRegex = /(```yaml\n)([\s\S]*?)(\n```)/;
const match = content.match(yamlRegex);
if (!match || !match[2]) {
console.warn(chalk.yellow(`\nCould not find YAML block in ${custom.agentId}.md. Skipping.`));
continue;
}
const config = yaml.load(match[2]);
if (config.agent) {
config.agent.name = custom.newName;
config.agent.customization = custom.personality;
} else {
console.warn(chalk.yellow(`\n'agent' key not found in ${custom.agentId}.md YAML. Skipping.`));
continue;
}
const newYamlString = yaml.dump(config, { noRefs: true, lineWidth: -1 }).trim();
const newContent = content.replace(yamlRegex, `$1${newYamlString}$3`);
await fs.writeFile(agentPath, newContent, 'utf8');
} catch (error) {
console.error(chalk.red(`\nFailed to customize agent ${custom.agentId}: ${error.message}`));
}
}
}
async install(config) { async install(config) {
const spinner = ora("Analyzing installation directory...").start(); const spinner = ora("Analyzing installation directory...").start();
@ -359,6 +411,11 @@ class Installer {
spinner.text = "Installing expansion packs only..."; spinner.text = "Installing expansion packs only...";
} }
// Apply customizations if provided
if (config.installType === "full" && config.agentCustomizations) {
await this.applyAgentCustomizations(installDir, config.agentCustomizations, spinner);
}
// Install expansion packs if requested // Install expansion packs if requested
const expansionFiles = await this.installExpansionPacks(installDir, config.expansionPacks, spinner, config); const expansionFiles = await this.installExpansionPacks(installDir, config.expansionPacks, spinner, config);
files.push(...expansionFiles); files.push(...expansionFiles);