sidecar content goes to custom core config location

This commit is contained in:
Brian Madison 2025-12-06 21:08:57 -06:00
parent ba2c81263b
commit 1697a45376
12 changed files with 377 additions and 30 deletions

View File

@ -375,7 +375,7 @@ exec: "../../../core/tasks/validate.xml"
- `{project-root}` - Project root directory - `{project-root}` - Project root directory
- `{bmad_folder}` - BMAD installation folder - `{bmad_folder}` - BMAD installation folder
- `{agent-folder}` - Agent installation directory (Expert agents) - `{agent_sidecar_folder}` - Agent installation directory (Expert agents)
- `{output_folder}` - Document output location - `{output_folder}` - Document output location
- `{user_name}` - User's name from config - `{user_name}` - User's name from config
- `{communication_language}` - Language preference - `{communication_language}` - Language preference

View File

@ -196,7 +196,7 @@ critical_actions:
- **Memory integration** - Past context becomes part of current session - **Memory integration** - Past context becomes part of current session
- **Protocol adherence** - Ensures consistent behavior - **Protocol adherence** - Ensures consistent behavior
### {agent-folder} Variable ### {agent_sidecar_folder} Variable
Special variable resolved during installation: Special variable resolved during installation:
@ -313,7 +313,7 @@ critical_actions:
1. **Load sidecar files in critical_actions** - Must be explicit and MANDATORY 1. **Load sidecar files in critical_actions** - Must be explicit and MANDATORY
2. **Enforce domain restrictions** - Clear boundaries prevent scope creep 2. **Enforce domain restrictions** - Clear boundaries prevent scope creep
3. **Use {agent-folder} paths** - Portable across installations 3. **Use {agent_sidecar_folder} paths** - Portable across installations
4. **Design for memory growth** - Structure sidecar files for accumulation 4. **Design for memory growth** - Structure sidecar files for accumulation
5. **Reference past naturally** - Don't dump memory, weave it into conversation 5. **Reference past naturally** - Don't dump memory, weave it into conversation
6. **Separate concerns** - Memories, instructions, knowledge in distinct files 6. **Separate concerns** - Memories, instructions, knowledge in distinct files
@ -356,8 +356,8 @@ identity: |
- [ ] Sidecar folder structure created and populated - [ ] Sidecar folder structure created and populated
- [ ] memories.md has clear section structure - [ ] memories.md has clear section structure
- [ ] instructions.md contains core directives - [ ] instructions.md contains core directives
- [ ] Menu actions reference {agent-folder} correctly - [ ] Menu actions reference {agent_sidecar_folder} correctly
- [ ] File paths use {agent-folder} variable - [ ] File paths use {agent_sidecar_folder} variable
- [ ] Install config personalizes sidecar references - [ ] Install config personalizes sidecar references
- [ ] Agent folder named consistently: `{agent-name}/` - [ ] Agent folder named consistently: `{agent-name}/`
- [ ] YAML file named: `{agent-name}.agent.yaml` - [ ] YAML file named: `{agent-name}.agent.yaml`

View File

@ -170,7 +170,7 @@ Expert agents support three types of menu actions:
- Sidecar folders go in: `{custom_module_location}/{module_name}/agents/[agent-name]-sidecar/` - Sidecar folders go in: `{custom_module_location}/{module_name}/agents/[agent-name]-sidecar/`
2. **Variable Usage**: 2. **Variable Usage**:
- `{agent-folder}` resolves to the agents folder within your module - `{agent_sidecar_folder}` resolves to the agents sidecar folder destination after installation
- `{bmad_folder}` resolves to .bmad - `{bmad_folder}` resolves to .bmad
- `{custom_module}` resolves to custom/src/modules - `{custom_module}` resolves to custom/src/modules
- `{module}` is your module code/name - `{module}` is your module code/name

View File

@ -245,12 +245,20 @@ module.exports = {
// Load agent configuration // Load agent configuration
const agentConfig = loadAgentConfig(selectedAgent.yamlFile); const agentConfig = loadAgentConfig(selectedAgent.yamlFile);
// Check if agent has sidecar
if (agentConfig.metadata.hasSidecar) {
selectedAgent.hasSidecar = true;
}
if (agentConfig.metadata.name) { if (agentConfig.metadata.name) {
console.log(chalk.dim(`Agent Name: ${agentConfig.metadata.name}`)); console.log(chalk.dim(`Agent Name: ${agentConfig.metadata.name}`));
} }
if (agentConfig.metadata.title) { if (agentConfig.metadata.title) {
console.log(chalk.dim(`Title: ${agentConfig.metadata.title}`)); console.log(chalk.dim(`Title: ${agentConfig.metadata.title}`));
} }
if (agentConfig.metadata.hasSidecar) {
console.log(chalk.dim(`Sidecar: Yes`));
}
// Get the agent type (source name) // Get the agent type (source name)
const agentType = selectedAgent.name; // e.g., "commit-poet" const agentType = selectedAgent.name; // e.g., "commit-poet"
@ -508,12 +516,22 @@ module.exports = {
const compiledPath = path.join(agentTargetDir, compiledFileName); const compiledPath = path.join(agentTargetDir, compiledFileName);
const relativePath = path.relative(projectRoot, compiledPath); const relativePath = path.relative(projectRoot, compiledPath);
// Read core config to get agent_sidecar_folder
const coreConfigPath = path.join(config.bmadFolder, 'bmb', 'config.yaml');
let coreConfig = {};
if (fs.existsSync(coreConfigPath)) {
const yamlLib = require('yaml');
const content = fs.readFileSync(coreConfigPath, 'utf8');
coreConfig = yamlLib.parse(content);
}
// Compile with proper name and path // Compile with proper name and path
const { xml, metadata, processedYaml } = compileAgent( const { xml, metadata, processedYaml } = compileAgent(
fs.readFileSync(selectedAgent.yamlFile, 'utf8'), fs.readFileSync(selectedAgent.yamlFile, 'utf8'),
answers, answers,
finalAgentName, finalAgentName,
relativePath, relativePath,
{ config: coreConfig },
); );
// Write compiled XML (.md) with custom name // Write compiled XML (.md) with custom name
@ -527,12 +545,31 @@ module.exports = {
sidecarCopied: false, sidecarCopied: false,
}; };
// Copy sidecar files for expert agents // Handle sidecar files for agents with hasSidecar flag
if (selectedAgent.hasSidecar && selectedAgent.type === 'expert') { if (selectedAgent.hasSidecar === true && selectedAgent.type === 'expert') {
const { copySidecarFiles } = require('../lib/agent/installer'); const { copyAgentSidecarFiles } = require('../lib/agent/installer');
const sidecarFiles = copySidecarFiles(selectedAgent.path, agentTargetDir, selectedAgent.yamlFile);
// Get agent sidecar folder from config or use default
const agentSidecarFolder = coreConfig?.agent_sidecar_folder || '{project-root}/.myagent-data';
// Resolve path variables
const resolvedSidecarFolder = agentSidecarFolder
.replaceAll('{project-root}', projectRoot)
.replaceAll('{bmad_folder}', config.bmadFolder);
// Create sidecar directory for this agent
const agentSidecarDir = path.join(resolvedSidecarFolder, finalAgentName);
if (!fs.existsSync(agentSidecarDir)) {
fs.mkdirSync(agentSidecarDir, { recursive: true });
}
// Find and copy sidecar folder
const sidecarFiles = copyAgentSidecarFiles(selectedAgent.path, agentSidecarDir, selectedAgent.yamlFile);
result.sidecarCopied = true; result.sidecarCopied = true;
result.sidecarFiles = sidecarFiles; result.sidecarFiles = sidecarFiles;
result.sidecarDir = agentSidecarDir;
console.log(chalk.dim(` Sidecar copied to: ${agentSidecarDir}`));
} }
console.log(chalk.green('\n✨ Agent installed successfully!')); console.log(chalk.green('\n✨ Agent installed successfully!'));

View File

@ -37,6 +37,7 @@ const { AgentPartyGenerator } = require('../../../lib/agent-party-generator');
const { CLIUtils } = require('../../../lib/cli-utils'); const { CLIUtils } = require('../../../lib/cli-utils');
const { ManifestGenerator } = require('./manifest-generator'); const { ManifestGenerator } = require('./manifest-generator');
const { IdeConfigManager } = require('./ide-config-manager'); const { IdeConfigManager } = require('./ide-config-manager');
const { replaceAgentSidecarFolders } = require('./post-install-sidecar-replacement');
class Installer { class Installer {
constructor() { constructor() {
@ -1024,6 +1025,20 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
} }
} }
// Replace {agent_sidecar_folder} placeholders in all agent files
console.log(chalk.dim('\n Configuring agent sidecar folders...'));
const sidecarResults = await replaceAgentSidecarFolders(bmadDir);
if (sidecarResults.filesReplaced > 0) {
console.log(
chalk.green(
` ✓ Updated ${sidecarResults.filesReplaced} agent file(s) with ${sidecarResults.totalReplacements} sidecar reference(s)`,
),
);
} else {
console.log(chalk.dim(' No agent sidecar references found'));
}
// Display completion message // Display completion message
const { UI } = require('../../../lib/ui'); const { UI } = require('../../../lib/ui');
const ui = new UI(); const ui = new UI();
@ -1529,18 +1544,71 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime // DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir); // const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
// 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) // Process TTS injection points (pass targetPath for tracking)
xmlContent = this.processTTSInjectionPoints(xmlContent, mdPath); xmlContent = this.processTTSInjectionPoints(xmlContent, mdPath);
// Check if agent has sidecar and copy it
let agentYamlContent = null;
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;
} catch {
// Continue without sidecar processing
}
// Write the built .md file to bmad/{module}/agents/ with POSIX-compliant final newline // Write the built .md file to bmad/{module}/agents/ with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n'; const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
await fs.writeFile(mdPath, content, 'utf8'); await fs.writeFile(mdPath, content, 'utf8');
this.installedFiles.push(mdPath); this.installedFiles.push(mdPath);
// Copy sidecar files if agent has hasSidecar flag
if (hasSidecar) {
const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
// Get agent sidecar folder from core config
const coreConfigPath = path.join(bmadDir, 'bmb', 'config.yaml');
let agentSidecarFolder = '{project-root}/.myagent-data';
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 || agentSidecarFolder;
}
// Resolve path variables
const resolvedSidecarFolder = agentSidecarFolder
.replaceAll('{project-root}', projectDir)
.replaceAll('{bmad_folder}', this.bmadFolderName || 'bmad');
// Create sidecar directory for this agent
const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
await fs.ensureDir(agentSidecarDir);
// Find and copy sidecar folder from source module
const sourceModulePath = getSourcePath(`modules/${moduleName}`);
const sourceAgentPath = path.join(sourceModulePath, 'agents');
// Copy sidecar files
const sidecarFiles = copyAgentSidecarFiles(sourceAgentPath, agentSidecarDir, yamlPath);
console.log(chalk.dim(` Copied sidecar to: ${agentSidecarDir}`));
}
// Remove the source YAML file - we can regenerate from installer source if needed // Remove the source YAML file - we can regenerate from installer source if needed
await fs.remove(yamlPath); await fs.remove(yamlPath);
console.log(chalk.dim(` Built agent: ${agentName}.md`)); console.log(chalk.dim(` Built agent: ${agentName}.md${hasSidecar ? ' (with sidecar)' : ''}`));
} }
// Handle legacy .md agents - inject activation if needed // Handle legacy .md agents - inject activation if needed
else if (agentFile.endsWith('.md')) { else if (agentFile.endsWith('.md')) {
@ -1731,6 +1799,21 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime // DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir); // 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) // Process TTS injection points (pass targetPath for tracking)
xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath); xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
@ -2532,6 +2615,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
agentConfig.defaults || {}, agentConfig.defaults || {},
finalAgentName, finalAgentName,
relativePath, relativePath,
{ config: config.coreConfig },
); );
// Write compiled agent // Write compiled agent
@ -2547,10 +2631,22 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
await fs.copy(agent.yamlFile, backupYamlPath); await fs.copy(agent.yamlFile, backupYamlPath);
} }
// Copy sidecar files if expert agent // Copy sidecar files for agents with hasSidecar flag
if (agent.hasSidecar && agent.type === 'expert') { if (agentConfig.hasSidecar === true && agent.type === 'expert') {
const { copySidecarFiles } = require('../../../lib/agent/installer'); const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
copySidecarFiles(agent.path, agentTargetDir, agent.yamlFile);
// Get agent sidecar folder from config or use default
const agentSidecarFolder = config.coreConfig?.agent_sidecar_folder || '{project-root}/.myagent-data';
// Resolve path variables
const resolvedSidecarFolder = agentSidecarFolder.replaceAll('{project-root}', projectDir).replaceAll('{bmad_folder}', bmadDir);
// Create sidecar directory for this agent
const agentSidecarDir = path.join(resolvedSidecarFolder, finalAgentName);
await fs.ensureDir(agentSidecarDir);
// Find and copy sidecar folder
const sidecarFiles = copyAgentSidecarFiles(agent.path, agentSidecarDir, agent.yamlFile);
} }
// Update manifest CSV // Update manifest CSV

View File

@ -0,0 +1,79 @@
/**
* Post-installation sidecar folder replacement utility
* Replaces {agent_sidecar_folder} placeholders in all installed agents
*/
const fs = require('fs-extra');
const path = require('node:path');
const yaml = require('yaml');
const glob = require('glob');
const chalk = require('chalk');
/**
* Replace {agent_sidecar_folder} placeholders in all agent files
* @param {string} bmadDir - Path to .bmad directory
* @returns {Object} Statistics about replacements made
*/
async function replaceAgentSidecarFolders(bmadDir) {
const results = {
filesScanned: 0,
filesReplaced: 0,
totalReplacements: 0,
errors: [],
};
try {
// Load core config to get agent_sidecar_folder value
const coreConfigPath = path.join(bmadDir, 'bmb', 'config.yaml');
if (!(await fs.pathExists(coreConfigPath))) {
throw new Error(`Core config not found at ${coreConfigPath}`);
}
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
const coreConfig = yaml.parse(coreConfigContent);
const agentSidecarFolder = coreConfig.agent_sidecar_folder || '{project-root}/.myagent-data';
// Use the literal value from config, don't resolve the placeholders
console.log(chalk.dim(`\n Replacing {agent_sidecar_folder} with: ${agentSidecarFolder}`));
// Find all agent .md files
const agentPattern = path.join(bmadDir, '**/*.md');
const agentFiles = glob.sync(agentPattern);
for (const agentFile of agentFiles) {
results.filesScanned++;
try {
let content = await fs.readFile(agentFile, 'utf8');
// Check if file contains {agent_sidecar_folder}
if (content.includes('{agent_sidecar_folder}')) {
// Replace all occurrences
const originalContent = content;
content = content.replaceAll('{agent_sidecar_folder}', agentSidecarFolder);
// Only write if content changed
if (content !== originalContent) {
await fs.writeFile(agentFile, content, 'utf8');
const replacementCount = (originalContent.match(/{agent_sidecar_folder}/g) || []).length;
results.filesReplaced++;
results.totalReplacements += replacementCount;
console.log(chalk.dim(` ✓ Replaced ${replacementCount} occurrence(s) in ${path.relative(bmadDir, agentFile)}`));
}
}
} catch (error) {
results.errors.push(`Error processing ${agentFile}: ${error.message}`);
}
}
return results;
} catch (error) {
results.errors.push(`Fatal error: ${error.message}`);
return results;
}
}
module.exports = { replaceAgentSidecarFolders };

View File

@ -28,11 +28,13 @@ class AgentCommandGenerator {
for (const agent of agents) { for (const agent of agents) {
const launcherContent = await this.generateLauncherContent(agent); const launcherContent = await this.generateLauncherContent(agent);
// Use relativePath if available (for nested agents), otherwise just name with .md
const agentPathInModule = agent.relativePath || `${agent.name}.md`;
artifacts.push({ artifacts.push({
type: 'agent-launcher', type: 'agent-launcher',
module: agent.module, module: agent.module,
name: agent.name, name: agent.name,
relativePath: path.join(agent.module, 'agents', `${agent.name}.md`), relativePath: path.join(agent.module, 'agents', agentPathInModule),
content: launcherContent, content: launcherContent,
sourcePath: agent.path, sourcePath: agent.path,
}); });
@ -56,9 +58,12 @@ class AgentCommandGenerator {
const template = await fs.readFile(this.templatePath, 'utf8'); const template = await fs.readFile(this.templatePath, 'utf8');
// Replace template variables // Replace template variables
// Use relativePath if available (for nested agents), otherwise just name with .md
const agentPathInModule = agent.relativePath || `${agent.name}.md`;
return template return template
.replaceAll('{{name}}', agent.name) .replaceAll('{{name}}', agent.name)
.replaceAll('{{module}}', agent.module) .replaceAll('{{module}}', agent.module)
.replaceAll('{{path}}', agentPathInModule)
.replaceAll('{{description}}', agent.description || `${agent.name} agent`) .replaceAll('{{description}}', agent.description || `${agent.name} agent`)
.replaceAll('{bmad_folder}', this.bmadFolderName) .replaceAll('{bmad_folder}', this.bmadFolderName)
.replaceAll('{*bmad_folder*}', '{bmad_folder}'); .replaceAll('{*bmad_folder*}', '{bmad_folder}');

View File

@ -76,7 +76,7 @@ async function getTasksFromBmad(bmadDir, selectedModules = []) {
return tasks; return tasks;
} }
async function getAgentsFromDir(dirPath, moduleName) { async function getAgentsFromDir(dirPath, moduleName, relativePath = '') {
const agents = []; const agents = [];
if (!(await fs.pathExists(dirPath))) { if (!(await fs.pathExists(dirPath))) {
@ -87,10 +87,11 @@ async function getAgentsFromDir(dirPath, moduleName) {
for (const entry of entries) { for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name); const fullPath = path.join(dirPath, entry.name);
const newRelativePath = relativePath ? `${relativePath}/${entry.name}` : entry.name;
if (entry.isDirectory()) { if (entry.isDirectory()) {
// Recurse into subdirectories // Recurse into subdirectories
const subDirAgents = await getAgentsFromDir(fullPath, moduleName); const subDirAgents = await getAgentsFromDir(fullPath, moduleName, newRelativePath);
agents.push(...subDirAgents); agents.push(...subDirAgents);
} else if (entry.name.endsWith('.md')) { } else if (entry.name.endsWith('.md')) {
// Skip README files and other non-agent files // Skip README files and other non-agent files
@ -117,6 +118,7 @@ async function getAgentsFromDir(dirPath, moduleName) {
path: fullPath, path: fullPath,
name: entry.name.replace('.md', ''), name: entry.name.replace('.md', ''),
module: moduleName, module: moduleName,
relativePath: newRelativePath, // Keep the .md extension for the full path
}); });
} }
} }

View File

@ -6,7 +6,7 @@ description: '{{description}}'
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command. You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
<agent-activation CRITICAL="TRUE"> <agent-activation CRITICAL="TRUE">
1. LOAD the FULL agent file from @{bmad_folder}/{{module}}/agents/{{name}}.md 1. LOAD the FULL agent file from @{bmad_folder}/{{module}}/agents/{{path}}
2. READ its entire contents - this contains the complete agent persona, menu, and instructions 2. READ its entire contents - this contains the complete agent persona, menu, and instructions
3. Execute ALL activation steps exactly as written in the agent file 3. Execute ALL activation steps exactly as written in the agent file
4. Follow the agent's persona and menu system precisely 4. Follow the agent's persona and menu system precisely

View File

@ -484,6 +484,16 @@ class ModuleManager {
continue; 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 // Skip _module-installer directory - it's only needed at install time
if (file.startsWith('_module-installer/')) { if (file.startsWith('_module-installer/')) {
continue; continue;
@ -697,13 +707,58 @@ class ModuleManager {
customizedFields = customizeData.customized_fields || []; customizedFields = customizeData.customized_fields || [];
} }
// Load core config to get agent_sidecar_folder
const coreConfigPath = path.join(bmadDir, 'bmb', 'config.yaml');
let coreConfig = {};
if (await fs.pathExists(coreConfigPath)) {
const yamlLib = require('yaml');
const coreConfigContent = await fs.readFile(coreConfigPath, 'utf8');
coreConfig = yamlLib.parse(coreConfigContent);
}
// Check if agent has sidecar
let hasSidecar = false;
try {
const yamlLib = require('yaml');
const agentYaml = yamlLib.parse(yamlContent);
hasSidecar = agentYaml?.agent?.metadata?.hasSidecar === true;
} catch {
// Continue without sidecar processing
}
// Compile with customizations if any // Compile with customizations if any
const { xml } = compileAgent(yamlContent, customizedFields, agentName, relativePath); const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config: coreConfig });
// Write the compiled MD file // Write the compiled MD file
await fs.writeFile(targetMdPath, xml, 'utf8'); await fs.writeFile(targetMdPath, xml, 'utf8');
console.log(chalk.dim(` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}`)); // Copy sidecar files if agent has hasSidecar flag
if (hasSidecar) {
const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
// Get agent sidecar folder from core config or use default
const agentSidecarFolder = coreConfig.agent_sidecar_folder || '{project-root}/.myagent-data';
// Resolve path variables
const projectDir = path.dirname(bmadDir);
const resolvedSidecarFolder = agentSidecarFolder
.replaceAll('{project-root}', projectDir)
.replaceAll('{bmad_folder}', path.basename(bmadDir));
// Create sidecar directory for this agent
const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
await fs.ensureDir(agentSidecarDir);
// Copy sidecar files
const sidecarFiles = copyAgentSidecarFiles(path.dirname(sourceYamlPath), agentSidecarDir, sourceYamlPath);
console.log(chalk.dim(` Copied sidecar to: ${agentSidecarDir}`));
}
console.log(
chalk.dim(` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`),
);
} catch (error) { } catch (error) {
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message)); console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
} }

View File

@ -438,9 +438,10 @@ function compileToXml(agentYaml, agentName = '', targetPath = '') {
* @param {Object} answers - Answers from install_config questions (or defaults) * @param {Object} answers - Answers from install_config questions (or defaults)
* @param {string} agentName - Optional final agent name (user's custom persona name) * @param {string} agentName - Optional final agent name (user's custom persona name)
* @param {string} targetPath - Optional target path for agent ID * @param {string} targetPath - Optional target path for agent ID
* @param {Object} options - Additional options including config
* @returns {Object} { xml: string, metadata: Object } * @returns {Object} { xml: string, metadata: Object }
*/ */
function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = '') { function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = '', options = {}) {
// Parse YAML // Parse YAML
const agentYaml = yaml.parse(yamlContent); const agentYaml = yaml.parse(yamlContent);
@ -466,14 +467,22 @@ function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = ''
finalAnswers = { ...defaults, ...answers }; finalAnswers = { ...defaults, ...answers };
} }
// Add agent_sidecar_folder to answers if provided in config
if (options.config && options.config.agent_sidecar_folder) {
finalAnswers.agent_sidecar_folder = options.config.agent_sidecar_folder;
}
// Process templates with answers // Process templates with answers
const processedYaml = processAgentYaml(agentYaml, finalAnswers); const processedYaml = processAgentYaml(agentYaml, finalAnswers);
// Strip install_config from output // Strip install_config from output
const cleanYaml = stripInstallConfig(processedYaml); const cleanYaml = stripInstallConfig(processedYaml);
// Compile to XML // Replace {agent_sidecar_folder} in XML content
const xml = compileToXml(cleanYaml, agentName, targetPath); let xml = compileToXml(cleanYaml, agentName, targetPath);
if (finalAnswers.agent_sidecar_folder) {
xml = xml.replaceAll('{agent_sidecar_folder}', finalAnswers.agent_sidecar_folder);
}
return { return {
xml, xml,

View File

@ -93,7 +93,6 @@ function discoverAgents(searchPath) {
name: agentName, name: agentName,
path: fullPath, path: fullPath,
yamlFile: agentYamlPath, yamlFile: agentYamlPath,
hasSidecar: true,
relativePath: agentRelativePath, relativePath: agentRelativePath,
}); });
} }
@ -127,12 +126,15 @@ function loadAgentConfig(yamlPath) {
// These take precedence over defaults // These take precedence over defaults
const savedAnswers = agentYaml?.saved_answers || {}; const savedAnswers = agentYaml?.saved_answers || {};
const metadata = agentYaml?.agent?.metadata || {};
return { return {
yamlContent: content, yamlContent: content,
agentYaml, agentYaml,
installConfig, installConfig,
defaults: { ...defaults, ...savedAnswers }, // saved_answers override defaults defaults: { ...defaults, ...savedAnswers }, // saved_answers override defaults
metadata: agentYaml?.agent?.metadata || {}, metadata,
hasSidecar: metadata.hasSidecar === true,
}; };
} }
@ -232,9 +234,10 @@ async function promptInstallQuestions(installConfig, defaults, presetAnswers = {
* @param {Object} agentInfo - Agent discovery info * @param {Object} agentInfo - Agent discovery info
* @param {Object} answers - User answers for install_config * @param {Object} answers - User answers for install_config
* @param {string} targetPath - Target installation directory * @param {string} targetPath - Target installation directory
* @param {Object} options - Additional options including config
* @returns {Object} Installation result * @returns {Object} Installation result
*/ */
function installAgent(agentInfo, answers, targetPath) { function installAgent(agentInfo, answers, targetPath, options = {}) {
// Compile the agent // Compile the agent
const { xml, metadata, processedYaml } = compileAgent(fs.readFileSync(agentInfo.yamlFile, 'utf8'), answers); const { xml, metadata, processedYaml } = compileAgent(fs.readFileSync(agentInfo.yamlFile, 'utf8'), answers);
@ -261,11 +264,27 @@ function installAgent(agentInfo, answers, targetPath) {
sidecarCopied: false, sidecarCopied: false,
}; };
// Copy sidecar files for expert agents // Handle sidecar files for agents with hasSidecar flag
if (agentInfo.hasSidecar && agentInfo.type === 'expert') { if (agentInfo.hasSidecar === true && agentInfo.type === 'expert') {
const sidecarFiles = copySidecarFiles(agentInfo.path, agentTargetDir, agentInfo.yamlFile); // Get agent sidecar folder from config or use default
const agentSidecarFolder = options.config?.agent_sidecar_folder || '{project-root}/.myagent-data';
// Resolve path variables
const resolvedSidecarFolder = agentSidecarFolder
.replaceAll('{project-root}', options.projectRoot || process.cwd())
.replaceAll('{bmad_folder}', options.bmadFolder || '.bmad');
// Create sidecar directory for this agent
const agentSidecarDir = path.join(resolvedSidecarFolder, agentFolderName);
if (!fs.existsSync(agentSidecarDir)) {
fs.mkdirSync(agentSidecarDir, { recursive: true });
}
// Find and copy sidecar folder
const sidecarFiles = copyAgentSidecarFiles(agentInfo.path, agentSidecarDir, agentInfo.yamlFile);
result.sidecarCopied = true; result.sidecarCopied = true;
result.sidecarFiles = sidecarFiles; result.sidecarFiles = sidecarFiles;
result.sidecarDir = agentSidecarDir;
} }
return result; return result;
@ -309,6 +328,50 @@ function copySidecarFiles(sourceDir, targetDir, excludeYaml) {
return copied; return copied;
} }
/**
* Find and copy agent sidecar folders
* @param {string} sourceDir - Source agent directory
* @param {string} targetSidecarDir - Target sidecar directory for the agent
* @param {string} excludeYaml - The .agent.yaml file to exclude
* @returns {Array} List of copied files
*/
function copyAgentSidecarFiles(sourceDir, targetSidecarDir, excludeYaml) {
const copied = [];
// Find folders with "sidecar" in the name
const entries = fs.readdirSync(sourceDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name.toLowerCase().includes('sidecar')) {
const sidecarSourcePath = path.join(sourceDir, entry.name);
// Recursively copy the sidecar folder contents
function copySidecarDir(src, dest) {
if (!fs.existsSync(dest)) {
fs.mkdirSync(dest, { recursive: true });
}
const sidecarEntries = fs.readdirSync(src, { withFileTypes: true });
for (const sidecarEntry of sidecarEntries) {
const srcPath = path.join(src, sidecarEntry.name);
const destPath = path.join(dest, sidecarEntry.name);
if (sidecarEntry.isDirectory()) {
copySidecarDir(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
copied.push(destPath);
}
}
}
copySidecarDir(sidecarSourcePath, targetSidecarDir);
}
}
return copied;
}
/** /**
* Update agent metadata ID to reflect installed location * Update agent metadata ID to reflect installed location
* @param {string} compiledContent - Compiled XML content * @param {string} compiledContent - Compiled XML content
@ -745,6 +808,7 @@ module.exports = {
promptInstallQuestions, promptInstallQuestions,
installAgent, installAgent,
copySidecarFiles, copySidecarFiles,
copyAgentSidecarFiles,
updateAgentId, updateAgentId,
detectBmadProject, detectBmadProject,
addToManifest, addToManifest,