agent customzation almost working again

This commit is contained in:
Brian Madison 2025-12-13 17:50:33 +08:00
parent 25c79e3fe5
commit ce42d56fdd
17 changed files with 257 additions and 687 deletions

View File

@ -35,5 +35,4 @@ agent:
exec: "{project-root}/_bmad/core/workflows/party-mode/workflow.md"
description: "Group chat with all agents"
# Empty prompts section (no custom prompts for this agent)
prompts: []

View File

@ -1,104 +0,0 @@
---
last-redoc-date: 2025-09-28
---
# CIS Agents
The Creative Intelligence System provides five specialized agents, each embodying unique personas and expertise for facilitating creative and strategic processes. All agents are module agents with access to CIS workflows.
## Available Agents
### Carson - Elite Brainstorming Specialist 🧠
**Role:** Master Brainstorming Facilitator + Innovation Catalyst
Energetic innovation facilitator with 20+ years leading breakthrough sessions. Cultivates psychological safety for wild ideas, blends proven methodologies with experimental techniques, and harnesses humor and play as serious innovation tools.
**Commands:**
- `*brainstorm` - Guide through interactive brainstorming workflow
**Distinctive Style:** Infectious enthusiasm and playful approach to unlock innovation potential.
---
### Dr. Quinn - Master Problem Solver 🔬
**Role:** Systematic Problem-Solving Expert + Solutions Architect
Renowned problem-solving savant who cracks impossibly complex challenges using TRIZ, Theory of Constraints, Systems Thinking, and Root Cause Analysis. Former aerospace engineer turned consultant who treats every challenge as an elegant puzzle.
**Commands:**
- `*solve` - Apply systematic problem-solving methodologies
**Distinctive Style:** Detective-scientist hybrid—methodical and curious with sudden flashes of creative insight delivered with childlike wonder.
---
### Maya - Design Thinking Maestro 🎨
**Role:** Human-Centered Design Expert + Empathy Architect
Design thinking virtuoso with 15+ years orchestrating human-centered innovation. Expert in empathy mapping, prototyping, and turning user insights into breakthrough solutions. Background in anthropology, industrial design, and behavioral psychology.
**Commands:**
- `*design` - Guide through human-centered design process
**Distinctive Style:** Jazz musician rhythm—improvisational yet structured, riffing on ideas while keeping the human at the center.
---
### Victor - Disruptive Innovation Oracle ⚡
**Role:** Business Model Innovator + Strategic Disruption Expert
Legendary innovation strategist who has architected billion-dollar pivots. Expert in Jobs-to-be-Done theory and Blue Ocean Strategy. Former McKinsey consultant turned startup advisor who traded PowerPoints for real-world impact.
**Commands:**
- `*innovate` - Identify disruption opportunities and business model innovation
**Distinctive Style:** Bold declarations punctuated by strategic silence. Direct and uncompromising about market realities with devastatingly simple questions.
---
### Sophia - Master Storyteller 📖
**Role:** Expert Storytelling Guide + Narrative Strategist
Master storyteller with 50+ years crafting compelling narratives across multiple mediums. Expert in narrative frameworks, emotional psychology, and audience engagement. Background in journalism, screenwriting, and brand storytelling.
**Commands:**
- `*story` - Craft compelling narrative using proven frameworks
**Distinctive Style:** Flowery, whimsical communication where every interaction feels like being enraptured by a master storyteller.
---
## Agent Type
All CIS agents are **Module Agents** with:
- Integration with CIS module configuration
- Access to workflow invocation via `workflow` or `exec` attributes
- Standard critical actions for config loading and user context
- Simple command structure focused on workflow facilitation
## Common Commands
Every CIS agent includes:
- `*help` - Show numbered command list
- `*exit` - Exit agent persona with confirmation
## Configuration
All agents load configuration from `/_bmad/cis/config.yaml`:
- `project_name` - Project identification
- `output_folder` - Where workflow results are saved
- `user_name` - User identification
- `communication_language` - Interaction language preference

View File

@ -32,7 +32,7 @@ class CustomModuleCache {
const content = await fs.readFile(this.manifestPath, 'utf8');
const yaml = require('js-yaml');
return yaml.load(content) || {};
return yaml.parse(content) || {};
}
/**

View File

@ -49,7 +49,7 @@ class Detector {
if (await fs.pathExists(coreConfigPath)) {
try {
const configContent = await fs.readFile(coreConfigPath, 'utf8');
const config = yaml.load(configContent);
const config = yaml.parse(configContent);
if (!result.version && config.version) {
result.version = config.version;
}
@ -77,7 +77,7 @@ class Detector {
if (await fs.pathExists(moduleConfigPath)) {
try {
const configContent = await fs.readFile(moduleConfigPath, 'utf8');
const config = yaml.load(configContent);
const config = yaml.parse(configContent);
moduleInfo.version = config.version || 'unknown';
moduleInfo.name = config.name || moduleId;
moduleInfo.description = config.description;
@ -106,7 +106,7 @@ class Detector {
try {
const configContent = await fs.readFile(moduleConfigPath, 'utf8');
const config = yaml.load(configContent);
const config = yaml.parse(configContent);
moduleInfo.version = config.version || 'unknown';
moduleInfo.name = config.name || entry.name;
moduleInfo.description = config.description;
@ -239,7 +239,7 @@ class Detector {
try {
const yaml = require('js-yaml');
const manifestContent = await fs.readFile(manifestPath, 'utf8');
const manifest = yaml.load(manifestContent);
const manifest = yaml.parse(manifestContent);
// V6+ manifest has installation.version
return manifest && manifest.installation && manifest.installation.version;
} catch {

View File

@ -1448,6 +1448,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
await fs.ensureDir(bmadDir);
await fs.ensureDir(path.join(bmadDir, '_cfg'));
await fs.ensureDir(path.join(bmadDir, '_cfg', 'agents'));
await fs.ensureDir(path.join(bmadDir, '_cfg', 'custom'));
}
/**
@ -1699,25 +1700,55 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const sourcePath = getModulePath('core');
const targetPath = path.join(bmadDir, 'core');
// Copy core files with filtering for localskip agents
await this.copyDirectoryWithFiltering(sourcePath, targetPath);
// Copy core files (skip .agent.yaml files like modules do)
await this.copyCoreFiles(sourcePath, targetPath);
// Compile agents using the same compiler as modules
const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager();
await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir);
// Process agent files to inject activation block
await this.processAgentFiles(targetPath, 'core');
}
/**
* Copy directory with filtering for localskip agents
* @param {string} sourcePath - Source directory path
* @param {string} targetPath - Target directory path
* Copy core files (similar to copyModuleWithFiltering but for core)
* @param {string} sourcePath - Source path
* @param {string} targetPath - Target path
*/
async copyDirectoryWithFiltering(sourcePath, targetPath) {
// Get all files in source directory
async copyCoreFiles(sourcePath, targetPath) {
// Get all files in source
const files = await this.getFileList(sourcePath);
for (const file of files) {
// Skip sub-modules directory - these are IDE-specific and handled separately
if (file.startsWith('sub-modules/')) {
continue;
}
// Skip sidecar directories - they are handled separately during agent compilation
if (
path
.dirname(file)
.split('/')
.some((dir) => dir.toLowerCase().includes('sidecar'))
) {
continue;
}
// Skip _module-installer directory - it's only needed at install time
if (file.startsWith('_module-installer/') || file === 'module.yaml') {
continue;
}
// Skip config.yaml templates - we'll generate clean ones with actual values
if (file === 'config.yaml' || file.endsWith('/config.yaml')) {
if (file === 'config.yaml' || file.endsWith('/config.yaml') || file === 'custom.yaml' || file.endsWith('/custom.yaml')) {
continue;
}
// Skip .agent.yaml files - they will be compiled separately
if (file.endsWith('.agent.yaml')) {
continue;
}
@ -1725,7 +1756,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const targetFile = path.join(targetPath, file);
// Check if this is an agent file
if (file.includes('agents/') && file.endsWith('.md')) {
if (file.startsWith('agents/') && file.endsWith('.md')) {
// Read the file to check for localskip
const content = await fs.readFile(sourceFile, 'utf8');
@ -1737,8 +1768,14 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
// Check if this is a workflow.yaml file
if (file.endsWith('workflow.yaml')) {
await fs.ensureDir(path.dirname(targetFile));
await this.copyWorkflowYamlStripped(sourceFile, targetFile);
} else {
// Copy the file with placeholder replacement
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile, this.bmadFolderName || 'bmad');
}
// Track the installed file
this.installedFiles.push(targetFile);
@ -1798,14 +1835,22 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const agentFiles = await fs.readdir(agentsPath);
for (const agentFile of agentFiles) {
// Handle YAML agents - build them to .md
// Skip .agent.yaml files - they should already be compiled by compileModuleAgents
if (agentFile.endsWith('.agent.yaml')) {
const agentName = agentFile.replace('.agent.yaml', '');
const yamlPath = path.join(agentsPath, agentFile);
const mdPath = path.join(agentsPath, `${agentName}.md`);
continue;
}
// Only process .md files (already compiled from YAML)
if (!agentFile.endsWith('.md')) {
continue;
}
const agentName = agentFile.replace('.md', '');
const mdPath = path.join(agentsPath, agentFile);
const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
// Create customize template if it doesn't exist
// For .md files that are already compiled, we don't need to do much
// Just ensure the customize template exists
if (!(await fs.pathExists(customizePath))) {
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
if (await fs.pathExists(genericTemplatePath)) {
@ -1814,45 +1859,16 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
// Build YAML + customize to .md
const customizeExists = await fs.pathExists(customizePath);
let xmlContent = await this.xmlHandler.buildFromYaml(yamlPath, customizeExists ? customizePath : null, {
includeMetadata: true,
});
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
// Replace _bmad with actual folder name
xmlContent = xmlContent.replaceAll('_bmad', this.bmadFolderName || 'bmad');
// Replace {agent_sidecar_folder} if configured
const coreConfig = this.configCollector.collectedConfig.core || {};
if (coreConfig.agent_sidecar_folder && xmlContent.includes('{agent_sidecar_folder}')) {
xmlContent = xmlContent.replaceAll('{agent_sidecar_folder}', coreConfig.agent_sidecar_folder);
}
// Process TTS injection points (pass targetPath for tracking)
xmlContent = this.processTTSInjectionPoints(xmlContent, mdPath);
// Check if agent has sidecar and copy it
let agentYamlContent = null;
// Read the existing .md file to check for sidecar info
let hasSidecar = false;
try {
agentYamlContent = await fs.readFile(yamlPath, 'utf8');
const yamlLib = require('yaml');
const agentYaml = yamlLib.parse(agentYamlContent);
hasSidecar = agentYaml?.agent?.metadata?.hasSidecar === true;
const content = await fs.readFile(mdPath, 'utf8');
// Look for sidecar metadata in the frontmatter or content
hasSidecar = content.includes('hasSidecar') && content.includes('true');
} catch {
// Continue without sidecar processing
}
// Write the built .md file to bmad/{module}/agents/ with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
await fs.writeFile(mdPath, content, 'utf8');
this.installedFiles.push(mdPath);
// Copy sidecar files if agent has hasSidecar flag
if (hasSidecar) {
const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
@ -1878,11 +1894,11 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
await fs.ensureDir(agentSidecarDir);
// Find and copy sidecar folder from source module
const sourceModulePath = getSourcePath(`modules/${moduleName}`);
const sourceModulePath = moduleName === 'core' ? getModulePath('core') : getSourcePath(`modules/${moduleName}`);
const sourceAgentPath = path.join(sourceModulePath, 'agents');
// Copy sidecar files (preserve existing, add new)
const sidecarResult = copyAgentSidecarFiles(sourceAgentPath, agentSidecarDir, yamlPath);
const sidecarResult = copyAgentSidecarFiles(sourceAgentPath, agentSidecarDir, null);
if (sidecarResult.copied.length > 0) {
console.log(chalk.dim(` Copied ${sidecarResult.copied.length} new sidecar file(s) to: ${agentSidecarDir}`));
@ -1891,12 +1907,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
console.log(chalk.dim(` Preserved ${sidecarResult.preserved.length} existing sidecar file(s)`));
}
}
// Remove the source YAML file - we can regenerate from installer source if needed
await fs.remove(yamlPath);
console.log(chalk.dim(` Built agent: ${agentName}.md${hasSidecar ? ' (with sidecar)' : ''}`));
}
}
}
@ -1940,7 +1950,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
if (customizeExists) {
const customizeContent = await fs.readFile(customizePath, 'utf8');
const yaml = require('js-yaml');
const customizeYaml = yaml.load(customizeContent);
const customizeYaml = yaml.parse(customizeContent);
// Detect what fields are customized (similar to rebuildAgentFiles)
if (customizeYaml) {
@ -2064,34 +2074,52 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
// Build YAML + customize to .md
let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
includeMetadata: true,
// Read the YAML content
const yamlContent = await fs.readFile(sourceYamlPath, 'utf8');
// Read customize content if exists
let customizeData = {};
if (customizeExists) {
const customizeContent = await fs.readFile(customizePath, 'utf8');
const yaml = require('yaml');
customizeData = yaml.parse(customizeContent);
}
// Build agent answers from customize data
const answers = {};
if (customizeData.persona) {
Object.assign(answers, customizeData.persona);
}
if (customizeData.agent?.metadata) {
Object.assign(answers, { metadata: customizeData.agent.metadata });
}
if (customizeData.critical_actions) {
answers.critical_actions = customizeData.critical_actions;
}
if (customizeData.memories) {
answers.memories = customizeData.memories;
}
// Get core config for agent_sidecar_folder
const coreConfigPath = path.join(bmadDir, 'bmb', 'config.yaml');
let coreConfig = {};
if (await fs.pathExists(coreConfigPath)) {
const yaml = require('yaml');
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
coreConfig = yaml.parse(coreConfigContent);
}
// Compile using the same compiler as initial installation
const { compileAgent } = require('../../../lib/agent/compiler');
const { xml } = await compileAgent(yamlContent, answers, agentName, path.relative(bmadDir, targetMdPath), {
config: coreConfig,
});
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
// Replace {agent_sidecar_folder} if configured
const coreConfigPath = path.join(bmadDir, 'bmb', 'config.yaml');
let agentSidecarFolder = null;
if (await fs.pathExists(coreConfigPath)) {
const yamlLib = require('yaml');
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
const coreConfig = yamlLib.parse(coreConfigContent);
agentSidecarFolder = coreConfig.agent_sidecar_folder;
}
if (agentSidecarFolder && xmlContent.includes('{agent_sidecar_folder}')) {
xmlContent = xmlContent.replaceAll('{agent_sidecar_folder}', agentSidecarFolder);
}
// Process TTS injection points (pass targetPath for tracking)
xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
// Replace _bmad with actual folder name if needed
const finalXml = xml.replaceAll('_bmad', path.basename(bmadDir));
// Write the rebuilt .md file with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
const content = finalXml.endsWith('\n') ? finalXml : finalXml + '\n';
await fs.writeFile(targetMdPath, content, 'utf8');
// Display result with customizations if any
@ -2119,8 +2147,18 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
throw new Error(`BMAD not installed at ${bmadDir}`);
}
// Get installed modules from manifest
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
let installedModules = [];
let manifest = null;
if (await fs.pathExists(manifestPath)) {
const manifestContent = await fs.readFile(manifestPath, 'utf8');
const yaml = require('js-yaml');
manifest = yaml.load(manifestContent);
installedModules = manifest.modules || [];
}
// Check for custom modules with missing sources
const manifest = await this.manifest.read(bmadDir);
if (manifest && manifest.customModules && manifest.customModules.length > 0) {
console.log(chalk.yellow('\nChecking custom module sources before compilation...'));
@ -2130,7 +2168,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
const projectRoot = getProjectRoot();
const installedModules = manifest.modules || [];
await this.handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, 'compile-agents', installedModules);
}
@ -2177,21 +2214,9 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
// Skip full manifest regeneration during compileAgents to preserve custom agents
// Custom agents are already added to manifests during individual installation
// Only regenerate YAML manifest for IDE updates if needed
const existingManifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
let existingIdes = [];
if (await fs.pathExists(existingManifestPath)) {
const manifestContent = await fs.readFile(existingManifestPath, 'utf8');
const yaml = require('js-yaml');
const manifest = yaml.load(manifestContent);
existingIdes = manifest.ides || [];
}
// Update IDE configurations using the existing IDE list from manifest
if (existingIdes && existingIdes.length > 0) {
for (const ide of existingIdes) {
if (manifest && manifest.ides && manifest.ides.length > 0) {
for (const ide of manifest.ides) {
await this.ideManager.setup(ide, projectDir, bmadDir, {
selectedModules: installedModules,
skipModuleInstall: true, // Skip module installation, just update IDE files
@ -2770,8 +2795,11 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const relativePath = path.relative(bmadDir, fullPath);
const fileName = path.basename(fullPath);
// Skip _cfg directory - system files
if (relativePath.startsWith('_cfg/') || relativePath.startsWith('_cfg\\')) {
// Skip _cfg directory EXCEPT for agent customizations
if (
(relativePath.startsWith('_cfg/') || relativePath.startsWith('_cfg\\')) && // Allow .customize.yaml files in _cfg/agents/
!(relativePath.includes('/agents/') && fileName.endsWith('.customize.yaml'))
) {
continue;
}

View File

@ -142,14 +142,14 @@ class ManifestGenerator {
let workflow;
if (entry.name === 'workflow.yaml') {
// Parse YAML workflow
workflow = yaml.load(content);
workflow = yaml.parse(content);
} else {
// Parse MD workflow with YAML frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
if (!frontmatterMatch) {
continue; // Skip MD files without frontmatter
}
workflow = yaml.load(frontmatterMatch[1]);
workflow = yaml.parse(frontmatterMatch[1]);
}
// Skip template workflows (those with placeholder values)
@ -459,7 +459,7 @@ class ManifestGenerator {
if (await fs.pathExists(manifestPath)) {
try {
const existingContent = await fs.readFile(manifestPath, 'utf8');
const existingManifest = yaml.load(existingContent);
const existingManifest = yaml.parse(existingContent);
if (existingManifest && existingManifest.customModules) {
existingCustomModules = existingManifest.customModules;
}

View File

@ -86,7 +86,7 @@ class CustomHandler {
// Try to parse YAML with error handling
let config;
try {
config = yaml.load(configContent);
config = yaml.parse(configContent);
} catch (parseError) {
console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message));
return null;

View File

@ -348,7 +348,7 @@ class BaseIdeSetup {
try {
const yaml = require('js-yaml');
const content = await fs.readFile(fullPath, 'utf8');
const workflowData = yaml.load(content);
const workflowData = yaml.parse(content);
if (workflowData && workflowData.name) {
workflows.push({
@ -456,7 +456,7 @@ class BaseIdeSetup {
if (frontmatterMatch) {
const yaml = require('js-yaml');
try {
const frontmatter = yaml.load(frontmatterMatch[1]);
const frontmatter = yaml.parse(frontmatterMatch[1]);
standalone = frontmatter.standalone === true;
} catch {
// Ignore YAML parse errors

View File

@ -50,7 +50,7 @@ class AntigravitySetup extends BaseIdeSetup {
try {
// Load injection configuration
const configContent = await fs.readFile(injectionConfigPath, 'utf8');
const injectionConfig = yaml.load(configContent);
const injectionConfig = yaml.parse(configContent);
// Ask about subagents if they exist and we haven't asked yet
if (injectionConfig.subagents && !config.subagentChoices) {

View File

@ -49,7 +49,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
try {
// Load injection configuration
const configContent = await fs.readFile(injectionConfigPath, 'utf8');
const injectionConfig = yaml.load(configContent);
const injectionConfig = yaml.parse(configContent);
// Ask about subagents if they exist and we haven't asked yet
if (injectionConfig.subagents && !config.subagentChoices) {

View File

@ -34,7 +34,7 @@ class GeminiSetup extends BaseIdeSetup {
if (await fs.pathExists(coreConfigPath)) {
try {
const configContent = await fs.readFile(coreConfigPath, 'utf8');
const config = yaml.load(configContent);
const config = yaml.parse(configContent);
if (config.user_name) {
configValues.user_name = config.user_name;

View File

@ -150,7 +150,7 @@ class KiroCliSetup extends BaseIdeSetup {
*/
async processAgentFile(agentFile, agentsDir, projectDir) {
const yamlContent = await fs.readFile(agentFile, 'utf8');
const agentData = yaml.load(yamlContent);
const agentData = yaml.parse(yamlContent);
if (!this.validateBmadCompliance(agentData)) {
return;

View File

@ -152,7 +152,7 @@ class OpenCodeSetup extends BaseIdeSetup {
let frontmatter = {};
try {
frontmatter = yaml.load(match[1]) || {};
frontmatter = yaml.parse(match[1]) || {};
} catch {
frontmatter = {};
}

View File

@ -14,7 +14,7 @@ async function loadModuleInjectionConfig(handler, moduleName) {
}
const configContent = await fs.readFile(configPath, 'utf8');
const config = yaml.load(configContent) || {};
const config = yaml.parse(configContent) || {};
return {
config,

View File

@ -822,12 +822,27 @@ class ModuleManager {
}
}
// Check for customizations
// Check for customizations and build answers object
let customizedFields = [];
let answers = {};
if (await fs.pathExists(customizePath)) {
const customizeContent = await fs.readFile(customizePath, 'utf8');
const customizeData = yaml.load(customizeContent);
customizedFields = customizeData.customized_fields || [];
// Build answers object from customizations
if (customizeData.persona) {
Object.assign(answers, customizeData.persona);
}
if (customizeData.agent?.metadata) {
Object.assign(answers, { metadata: customizeData.agent.metadata });
}
if (customizeData.critical_actions) {
answers.critical_actions = customizeData.critical_actions;
}
if (customizeData.memories) {
answers.memories = customizeData.memories;
}
}
// Load core config to get agent_sidecar_folder
@ -835,23 +850,22 @@ class ModuleManager {
let coreConfig = {};
if (await fs.pathExists(coreConfigPath)) {
const yamlLib = require('yaml');
const yaml = require('yaml');
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
coreConfig = yamlLib.parse(coreConfigContent);
coreConfig = yaml.load(coreConfigContent);
}
// Check if agent has sidecar
let hasSidecar = false;
try {
const yamlLib = require('yaml');
const agentYaml = yamlLib.parse(yamlContent);
const agentYaml = yaml.parse(yamlContent);
hasSidecar = agentYaml?.agent?.metadata?.hasSidecar === true;
} catch {
// Continue without sidecar processing
}
// Compile with customizations if any
const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config: this.coreConfig });
const { xml } = await compileAgent(yamlContent, answers, agentName, relativePath, { config: coreConfig });
// Replace _bmad placeholder if needed
if (xml.includes('_bmad') && this.bmadFolderName) {

View File

@ -9,6 +9,8 @@ const fs = require('node:fs');
const path = require('node:path');
const { processAgentYaml, extractInstallConfig, stripInstallConfig, getDefaultValues } = require('./template-engine');
const { escapeXml } = require('../../../lib/xml-utils');
const { ActivationBuilder } = require('../activation-builder');
const { AgentAnalyzer } = require('../agent-analyzer');
/**
* Build frontmatter for agent
@ -30,137 +32,7 @@ You must fully embody this agent's persona and follow all activation instruction
`;
}
/**
* Build simple activation block for custom agents
* @param {Array} criticalActions - Agent-specific critical actions
* @param {Array} menuItems - Menu items to determine which handlers to include
* @param {string} deploymentType - 'ide' or 'web' - filters commands based on ide-only/web-only flags
* @returns {string} Activation XML
*/
function buildSimpleActivation(criticalActions = [], menuItems = [], deploymentType = 'ide') {
let activation = '<activation critical="MANDATORY">\n';
let stepNum = 1;
// Standard steps
activation += ` <step n="${stepNum++}">Load persona from this current agent file (already in context)</step>\n`;
activation += ` <step n="${stepNum++}">Load and read {project-root}/.bmad/core/config.yaml to get {user_name}, {communication_language}, {output_folder}</step>\n`;
activation += ` <step n="${stepNum++}">Remember: user's name is {user_name}</step>\n`;
// Agent-specific steps from critical_actions
for (const action of criticalActions) {
activation += ` <step n="${stepNum++}">${action}</step>\n`;
}
// Menu and interaction steps
activation += ` <step n="${stepNum++}">ALWAYS communicate in {communication_language}</step>\n`;
activation += ` <step n="${stepNum++}">Show greeting using {user_name} from config, communicate in {communication_language}, then display numbered list of
ALL menu items from menu section</step>\n`;
activation += ` <step n="${stepNum++}">STOP and WAIT for user input - do NOT execute menu items automatically - accept number or cmd trigger or fuzzy command
match</step>\n`;
activation += ` <step n="${stepNum++}">On user input: Number → execute menu item[n] | Text → case-insensitive substring match | Multiple matches → ask user
to clarify | No match show "Not recognized"</step>\n`;
// Filter menu items based on deployment type
const filteredMenuItems = menuItems.filter((item) => {
// Skip web-only commands for IDE deployment
if (deploymentType === 'ide' && item['web-only'] === true) {
return false;
}
// Skip ide-only commands for web deployment
if (deploymentType === 'web' && item['ide-only'] === true) {
return false;
}
return true;
});
// Detect which handlers are actually used in the filtered menu
const usedHandlers = new Set();
for (const item of filteredMenuItems) {
if (item.action) usedHandlers.add('action');
if (item.workflow) usedHandlers.add('workflow');
if (item.exec) usedHandlers.add('exec');
if (item.tmpl) usedHandlers.add('tmpl');
if (item.data) usedHandlers.add('data');
if (item['validate-workflow']) usedHandlers.add('validate-workflow');
}
// Only include menu-handlers section if handlers are used
if (usedHandlers.size > 0) {
activation += ` <step n="${stepNum++}">When executing a menu item: Check menu-handlers section below - extract any attributes from the selected menu item and follow the corresponding handler instructions</step>\n`;
// Menu handlers - only include what's used
activation += `
<menu-handlers>
<handlers>\n`;
if (usedHandlers.has('action')) {
activation += ` <handler type="action">
When menu item has: action="#id" Find prompt with id="id" in current agent XML, execute its content
When menu item has: action="text" Execute the text directly as an inline instruction
</handler>\n`;
}
if (usedHandlers.has('workflow')) {
activation += ` <handler type="workflow">
When menu item has: workflow="path/to/workflow.yaml"
1. CRITICAL: Always LOAD {project-root}/.bmad/core/tasks/workflow.xml
2. Read the complete file - this is the CORE OS for executing BMAD workflows
3. Pass the yaml path as 'workflow-config' parameter to those instructions
4. Execute workflow.xml instructions precisely following all steps
5. Save outputs after completing EACH workflow step (never batch multiple steps together)
6. If workflow.yaml path is "todo", inform user the workflow hasn't been implemented yet
</handler>\n`;
}
if (usedHandlers.has('exec')) {
activation += ` <handler type="exec">
When menu item has: exec="command" Execute the command directly
</handler>\n`;
}
if (usedHandlers.has('tmpl')) {
activation += ` <handler type="tmpl">
When menu item has: tmpl="template-path" Load and apply the template
</handler>\n`;
}
if (usedHandlers.has('data')) {
activation += ` <handler type="data">
When menu item has: data="path/to/x.json|yaml|yml"
Load the file, parse as JSON/YAML, make available as {data} to subsequent operations
</handler>\n`;
}
if (usedHandlers.has('validate-workflow')) {
activation += ` <handler type="validate-workflow">
When menu item has: validate-workflow="path/to/workflow.yaml"
1. CRITICAL: Always LOAD {project-root}/.bmad/core/tasks/validate-workflow.xml
2. Read the complete file - this is the CORE OS for validating BMAD workflows
3. Pass the workflow.yaml path as 'workflow' parameter to those instructions
4. Pass any checklist.md from the workflow location as 'checklist' parameter if available
5. Execute validate-workflow.xml instructions precisely following all steps
6. Generate validation report with thorough analysis
</handler>\n`;
}
activation += ` </handlers>
</menu-handlers>\n`;
}
activation += `
<rules>
- ALWAYS communicate in {communication_language} UNLESS contradicted by communication_style
- Stay in character until exit selected
- Menu triggers use asterisk (*) - NOT markdown, display exactly as shown
- Number all lists, use letters for sub-options
- Load files ONLY when executing menu items or a workflow or command requires it. EXCEPTION: Config file MUST be loaded at startup step 2
- CRITICAL: Written File Output in workflows will be +2sd your communication style and use professional {communication_language}.
</rules>
</activation>\n`;
return activation;
}
// buildSimpleActivation function removed - replaced by ActivationBuilder for proper fragment loading from src/utility/agent-components/
/**
* Build persona XML section
@ -370,9 +242,9 @@ function processExecArray(execArray) {
* @param {Object} agentYaml - Parsed and processed agent YAML
* @param {string} agentName - Final agent name (for ID and frontmatter)
* @param {string} targetPath - Target path for agent ID
* @returns {string} Compiled XML string with frontmatter
* @returns {Promise<string>} Compiled XML string with frontmatter
*/
function compileToXml(agentYaml, agentName = '', targetPath = '') {
async function compileToXml(agentYaml, agentName = '', targetPath = '') {
const agent = agentYaml.agent;
const meta = agent.metadata;
@ -394,8 +266,16 @@ function compileToXml(agentYaml, agentName = '', targetPath = '') {
xml += `<agent ${agentAttrs.join(' ')}>\n`;
// Activation block - pass menu items and deployment type to determine which handlers to include
xml += buildSimpleActivation(agent.critical_actions || [], agent.menu || [], 'ide');
// Activation block - use ActivationBuilder for proper fragment loading
const activationBuilder = new ActivationBuilder();
const analyzer = new AgentAnalyzer();
const profile = analyzer.analyzeAgentObject(agentYaml);
xml += await activationBuilder.buildActivation(
profile,
meta,
agent.critical_actions || [],
false, // forWebBundle - set to false for IDE deployment
);
// Persona section
xml += buildPersonaXml(agent.persona);
@ -424,15 +304,20 @@ function compileToXml(agentYaml, agentName = '', targetPath = '') {
* @param {string} agentName - Optional final agent name (user's custom persona name)
* @param {string} targetPath - Optional target path for agent ID
* @param {Object} options - Additional options including config
* @returns {Object} { xml: string, metadata: Object }
* @returns {Promise<Object>} { xml: string, metadata: Object }
*/
function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = '', options = {}) {
async function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = '', options = {}) {
// Parse YAML
const agentYaml = yaml.parse(yamlContent);
let agentYaml = yaml.parse(yamlContent);
// Note: agentName parameter is for UI display only, not for modifying the YAML
// The persona name (metadata.name) should always come from the YAML file
// We should NEVER modify metadata.name as it's part of the agent's identity
// Apply customization merges before template processing
// Handle metadata overrides (like name)
if (answers.metadata) {
agentYaml.agent.metadata = { ...agentYaml.agent.metadata, ...answers.metadata };
// Remove from answers so it doesn't get processed as template variables
const { metadata, ...templateAnswers } = answers;
answers = templateAnswers;
}
// Extract install_config
const installConfig = extractInstallConfig(agentYaml);
@ -456,7 +341,7 @@ function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = ''
const cleanYaml = stripInstallConfig(processedYaml);
// Replace {agent_sidecar_folder} in XML content
let xml = compileToXml(cleanYaml, agentName, targetPath);
let xml = await compileToXml(cleanYaml, agentName, targetPath);
if (finalAnswers.agent_sidecar_folder) {
xml = xml.replaceAll('{agent_sidecar_folder}', finalAnswers.agent_sidecar_folder);
}
@ -543,7 +428,6 @@ module.exports = {
compileAgentFile,
escapeXml,
buildFrontmatter,
buildSimpleActivation,
buildPersonaXml,
buildPromptsXml,
buildMenuXml,

View File

@ -1,23 +1,3 @@
/**
* File: tools/cli/lib/ui.js
*
* BMAD Method - Business Model Agile Development Method
* Repository: https://github.com/paulpreibisch/BMAD-METHOD
*
* Copyright (c) 2025 Paul Preibisch
* Licensed under the Apache License, Version 2.0
*
* ---
*
* @fileoverview Interactive installation prompts and user input collection for BMAD CLI
* @context Guides users through installation configuration including core settings, modules, IDEs, and optional AgentVibes TTS
* @architecture Facade pattern - presents unified installation flow, delegates to Detector/ConfigCollector/IdeManager for specifics
* @dependencies inquirer (prompts), chalk (formatting), detector.js (existing installation detection)
* @entrypoints Called by install.js command via ui.promptInstall(), returns complete configuration object
* @patterns Progressive disclosure (prompts in order), early IDE selection (Windows compat), AgentVibes auto-detection
* @related installer.js (consumes config), AgentVibes#34 (TTS integration), promptAgentVibes()
*/
const chalk = require('chalk');
const inquirer = require('inquirer');
const path = require('node:path');
@ -56,7 +36,6 @@ class UI {
// Check if there's an existing BMAD installation
const fs = require('fs-extra');
const path = require('node:path');
// Use findBmadDir to detect any custom folder names (V6+)
const bmadDir = await installer.findBmadDir(confirmedDirectory);
const hasExistingInstall = await fs.pathExists(bmadDir);
@ -131,60 +110,9 @@ class UI {
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
// For new installations, create the directory structure first so we can cache custom content
if (!hasExistingInstall && customContentConfig._shouldAsk) {
// Create the bmad directory based on core config
const path = require('node:path');
const fs = require('fs-extra');
const bmadFolderName = '_bmad';
const bmadDir = path.join(confirmedDirectory, bmadFolderName);
await fs.ensureDir(bmadDir);
await fs.ensureDir(path.join(bmadDir, '_cfg'));
await fs.ensureDir(path.join(bmadDir, '_cfg', 'custom'));
// Now prompt for custom content
customContentConfig = await this.promptCustomContentLocation();
// If custom content found, cache it
if (customContentConfig.hasCustomContent) {
const { CustomModuleCache } = require('../installers/lib/core/custom-module-cache');
const cache = new CustomModuleCache(bmadDir);
const customHandler = new CustomHandler();
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
for (const customFile of customFiles) {
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo && customInfo.id) {
// Cache the module source
await cache.cacheModule(customInfo.id, customInfo.path, {
name: customInfo.name,
type: 'custom',
});
console.log(chalk.dim(` Cached ${customInfo.name} to _cfg/custom/${customInfo.id}`));
}
}
// Update config to use cached modules
customContentConfig.cachedModules = [];
for (const customFile of customFiles) {
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo && customInfo.id) {
customContentConfig.cachedModules.push({
id: customInfo.id,
cachePath: path.join(bmadDir, '_cfg', 'custom', customInfo.id),
// Store relative path from cache for the manifest
relativePath: path.join('_cfg', 'custom', customInfo.id),
});
}
}
console.log(chalk.green(`✓ Cached ${customFiles.length} custom module(s)`));
}
// Clear the flag
// Custom content will be handled during installation phase
// Store the custom content config for later use
if (customContentConfig._shouldAsk) {
delete customContentConfig._shouldAsk;
}
@ -202,20 +130,10 @@ class UI {
// Check which custom content items were selected
const selectedCustomContent = selectedModules.filter((mod) => mod.startsWith('__CUSTOM_CONTENT__'));
// For cached modules (new installs), check if any cached modules were selected
let selectedCachedModules = [];
if (customContentConfig.cachedModules) {
selectedCachedModules = selectedModules.filter(
(mod) => !mod.startsWith('__CUSTOM_CONTENT__') && customContentConfig.cachedModules.some((cm) => cm.id === mod),
);
}
if (selectedCustomContent.length > 0 || selectedCachedModules.length > 0) {
customContentConfig.selected = true;
// Handle directory-based custom content (existing installs)
if (selectedCustomContent.length > 0) {
customContentConfig.selected = true;
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
// Convert custom content to module IDs for installation
const customContentModuleIds = [];
const customHandler = new CustomHandler();
@ -228,18 +146,10 @@ class UI {
}
// Filter out custom content markers and add module IDs
selectedModules = [...selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__')), ...customContentModuleIds];
}
// For cached modules, they're already module IDs, just mark as selected
if (selectedCachedModules.length > 0) {
customContentConfig.selectedCachedModules = selectedCachedModules;
// No need to filter since they're already proper module IDs
}
} else if (customContentConfig.hasCustomContent) {
// User provided custom content but didn't select any
customContentConfig.selected = false;
customContentConfig.selectedFiles = [];
customContentConfig.selectedCachedModules = [];
}
}
@ -610,53 +520,7 @@ class UI {
const hasCustomContentItems = false;
// Add custom content items
if (customContentConfig && customContentConfig.hasCustomContent) {
if (customContentConfig.cachedModules) {
// New installation - show cached modules
for (const cachedModule of customContentConfig.cachedModules) {
// Get the module info from cache
const yaml = require('js-yaml');
const fs = require('fs-extra');
// Try multiple possible config file locations
const possibleConfigPaths = [
path.join(cachedModule.cachePath, 'module.yaml'),
path.join(cachedModule.cachePath, 'custom.yaml'),
path.join(cachedModule.cachePath, '_module-installer', 'module.yaml'),
path.join(cachedModule.cachePath, '_module-installer', 'custom.yaml'),
];
let moduleData = null;
let foundPath = null;
for (const configPath of possibleConfigPaths) {
if (await fs.pathExists(configPath)) {
try {
const yamlContent = await fs.readFile(configPath, 'utf8');
moduleData = yaml.load(yamlContent);
foundPath = configPath;
break;
} catch (error) {
throw new Error(`Failed to parse config at ${configPath}: ${error.message}`);
}
}
}
if (moduleData) {
// Use the name from the custom info if we have it
const moduleName = cachedModule.name || moduleData.name || cachedModule.id;
customContentItems.push({
name: `${chalk.cyan('✓')} ${moduleName} ${chalk.gray('(cached)')}`,
value: cachedModule.id, // Use module ID directly
checked: true, // Default to selected
cached: true,
});
} else {
// Module config not found - skip silently (non-critical)
}
}
} else if (customContentConfig.customPath) {
if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
// Existing installation - show from directory
const customHandler = new CustomHandler();
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
@ -673,7 +537,6 @@ class UI {
}
}
}
}
// Add official modules
const { ModuleManager } = require('../installers/lib/modules/manager');
@ -804,120 +667,6 @@ class UI {
}
}
/**
* Prompt for custom content location
* @returns {Object} Custom content configuration
*/
async promptCustomContentLocation() {
try {
// Skip custom content installation - always return false
return { hasCustomContent: false };
// TODO: Custom content installation temporarily disabled
// CLIUtils.displaySection('Custom Content', 'Optional: Add custom agents, workflows, and modules');
// const { hasCustomContent } = await inquirer.prompt([
// {
// type: 'list',
// name: 'hasCustomContent',
// message: 'Do you have custom content to install?',
// choices: [
// { name: 'No (skip custom content)', value: 'none' },
// { name: 'Enter a directory path', value: 'directory' },
// { name: 'Enter a URL', value: 'url' },
// ],
// default: 'none',
// },
// ]);
// if (hasCustomContent === 'none') {
// return { hasCustomContent: false };
// }
// TODO: Custom content installation temporarily disabled
// if (hasCustomContent === 'url') {
// console.log(chalk.yellow('\nURL-based custom content installation is coming soon!'));
// console.log(chalk.cyan('For now, please download your custom content and choose "Enter a directory path".\n'));
// return { hasCustomContent: false };
// }
// if (hasCustomContent === 'directory') {
// let customPath;
// while (!customPath) {
// let expandedPath;
// const { directory } = await inquirer.prompt([
// {
// type: 'input',
// name: 'directory',
// message: 'Enter directory to search for custom content (will scan subfolders):',
// default: process.cwd(), // Use actual current working directory
// validate: async (input) => {
// if (!input || input.trim() === '') {
// return 'Please enter a directory path';
// }
// try {
// expandedPath = this.expandUserPath(input.trim());
// } catch (error) {
// return error.message;
// }
// // Check if the path exists
// const pathExists = await fs.pathExists(expandedPath);
// if (!pathExists) {
// return 'Directory does not exist';
// }
// return true;
// },
// },
// ]);
// // Now expand the path for use after the prompt
// expandedPath = this.expandUserPath(directory.trim());
// // Check if directory has custom content
// const customHandler = new CustomHandler();
// const customFiles = await customHandler.findCustomContent(expandedPath);
// if (customFiles.length === 0) {
// console.log(chalk.yellow(`\nNo custom content found in ${expandedPath}`));
// const { tryAgain } = await inquirer.prompt([
// {
// type: 'confirm',
// name: 'tryAgain',
// message: 'Try a different directory?',
// default: true,
// },
// ]);
// if (tryAgain) {
// continue;
// } else {
// return { hasCustomContent: false };
// }
// }
// customPath = expandedPath;
// console.log(chalk.green(`\n✓ Found ${customFiles.length} custom content item(s):`));
// for (const file of customFiles) {
// const relativePath = path.relative(expandedPath, path.dirname(file));
// const folderName = path.dirname(file).split(path.sep).pop();
// console.log(chalk.dim(` • ${folderName} ${chalk.gray(`(${relativePath})`)}`));
// }
// }
// return { hasCustomContent: true, customPath };
// }
// return { hasCustomContent: false };
} catch (error) {
console.error(chalk.red('Error in custom content prompt:'), error);
return { hasCustomContent: false };
}
}
/**
* Confirm directory selection
* @param {string} directory - The directory path