feat: implement recursive agent discovery and compilation

- Module agents now discovered recursively at any depth in agents folder
- .agent.yaml files are compiled to .md format during module installation
- Custom agents also support subdirectory structure
- Agents maintain their directory structure when installed
- YAML files are skipped during file copying as they're compiled separately
- Added compileModuleAgents method to handle YAML-to-MD compilation
- Updated discoverAgents to recursively search for .agent.yaml files
- Agents in subdirectories are properly placed in _cfg/agents with relative paths

This fixes issue where agents like cbt-coach were not being compiled
and were only copied as YAML files.
This commit is contained in:
Brian Madison 2025-12-06 15:38:38 -06:00
parent 0d83799ecf
commit 1bd01e1ce6
3 changed files with 172 additions and 28 deletions

View File

@ -2532,8 +2532,10 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
agentType = parts.slice(-2).join('-'); // Take last 2 parts as type
}
// Create target directory
const agentTargetDir = path.join(customAgentsDir, finalAgentName);
// Create target directory - use relative path if agent is in a subdirectory
const agentTargetDir = agent.relativePath
? path.join(customAgentsDir, agent.relativePath)
: path.join(customAgentsDir, finalAgentName);
await fs.ensureDir(agentTargetDir);
// Calculate paths

View File

@ -339,6 +339,9 @@ class ModuleManager {
// Copy module files with filtering
await this.copyModuleWithFiltering(sourcePath, targetPath, fileTrackingCallback, options.moduleConfig);
// Compile any .agent.yaml files to .md format
await this.compileModuleAgents(sourcePath, targetPath, moduleName, bmadDir);
// Process agent files to inject activation block
await this.processAgentFiles(targetPath, moduleName);
@ -491,6 +494,11 @@ class ModuleManager {
continue;
}
// Skip .agent.yaml files - they will be compiled separately
if (file.endsWith('.agent.yaml')) {
continue;
}
// Skip user documentation if install_user_docs is false
if (moduleConfig.install_user_docs === false && (file.startsWith('docs/') || file.startsWith('docs\\'))) {
console.log(chalk.dim(` Skipping user documentation: ${file}`));
@ -633,6 +641,91 @@ class ModuleManager {
}
}
/**
* Compile .agent.yaml files to .md format in modules
* @param {string} sourcePath - Source module path
* @param {string} targetPath - Target module path
* @param {string} moduleName - Module name
* @param {string} bmadDir - BMAD installation directory
*/
async compileModuleAgents(sourcePath, targetPath, moduleName, bmadDir) {
const sourceAgentsPath = path.join(sourcePath, 'agents');
const targetAgentsPath = path.join(targetPath, 'agents');
const cfgAgentsDir = path.join(bmadDir, '_cfg', 'agents');
// Check if agents directory exists in source
if (!(await fs.pathExists(sourceAgentsPath))) {
return; // No agents to compile
}
// Get all agent YAML files recursively
const agentFiles = await this.findAgentFiles(sourceAgentsPath);
for (const agentFile of agentFiles) {
if (!agentFile.endsWith('.agent.yaml')) continue;
const relativePath = path.relative(sourceAgentsPath, agentFile);
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
await fs.ensureDir(targetDir);
const agentName = path.basename(agentFile, '.agent.yaml');
const sourceYamlPath = agentFile;
const targetMdPath = path.join(targetDir, `${agentName}.md`);
const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
// Read and compile the YAML
try {
const yamlContent = await fs.readFile(sourceYamlPath, 'utf8');
const { compileAgent } = require('../../../lib/agent/compiler');
// Check for customizations
let customizedFields = [];
if (await fs.pathExists(customizePath)) {
const customizeContent = await fs.readFile(customizePath, 'utf8');
const customizeData = yaml.load(customizeContent);
customizedFields = customizeData.customized_fields || [];
}
// Compile with customizations if any
const { xml } = compileAgent(yamlContent, customizedFields, agentName, relativePath);
// Write the compiled MD file
await fs.writeFile(targetMdPath, xml, 'utf8');
console.log(chalk.dim(` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}`));
} catch (error) {
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
}
}
}
/**
* Find all .agent.yaml files recursively in a directory
* @param {string} dir - Directory to search
* @returns {Array} List of .agent.yaml file paths
*/
async findAgentFiles(dir) {
const agentFiles = [];
async function searchDirectory(searchDir) {
const entries = await fs.readdir(searchDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(searchDir, entry.name);
if (entry.isFile() && entry.name.endsWith('.agent.yaml')) {
agentFiles.push(fullPath);
} else if (entry.isDirectory()) {
await searchDirectory(fullPath);
}
}
}
await searchDirectory(dir);
return agentFiles;
}
/**
* Process agent files to inject activation block
* @param {string} modulePath - Path to installed module
@ -646,24 +739,49 @@ class ModuleManager {
return; // No agents to process
}
// Get all agent files
const agentFiles = await fs.readdir(agentsPath);
// Get all agent MD files recursively
const agentFiles = await this.findAgentMdFiles(agentsPath);
for (const agentFile of agentFiles) {
if (!agentFile.endsWith('.md')) continue;
const agentPath = path.join(agentsPath, agentFile);
let content = await fs.readFile(agentPath, 'utf8');
let content = await fs.readFile(agentFile, 'utf8');
// Check if content has agent XML and no activation block
if (content.includes('<agent') && !content.includes('<activation')) {
// Inject the activation block using XML handler
content = this.xmlHandler.injectActivationSimple(content);
await fs.writeFile(agentPath, content, 'utf8');
await fs.writeFile(agentFile, content, 'utf8');
}
}
}
/**
* Find all .md agent files recursively in a directory
* @param {string} dir - Directory to search
* @returns {Array} List of .md agent file paths
*/
async findAgentMdFiles(dir) {
const agentFiles = [];
async function searchDirectory(searchDir) {
const entries = await fs.readdir(searchDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(searchDir, entry.name);
if (entry.isFile() && entry.name.endsWith('.md')) {
agentFiles.push(fullPath);
} else if (entry.isDirectory()) {
await searchDirectory(fullPath);
}
}
}
await searchDirectory(dir);
return agentFiles;
}
/**
* Vendor cross-module workflows referenced in agent files
* Scans SOURCE agent.yaml files for workflow-install and copies workflows to destination

View File

@ -46,7 +46,7 @@ function resolvePath(pathStr, context) {
}
/**
* Discover available agents in the custom agent location
* Discover available agents in the custom agent location recursively
* @param {string} searchPath - Path to search for agents
* @returns {Array} List of agent info objects
*/
@ -56,35 +56,59 @@ function discoverAgents(searchPath) {
}
const agents = [];
const entries = fs.readdirSync(searchPath, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(searchPath, entry.name);
// Helper function to recursively search
function searchDirectory(dir, relativePath = '') {
const entries = fs.readdirSync(dir, { withFileTypes: true });
if (entry.isFile() && entry.name.endsWith('.agent.yaml')) {
// Simple agent (single file)
agents.push({
type: 'simple',
name: entry.name.replace('.agent.yaml', ''),
path: fullPath,
yamlFile: fullPath,
});
} else if (entry.isDirectory()) {
// Check for agent with sidecar (folder containing .agent.yaml)
const yamlFiles = fs.readdirSync(fullPath).filter((f) => f.endsWith('.agent.yaml'));
if (yamlFiles.length === 1) {
const agentYamlPath = path.join(fullPath, yamlFiles[0]);
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
const agentRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
if (entry.isFile() && entry.name.endsWith('.agent.yaml')) {
// Simple agent (single file)
// The agent name is based on the filename
const agentName = entry.name.replace('.agent.yaml', '');
agents.push({
type: 'expert',
name: entry.name,
type: 'simple',
name: agentName,
path: fullPath,
yamlFile: agentYamlPath,
hasSidecar: true,
yamlFile: fullPath,
relativePath: agentRelativePath.replace('.agent.yaml', ''),
});
} else if (entry.isDirectory()) {
// Check if this directory contains an .agent.yaml file
try {
const dirContents = fs.readdirSync(fullPath);
const yamlFiles = dirContents.filter((f) => f.endsWith('.agent.yaml'));
if (yamlFiles.length > 0) {
// Found .agent.yaml files in this directory
for (const yamlFile of yamlFiles) {
const agentYamlPath = path.join(fullPath, yamlFile);
const agentName = path.basename(yamlFile, '.agent.yaml');
agents.push({
type: 'expert',
name: agentName,
path: fullPath,
yamlFile: agentYamlPath,
hasSidecar: true,
relativePath: agentRelativePath,
});
}
} else {
// No .agent.yaml in this directory, recurse deeper
searchDirectory(fullPath, agentRelativePath);
}
} catch {
// Skip directories we can't read
}
}
}
}
searchDirectory(searchPath);
return agents;
}