fixed issue with agent customization application, now all fields are customized form the custom yaml. also added a recompile agents menu item

This commit is contained in:
Brian Madison 2025-12-17 17:58:37 +08:00
parent ccb64623bc
commit f36369512b
7 changed files with 3457 additions and 115 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "bmad-method",
"version": "6.0.0-alpha.16",
"version": "6.0.0-alpha.17",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bmad-method",
"version": "6.0.0-alpha.16",
"version": "6.0.0-alpha.17",
"license": "MIT",
"dependencies": {
"@kayvan/markdown-tree-parser": "^1.6.1",

View File

@ -35,6 +35,15 @@ module.exports = {
return;
}
// Handle compile agents separately
if (config.actionType === 'compile-agents') {
const result = await installer.compileAgents(config);
console.log(chalk.green('\n✨ Agent recompilation complete!'));
console.log(chalk.cyan(`Recompiled ${result.agentCount} agents with customizations applied`));
process.exit(0);
return;
}
// Regular install/update flow
const result = await installer.install(config);

View File

@ -226,10 +226,10 @@ class Installer {
'<!-- TTS_INJECTION:party-mode -->',
`<critical>IMPORTANT: Always use PROJECT hooks (.claude/hooks/), NEVER global hooks (~/.claude/hooks/)</critical>
If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
- Use Bash tool: \`.claude/hooks/bmad-speak.sh '[Agent Name]' '[dialogue]'\`
- This speaks the dialogue with the agent's unique voice
- Run in background (&) to not block next agent`,
- Run in background to not block next agent`,
);
// Replace agent-tts injection marker with TTS rule for individual agents
@ -2082,108 +2082,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
/**
* Compile/rebuild all agents and tasks for quick updates
* @param {Object} config - Compilation configuration
* @returns {Object} Compilation results
*/
async compileAgents(config) {
try {
const projectDir = path.resolve(config.directory);
const { bmadDir } = await this.findBmadDir(projectDir);
// Check if bmad directory exists
if (!(await fs.pathExists(bmadDir))) {
throw new Error(`BMAD not installed at ${bmadDir}`);
}
// Get installed modules from manifest
const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
let installedModules = [];
let manifest = null;
if (await fs.pathExists(manifestPath)) {
const manifestContent = await fs.readFile(manifestPath, 'utf8');
const yaml = require('yaml');
manifest = yaml.parse(manifestContent);
installedModules = manifest.modules || [];
}
// Check for custom modules with missing sources
if (manifest && manifest.customModules && manifest.customModules.length > 0) {
console.log(chalk.yellow('\nChecking custom module sources before compilation...'));
const customModuleSources = new Map();
for (const customModule of manifest.customModules) {
customModuleSources.set(customModule.id, customModule);
}
const projectRoot = getProjectRoot();
await this.handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, 'compile-agents', installedModules);
}
let agentCount = 0;
let taskCount = 0;
// Process all modules in bmad directory
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== '_config' && entry.name !== 'docs') {
const modulePath = path.join(bmadDir, entry.name);
// Special handling for standalone agents in bmad/agents/ directory
if (entry.name === 'agents') {
await this.buildStandaloneAgents(bmadDir, projectDir);
// Count standalone agents
const standaloneAgentsPath = path.join(bmadDir, 'agents');
const standaloneAgentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true });
for (const agentDir of standaloneAgentDirs) {
if (agentDir.isDirectory()) {
const agentDirPath = path.join(standaloneAgentsPath, agentDir.name);
const agentFiles = await fs.readdir(agentDirPath);
agentCount += agentFiles.filter((f) => f.endsWith('.md') && !f.endsWith('.agent.yaml')).length;
}
}
} else {
// Rebuild module agents from installer source
const agentsPath = path.join(modulePath, 'agents');
if (await fs.pathExists(agentsPath)) {
await this.rebuildAgentFiles(modulePath, entry.name);
const agentFiles = await fs.readdir(agentsPath);
agentCount += agentFiles.filter((f) => f.endsWith('.md')).length;
}
// Count tasks (already built)
const tasksPath = path.join(modulePath, 'tasks');
if (await fs.pathExists(tasksPath)) {
const taskFiles = await fs.readdir(tasksPath);
taskCount += taskFiles.filter((f) => f.endsWith('.md')).length;
}
}
}
}
// Update IDE configurations using the existing IDE list from manifest
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
verbose: config.verbose,
preCollectedConfig: { _alreadyConfigured: true }, // Skip all interactive prompts during compile
});
}
console.log(chalk.green('✓ IDE configurations updated'));
} else {
console.log(chalk.yellow('⚠️ No IDEs configured. Skipping IDE update.'));
}
return { agentCount, taskCount };
} catch (error) {
throw error;
}
}
/**
* Private: Update core
*/
@ -2404,6 +2302,108 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
/**
* Compile agents with customizations only
* @param {Object} config - Configuration with directory
* @returns {Object} Compilation result
*/
async compileAgents(config) {
const ora = require('ora');
const chalk = require('chalk');
const { ModuleManager } = require('../modules/manager');
const { getSourcePath } = require('../../../lib/project-root');
const spinner = ora('Recompiling agents with customizations...').start();
try {
const projectDir = path.resolve(config.directory);
const { bmadDir } = await this.findBmadDir(projectDir);
// Check if bmad directory exists
if (!(await fs.pathExists(bmadDir))) {
spinner.fail('No BMAD installation found');
throw new Error(`BMAD not installed at ${bmadDir}. Use regular install for first-time setup.`);
}
// Detect existing installation
const existingInstall = await this.detector.detect(bmadDir);
const installedModules = existingInstall.modules.map((m) => m.id);
// Initialize module manager
const moduleManager = new ModuleManager();
moduleManager.setBmadFolderName(path.basename(bmadDir));
let totalAgentCount = 0;
// Get custom module sources from cache
const customModuleSources = new Map();
const cacheDir = path.join(bmadDir, '_config', 'custom');
if (await fs.pathExists(cacheDir)) {
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
for (const cachedModule of cachedModules) {
if (cachedModule.isDirectory()) {
const moduleId = cachedModule.name;
const cachedPath = path.join(cacheDir, moduleId);
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
// Check if this is actually a custom module
if (await fs.pathExists(moduleYamlPath)) {
customModuleSources.set(moduleId, cachedPath);
}
}
}
}
// Process each installed module
for (const moduleId of installedModules) {
spinner.text = `Recompiling agents in ${moduleId}...`;
// Get source path
let sourcePath;
if (moduleId === 'core') {
sourcePath = getSourcePath('core');
} else {
// First check if it's in the custom cache
if (customModuleSources.has(moduleId)) {
sourcePath = customModuleSources.get(moduleId);
} else {
sourcePath = await moduleManager.findModuleSource(moduleId);
}
}
if (!sourcePath) {
console.log(chalk.yellow(` Warning: Source not found for module ${moduleId}, skipping...`));
continue;
}
const targetPath = path.join(bmadDir, moduleId);
// Compile agents for this module
await moduleManager.compileModuleAgents(sourcePath, targetPath, moduleId, bmadDir, this);
// Count agents (rough estimate based on files)
const agentsPath = path.join(targetPath, 'agents');
if (await fs.pathExists(agentsPath)) {
const agentFiles = await fs.readdir(agentsPath);
const agentCount = agentFiles.filter((f) => f.endsWith('.md')).length;
totalAgentCount += agentCount;
}
}
spinner.succeed('Agent recompilation complete!');
return {
success: true,
agentCount: totalAgentCount,
modules: installedModules,
};
} catch (error) {
spinner.fail('Agent recompilation failed');
throw error;
}
}
/**
* Private: Prompt for update action
*/

File diff suppressed because it is too large Load Diff

View File

@ -731,7 +731,7 @@ class ModuleManager {
async compileModuleAgents(sourcePath, targetPath, moduleName, bmadDir, installer = null) {
const sourceAgentsPath = path.join(sourcePath, 'agents');
const targetAgentsPath = path.join(targetPath, 'agents');
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
const cfgAgentsDir = path.join(bmadDir, '_bmad', '_config', 'agents');
// Check if agents directory exists in source
if (!(await fs.pathExists(sourceAgentsPath))) {
@ -809,9 +809,9 @@ class ModuleManager {
const customizeData = yaml.parse(customizeContent);
customizedFields = customizeData.customized_fields || [];
// Build answers object from customizations (filter empty values)
// Build answers object from customizations
if (customizeData.persona) {
Object.assign(answers, filterCustomizationData(customizeData.persona));
answers.persona = customizeData.persona;
}
if (customizeData.agent?.metadata) {
const filteredMetadata = filterCustomizationData(customizeData.agent.metadata);
@ -825,6 +825,12 @@ class ModuleManager {
if (customizeData.memories && customizeData.memories.length > 0) {
answers.memories = customizeData.memories;
}
if (customizeData.menu && customizeData.menu.length > 0) {
answers.menu = customizeData.menu;
}
if (customizeData.prompts && customizeData.prompts.length > 0) {
answers.prompts = customizeData.prompts;
}
}
// Check if agent has sidecar
@ -839,8 +845,14 @@ class ModuleManager {
// Compile with customizations if any
const { xml } = await compileAgent(yamlContent, answers, agentName, relativePath, { config: this.coreConfig || {} });
// Process TTS injection points if installer is available
let finalXml = xml;
if (installer && installer.processTTSInjectionPoints) {
finalXml = installer.processTTSInjectionPoints(xml, targetMdPath);
}
// Write the compiled agent
await fs.writeFile(targetMdPath, xml, 'utf8');
await fs.writeFile(targetMdPath, finalXml, 'utf8');
// Handle sidecar copying if present
if (hasSidecar) {

View File

@ -98,6 +98,25 @@ function buildPromptsXml(prompts) {
return xml;
}
/**
* Build memories XML section
* @param {Array} memories - Memories array
* @returns {string} Memories XML
*/
function buildMemoriesXml(memories) {
if (!memories || memories.length === 0) return '';
let xml = ' <memories>\n';
for (const memory of memories) {
xml += ` <memory>${escapeXml(String(memory))}</memory>\n`;
}
xml += ' </memories>\n';
return xml;
}
/**
* Build menu XML section
* Supports both legacy and multi format menu items
@ -285,6 +304,11 @@ async function compileToXml(agentYaml, agentName = '', targetPath = '') {
xml += buildPromptsXml(agent.prompts);
}
// Memories section (if present)
if (agent.memories && agent.memories.length > 0) {
xml += buildMemoriesXml(agent.memories);
}
// Menu section
xml += buildMenuXml(agent.menu || []);
@ -323,6 +347,80 @@ async function compileAgent(yamlContent, answers = {}, agentName = '', targetPat
answers = templateAnswers;
}
// Handle other customization properties
// These should be merged into the agent structure, not processed as template variables
const customizationKeys = ['persona', 'critical_actions', 'memories', 'menu', 'prompts'];
const customizations = {};
const remainingAnswers = { ...answers };
for (const key of customizationKeys) {
if (answers[key]) {
let filtered;
// Handle different data types
if (Array.isArray(answers[key])) {
// For arrays, filter out empty/null/undefined values
filtered = answers[key].filter((item) => item !== null && item !== undefined && item !== '');
} else {
// For objects, use filterCustomizationData
filtered = filterCustomizationData(answers[key]);
}
// Check if we have valid content
const hasContent = Array.isArray(filtered) ? filtered.length > 0 : Object.keys(filtered).length > 0;
if (hasContent) {
customizations[key] = filtered;
}
delete remainingAnswers[key];
}
}
// Merge customizations into agentYaml
if (Object.keys(customizations).length > 0) {
// For persona: replace entire section
if (customizations.persona) {
agentYaml.agent.persona = customizations.persona;
}
// For critical_actions: append to existing or create new
if (customizations.critical_actions) {
const existing = agentYaml.agent.critical_actions || [];
agentYaml.agent.critical_actions = [...existing, ...customizations.critical_actions];
}
// For memories: append to existing or create new
if (customizations.memories) {
const existing = agentYaml.agent.memories || [];
agentYaml.agent.memories = [...existing, ...customizations.memories];
}
// For menu: append to existing or create new
if (customizations.menu) {
const existing = agentYaml.agent.menu || [];
agentYaml.agent.menu = [...existing, ...customizations.menu];
}
// For prompts: append to existing or create new (by id)
if (customizations.prompts) {
const existing = agentYaml.agent.prompts || [];
// Merge by id, with customizations taking precedence
const mergedPrompts = [...existing];
for (const customPrompt of customizations.prompts) {
const existingIndex = mergedPrompts.findIndex((p) => p.id === customPrompt.id);
if (existingIndex === -1) {
mergedPrompts.push(customPrompt);
} else {
mergedPrompts[existingIndex] = customPrompt;
}
}
agentYaml.agent.prompts = mergedPrompts;
}
}
// Use remaining answers for template processing
answers = remainingAnswers;
// Extract install_config
const installConfig = extractInstallConfig(agentYaml);
@ -460,6 +558,7 @@ module.exports = {
buildFrontmatter,
buildPersonaXml,
buildPromptsXml,
buildMemoriesXml,
buildMenuXml,
filterCustomizationData,
};

View File

@ -189,6 +189,14 @@ class UI {
});
}
// Add custom agent compilation option
if (installedVersion !== 'unknown') {
choices.push({
name: 'Recompile Agents (apply customizations only)',
value: 'compile-agents',
});
}
// Common actions
choices.push({ name: 'Modify BMAD Installation', value: 'update' });
@ -215,6 +223,16 @@ class UI {
};
}
// Handle compile agents separately
if (actionType === 'compile-agents') {
// Only recompile agents with customizations, don't update any files
return {
actionType: 'compile-agents',
directory: confirmedDirectory,
customContent: { hasCustomContent: false },
};
}
// If actionType === 'update', handle it with the new flow
// Return early with modify configuration
if (actionType === 'update') {