mirror of
https://github.com/bmadcode/BMAD-METHOD.git
synced 2025-12-17 09:45:25 +00:00
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:
parent
0d83799ecf
commit
1bd01e1ce6
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user