Compare commits

...

5 Commits

Author SHA1 Message Date
Mario Semper 8e1daac736
Merge 59608d360a into 23f650ff4d 2025-12-18 01:13:24 +02:00
Brian Madison 23f650ff4d fixed _bmad folder stutter with agent custom files 2025-12-18 03:22:46 +08:00
Murat K Ozcan 59608d360a
Merge branch 'main' into feature/ring-of-fire-sessions 2025-12-12 13:41:21 -06:00
Brian 4d48b0dbe1
Merge branch 'main' into feature/ring-of-fire-sessions 2025-11-26 09:08:27 -06:00
Mario Semper 10dc25f43d feat: Ring of Fire (ROF) Sessions - Multi-agent parallel collaboration
Introduces Ring of Fire Sessions feature for BMad Method, enabling
multi-agent collaborative sessions that run in parallel to user workflow.

Key features:
- User-controlled scope (2 agents/5min to 10 agents/2hrs)
- Approval-gated tool access for safety
- Flexible reporting (brief/detailed/live)
- Parallel workflow support

Origin: tellingCube project (masemIT e.U.)
Real-world validated with successful multi-agent planning sessions.

Command: *rof "<topic>" --agents <list> [--report mode]
2025-11-23 02:22:21 +01:00
4 changed files with 288 additions and 3688 deletions

View File

@ -0,0 +1,256 @@
# BMad Method PR #1: Ring of Fire (ROF) Sessions
**Feature Type**: Core workflow enhancement
**Status**: Draft for community review
**Origin**: tellingCube project (masemIT e.U.)
**Author**: Mario Semper (@sempre)
**Date**: 2025-11-23
---
## Summary
**Ring of Fire (ROF) Sessions** enable multi-agent collaborative sessions that run in parallel to the user's main workflow, allowing users to delegate complex multi-perspective analysis while continuing other work.
---
## Problem Statement
Current BMad Method requires **sequential agent interaction**. When users need multiple agents to collaborate on a complex topic, they must:
- Manually orchestrate each agent conversation
- Stay in the loop for every exchange
- Wait for sequential responses before proceeding
- Context-switch constantly between tasks
This creates **bottlenecks** and prevents **parallel work streams**.
---
## Proposed Solution: Ring of Fire Sessions
A new command pattern that enables **scoped multi-agent collaboration sessions** that run while the user continues other work.
### Command Syntax
```bash
*rof "<topic>" --agents <agent-list> [--report brief|detailed|live]
```
### Example Usage
```bash
*rof "API Refactoring Strategy" --agents dev,architect,qa --report brief
```
**What happens**:
1. Dev, Architect, and QA agents enter a collaborative session
2. They analyze the topic together (code review, design discussion, testing concerns)
3. When agents need tool access (read files, run commands), they request user approval
4. User continues working on other tasks in parallel
5. Session ends with consolidated report (brief: just recommendations, detailed: full transcript)
---
## Key Features
### 1. User-Controlled Scope
- **Small**: 2 agents, 5-minute quick discussion
- **Large**: 10 agents, 2-hour deep analysis
- User decides granularity based on complexity
### 2. Approval-Gated Tool Access
- Agents can **discuss** freely within the session
- When agents need **tools** (read files, execute commands, make changes), they:
- Pause the session
- Request user approval
- Resume after user decision
**Why**: Maintains user control, prevents runaway agent actions
### 3. Flexible Reporting
| Mode | Description | Use Case |
|------|-------------|----------|
| `brief` | Final recommendations only | "Just tell me what to do" |
| `detailed` | Full transcript + recommendations | "Show me the reasoning" |
| `live` | Real-time updates as agents discuss | "I want to observe" |
**Default**: `brief` with Q&A available
### 4. Parallel Workflows
- User works on **Task A** while ROF session tackles **Task B**
- No context-switching overhead
- Efficient use of time
---
## Use Cases
### 1. Architecture Reviews
```bash
*rof "Evaluate microservices vs monolith for new feature" --agents architect,dev,qa
```
**Agents collaborate on**: Design trade-offs, implementation complexity, testing implications
### 2. Code Refactoring
```bash
*rof "Refactor authentication module" --agents dev,architect --report detailed
```
**Agents collaborate on**: Current code analysis, refactoring approach, migration strategy
### 3. Feature Planning
```bash
*rof "Plan user notifications feature" --agents pm,ux,dev --report brief
```
**Agents collaborate on**: Requirements, UX flow, technical feasibility, timeline
### 4. Quality Gates
```bash
*rof "Investigate test failures in CI/CD" --agents qa,dev --report live
```
**Agents collaborate on**: Root cause analysis, fix recommendations, regression prevention
### 5. Documentation Sprints
```bash
*rof "Document API endpoints" --agents dev,pm,ux
```
**Agents collaborate on**: Technical accuracy, user-friendly examples, completeness
---
## User Experience Flow
```mermaid
sequenceDiagram
User->>River: *rof "Topic" --agents dev,architect
River->>Dev: Join ROF session
River->>Architect: Join ROF session
River->>User: Session started, continue your work
Dev->>Architect: Discuss approach
Architect->>Dev: Suggest alternatives
Dev->>User: Need to read auth.ts - approve?
User->>Dev: Approved
Dev->>Architect: After reading file...
Architect->>Dev: Recommendation
Dev->>River: Session complete
River->>User: Brief report: [Recommendations]
```
---
## Implementation Considerations
### Technical Requirements
- **Session state management**: Track active ROF sessions, participating agents
- **Agent context sharing**: Agents share knowledge within session scope
- **User approval workflow**: Clear prompt for tool requests
- **Report generation**: Brief/detailed/live output formatting
- **Workflow integration**: Link ROF findings to existing workflow plans/todos
### Open Questions for Community
1. **Integration**: Core BMad feature or plugin/extension?
2. **Concurrency**: How to handle file conflicts if multiple agents want to edit?
3. **Cost Model**: Guidance for LLM call budgeting with multiple agents?
4. **Session Limits**: Recommended max agents/duration?
5. **Agent Communication**: Free-form discussion or structured turn-taking?
---
## Real-World Validation
**Origin Project**: tellingCube (BI dashboard, masemIT e.U.)
**Validation Scenario**:
- **Topic**: "Next steps for tellingCube after validation test"
- **Agents**: River (orchestrator), Mary (analyst), Winston (architect)
- **Report Mode**: Brief
- **Outcome**: Successfully analyzed post-validation roadmap with 3 scenarios (GO/CHANGE/NO-GO), delivered consolidated recommendations in 5 minutes
**User Feedback (Mario Semper)**:
> "This is exactly what I needed - I wanted multiple perspectives without having to orchestrate every conversation. The brief report gave me actionable next steps immediately."
**Documentation**: `docs/_masemIT/readme.md` in tellingCube repository
---
## Proposed Documentation Structure
```
.bmad-core/
features/
ring-of-fire.md # Feature specification
docs/
guides/
using-rof-sessions.md # User guide with examples
architecture/
agent-collaboration.md # Technical design
rof-session-management.md # State handling approach
```
---
## Benefits
**Unlocks parallel workflows** - User productivity gains
**Reduces context-switching** - Cognitive load reduction
**Enables complex analysis** - Multi-perspective insights
**Maintains user control** - Approval gates for tools
**Scales flexibly** - From quick checks to deep dives
---
## Comparison to Existing Patterns
| Feature | Standard Agent Use | ROF Session |
|---------|-------------------|-------------|
| Agent collaboration | Sequential (one at a time) | Parallel (multiple simultaneously) |
| User involvement | Required for every exchange | Only for approvals |
| Parallel work | No (user waits) | Yes (user continues tasks) |
| Output | Chat transcript | Consolidated report |
| Use case | Single-perspective tasks | Multi-perspective analysis |
---
## Next Steps
1. **Community feedback** on approach and open questions
2. **Technical design** refinement (state management, agent communication)
3. **Prototype implementation** in BMad core or as extension
4. **Beta testing** with real projects (beyond tellingCube)
5. **Documentation** completion with examples
---
## Alternatives Considered
### Alt 1: "Breakout Session"
- **Pros**: Clear meeting metaphor
- **Cons**: Less evocative, doesn't convey "continuous collaborative space"
### Alt 2: "Agent Huddle"
- **Pros**: Short, casual
- **Cons**: Implies quick/informal only
### Alt 3: "Lagerfeuer" (original German name)
- **Pros**: Warm, campfire metaphor
- **Cons**: Poor i18n, hard to pronounce/remember for non-German speakers
**Chosen**: **Ring of Fire** - evokes continuous collaboration circle, internationally understood, memorable, shortcut "ROF" works well
---
## References
- **Source Project**: tellingCube (https://github.com/masemIT/telling-cube) [if public]
- **Documentation**: `docs/_masemIT/readme.md`
- **Discussion**: [Link to BMad community discussion if applicable]
---
**Contribution ready for review.** Feedback welcome! 🔥

View File

@ -13,12 +13,13 @@ const { XmlHandler } = require('../../../lib/xml-handler');
const { DependencyResolver } = require('./dependency-resolver');
const { ConfigCollector } = require('./config-collector');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { AgentPartyGenerator } = require('../../../lib/agent-party-generator');
const { CLIUtils } = require('../../../lib/cli-utils');
const { ManifestGenerator } = require('./manifest-generator');
const { IdeConfigManager } = require('./ide-config-manager');
const { CustomHandler } = require('../custom/handler');
const { filterCustomizationData } = require('../../../lib/agent/compiler');
// BMAD installation folder name - this is constant and should never change
const BMAD_FOLDER_NAME = '_bmad';
class Installer {
constructor() {
@ -34,58 +35,35 @@ class Installer {
this.ideConfigManager = new IdeConfigManager();
this.installedFiles = new Set(); // Track all installed files
this.ttsInjectedFiles = []; // Track files with TTS injection applied
this.bmadFolderName = BMAD_FOLDER_NAME;
}
/**
* Find the bmad installation directory in a project
* V6+ installations can use ANY folder name but ALWAYS have _config/manifest.yaml
* Always uses the standard _bmad folder name
* Also checks for legacy _cfg folder for migration
* @param {string} projectDir - Project directory
* @returns {Promise<Object>} { bmadDir: string, hasLegacyCfg: boolean }
*/
async findBmadDir(projectDir) {
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
// Check if project directory exists
if (!(await fs.pathExists(projectDir))) {
// Project doesn't exist yet, return default
return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
return { bmadDir, hasLegacyCfg: false };
}
let bmadDir = null;
// Check for legacy _cfg folder if bmad directory exists
let hasLegacyCfg = false;
try {
const entries = await fs.readdir(projectDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const bmadPath = path.join(projectDir, entry.name);
// Check for current _config folder
const manifestPath = path.join(bmadPath, '_config', 'manifest.yaml');
if (await fs.pathExists(manifestPath)) {
// Found a V6+ installation with current _config folder
return { bmadDir: bmadPath, hasLegacyCfg: false };
}
// Check for legacy _cfg folder
const legacyManifestPath = path.join(bmadPath, '_cfg', 'manifest.yaml');
if (await fs.pathExists(legacyManifestPath)) {
bmadDir = bmadPath;
hasLegacyCfg = true;
}
}
if (await fs.pathExists(bmadDir)) {
const legacyCfgPath = path.join(bmadDir, '_cfg');
if (await fs.pathExists(legacyCfgPath)) {
hasLegacyCfg = true;
}
} catch {
console.log(chalk.red('Error reading project directory for BMAD installation detection'));
}
// If we found a bmad directory (with or without legacy _cfg)
if (bmadDir) {
return { bmadDir, hasLegacyCfg };
}
// No V6+ installation found, return default
// This will be used for new installations
return { bmadDir: path.join(projectDir, '_bmad'), hasLegacyCfg: false };
return { bmadDir, hasLegacyCfg };
}
/**
@ -120,7 +98,7 @@ class Installer {
*
* 3. Document marker in instructions.md (if applicable)
*/
async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) {
async copyFileWithPlaceholderReplacement(sourcePath, targetPath) {
// List of text file extensions that should have placeholder replacement
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
const ext = path.extname(sourcePath).toLowerCase();
@ -285,7 +263,7 @@ class Installer {
// Check for already configured IDEs
const { Detector } = require('./detector');
const detector = new Detector();
const bmadDir = path.join(projectDir, this.bmadFolderName || 'bmad');
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
// During full reinstall, use the saved previous IDEs since bmad dir was deleted
// Otherwise detect from existing installation
@ -532,18 +510,14 @@ class Installer {
}
}
// Always use _bmad as the folder name
const bmadFolderName = '_bmad';
this.bmadFolderName = bmadFolderName; // Store for use in other methods
// Store AgentVibes configuration for injection point processing
this.enableAgentVibes = config.enableAgentVibes || false;
// Set bmad folder name on module manager and IDE manager for placeholder replacement
this.moduleManager.setBmadFolderName(bmadFolderName);
this.moduleManager.setBmadFolderName(BMAD_FOLDER_NAME);
this.moduleManager.setCoreConfig(moduleConfigs.core || {});
this.moduleManager.setCustomModulePaths(customModulePaths);
this.ideManager.setBmadFolderName(bmadFolderName);
this.ideManager.setBmadFolderName(BMAD_FOLDER_NAME);
// Tool selection will be collected after we determine if it's a reinstall/update/new install
@ -553,14 +527,8 @@ class Installer {
// Resolve target directory (path.resolve handles platform differences)
const projectDir = path.resolve(config.directory);
let existingBmadDir = null;
let existingBmadFolderName = null;
if (await fs.pathExists(projectDir)) {
const result = await this.findBmadDir(projectDir);
existingBmadDir = result.bmadDir;
existingBmadFolderName = path.basename(existingBmadDir);
}
// Always use the standard _bmad folder name
const bmadDir = path.join(projectDir, BMAD_FOLDER_NAME);
// Create a project directory if it doesn't exist (user already confirmed)
if (!(await fs.pathExists(projectDir))) {
@ -582,8 +550,6 @@ class Installer {
}
}
const bmadDir = path.join(projectDir, bmadFolderName);
// Check existing installation
spinner.text = 'Checking for existing installation...';
const existingInstall = await this.detector.detect(bmadDir);
@ -1606,7 +1572,7 @@ class Installer {
const targetPath = path.join(agentsDir, fileName);
if (await fs.pathExists(sourcePath)) {
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
this.installedFiles.add(targetPath);
}
}
@ -1622,7 +1588,7 @@ class Installer {
const targetPath = path.join(tasksDir, fileName);
if (await fs.pathExists(sourcePath)) {
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
this.installedFiles.add(targetPath);
}
}
@ -1638,7 +1604,7 @@ class Installer {
const targetPath = path.join(toolsDir, fileName);
if (await fs.pathExists(sourcePath)) {
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
this.installedFiles.add(targetPath);
}
}
@ -1654,7 +1620,7 @@ class Installer {
const targetPath = path.join(templatesDir, fileName);
if (await fs.pathExists(sourcePath)) {
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath);
this.installedFiles.add(targetPath);
}
}
@ -1669,7 +1635,7 @@ class Installer {
await fs.ensureDir(path.dirname(targetPath));
if (await fs.pathExists(dataPath)) {
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad');
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath);
this.installedFiles.add(targetPath);
}
}
@ -1759,14 +1725,9 @@ class Installer {
}
}
// 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');
}
// Copy the file with placeholder replacement
await fs.ensureDir(path.dirname(targetFile));
await this.copyFileWithPlaceholderReplacement(sourceFile, targetFile);
// Track the installed file
this.installedFiles.add(targetFile);
@ -1844,7 +1805,7 @@ class Installer {
if (!(await fs.pathExists(customizePath))) {
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
if (await fs.pathExists(genericTemplatePath)) {
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath, this.bmadFolderName || 'bmad');
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath);
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
}
@ -1853,235 +1814,6 @@ class Installer {
}
}
/**
* Build standalone agents in bmad/agents/ directory
* @param {string} bmadDir - Path to bmad directory
* @param {string} projectDir - Path to project directory
*/
async buildStandaloneAgents(bmadDir, projectDir) {
const standaloneAgentsPath = path.join(bmadDir, 'agents');
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
// Check if standalone agents directory exists
if (!(await fs.pathExists(standaloneAgentsPath))) {
return;
}
// Get all subdirectories in agents/
const agentDirs = await fs.readdir(standaloneAgentsPath, { withFileTypes: true });
for (const agentDir of agentDirs) {
if (!agentDir.isDirectory()) continue;
const agentDirPath = path.join(standaloneAgentsPath, agentDir.name);
// Find any .agent.yaml file in the directory
const files = await fs.readdir(agentDirPath);
const yamlFile = files.find((f) => f.endsWith('.agent.yaml'));
if (!yamlFile) continue;
const agentName = path.basename(yamlFile, '.agent.yaml');
const sourceYamlPath = path.join(agentDirPath, yamlFile);
const targetMdPath = path.join(agentDirPath, `${agentName}.md`);
const customizePath = path.join(cfgAgentsDir, `${agentName}.customize.yaml`);
// Check for customizations
const customizeExists = await fs.pathExists(customizePath);
let customizedFields = [];
if (customizeExists) {
const customizeContent = await fs.readFile(customizePath, 'utf8');
const yaml = require('yaml');
const customizeYaml = yaml.parse(customizeContent);
// Detect what fields are customized (similar to rebuildAgentFiles)
if (customizeYaml) {
if (customizeYaml.persona) {
for (const [key, value] of Object.entries(customizeYaml.persona)) {
if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
customizedFields.push(`persona.${key}`);
}
}
}
if (customizeYaml.agent?.metadata) {
for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
if (value !== '' && value !== null) {
customizedFields.push(`metadata.${key}`);
}
}
}
if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
customizedFields.push('critical_actions');
}
if (customizeYaml.menu && customizeYaml.menu.length > 0) {
customizedFields.push('menu');
}
}
}
// Build YAML to XML .md
let xmlContent = await this.xmlHandler.buildFromYaml(sourceYamlPath, customizeExists ? customizePath : null, {
includeMetadata: true,
});
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
// Process TTS injection points (pass targetPath for tracking)
xmlContent = this.processTTSInjectionPoints(xmlContent, targetMdPath);
// Write the built .md file with POSIX-compliant final newline
const content = xmlContent.endsWith('\n') ? xmlContent : xmlContent + '\n';
await fs.writeFile(targetMdPath, content, 'utf8');
// Display result
if (customizedFields.length > 0) {
console.log(chalk.dim(` Built standalone agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
} else {
console.log(chalk.dim(` Built standalone agent: ${agentName}.md`));
}
}
}
/**
* Rebuild agent files from installer source (for compile command)
* @param {string} modulePath - Path to module in bmad/ installation
* @param {string} moduleName - Module name
*/
async rebuildAgentFiles(modulePath, moduleName) {
// Get source agents directory from installer
const sourceAgentsPath =
moduleName === 'core' ? path.join(getModulePath('core'), 'agents') : path.join(getSourcePath(`modules/${moduleName}`), 'agents');
if (!(await fs.pathExists(sourceAgentsPath))) {
return; // No source agents to rebuild
}
// Determine project directory (parent of bmad/ directory)
const bmadDir = path.dirname(modulePath);
const projectDir = path.dirname(bmadDir);
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
const targetAgentsPath = path.join(modulePath, 'agents');
// Ensure target directory exists
await fs.ensureDir(targetAgentsPath);
// Get all YAML agent files from source
const sourceFiles = await fs.readdir(sourceAgentsPath);
for (const file of sourceFiles) {
if (file.endsWith('.agent.yaml')) {
const agentName = file.replace('.agent.yaml', '');
const sourceYamlPath = path.join(sourceAgentsPath, file);
const targetMdPath = path.join(targetAgentsPath, `${agentName}.md`);
const customizePath = path.join(cfgAgentsDir, `${moduleName}-${agentName}.customize.yaml`);
// Check for customizations
const customizeExists = await fs.pathExists(customizePath);
let customizedFields = [];
if (customizeExists) {
const customizeContent = await fs.readFile(customizePath, 'utf8');
const yaml = require('yaml');
const customizeYaml = yaml.parse(customizeContent);
// Detect what fields are customized
if (customizeYaml) {
if (customizeYaml.persona) {
for (const [key, value] of Object.entries(customizeYaml.persona)) {
if (value !== '' && value !== null && !(Array.isArray(value) && value.length === 0)) {
customizedFields.push(`persona.${key}`);
}
}
}
if (customizeYaml.agent?.metadata) {
for (const [key, value] of Object.entries(customizeYaml.agent.metadata)) {
if (value !== '' && value !== null) {
customizedFields.push(`metadata.${key}`);
}
}
}
if (customizeYaml.critical_actions && customizeYaml.critical_actions.length > 0) {
customizedFields.push('critical_actions');
}
if (customizeYaml.memories && customizeYaml.memories.length > 0) {
customizedFields.push('memories');
}
if (customizeYaml.menu && customizeYaml.menu.length > 0) {
customizedFields.push('menu');
}
if (customizeYaml.prompts && customizeYaml.prompts.length > 0) {
customizedFields.push('prompts');
}
}
}
// 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 (filter empty values)
const answers = {};
if (customizeData.persona) {
Object.assign(answers, filterCustomizationData(customizeData.persona));
}
if (customizeData.agent?.metadata) {
const filteredMetadata = filterCustomizationData(customizeData.agent.metadata);
if (Object.keys(filteredMetadata).length > 0) {
Object.assign(answers, { metadata: filteredMetadata });
}
}
if (customizeData.critical_actions && customizeData.critical_actions.length > 0) {
answers.critical_actions = customizeData.critical_actions;
}
if (customizeData.memories && customizeData.memories.length > 0) {
answers.memories = customizeData.memories;
}
const coreConfigPath = path.join(bmadDir, 'core', '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 result = await compileAgent(yamlContent, answers, agentName, path.relative(bmadDir, targetMdPath), {
config: coreConfig,
});
// Check if compilation succeeded
if (!result || !result.xml) {
throw new Error(`Failed to compile agent ${agentName}: No XML returned from compiler`);
}
// Replace _bmad with actual folder name if needed
const finalXml = result.xml.replaceAll('_bmad', path.basename(bmadDir));
// Write the rebuilt .md file with POSIX-compliant final newline
const content = finalXml.endsWith('\n') ? finalXml : finalXml + '\n';
await fs.writeFile(targetMdPath, content, 'utf8');
// Display result with customizations if any
if (customizedFields.length > 0) {
console.log(chalk.dim(` Rebuilt agent: ${agentName}.md `) + chalk.yellow(`(customized: ${customizedFields.join(', ')})`));
} else {
console.log(chalk.dim(` Rebuilt agent: ${agentName}.md`));
}
}
}
}
/**
* Private: Update core
*/
@ -2677,190 +2409,6 @@ class Installer {
return { customFiles, modifiedFiles };
}
/**
* Private: Create agent configuration files
* @param {string} bmadDir - BMAD installation directory
* @param {Object} userInfo - User information including name and language
*/
async createAgentConfigs(bmadDir, userInfo = null) {
const agentConfigDir = path.join(bmadDir, '_config', 'agents');
await fs.ensureDir(agentConfigDir);
// Get all agents from all modules
const agents = [];
const agentDetails = []; // For manifest generation
// Check modules for agents (including core)
const entries = await fs.readdir(bmadDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name !== '_config') {
const moduleAgentsPath = path.join(bmadDir, entry.name, 'agents');
if (await fs.pathExists(moduleAgentsPath)) {
const agentFiles = await fs.readdir(moduleAgentsPath);
for (const agentFile of agentFiles) {
if (agentFile.endsWith('.md')) {
const agentPath = path.join(moduleAgentsPath, agentFile);
const agentContent = await fs.readFile(agentPath, 'utf8');
// Skip agents with localskip="true"
const hasLocalSkip = agentContent.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
if (hasLocalSkip) {
continue; // Skip this agent - it should not have been installed
}
const agentName = path.basename(agentFile, '.md');
// Extract any nodes with agentConfig="true"
const agentConfigNodes = this.extractAgentConfigNodes(agentContent);
agents.push({
name: agentName,
module: entry.name,
agentConfigNodes: agentConfigNodes,
});
// Use shared AgentPartyGenerator to extract details
let details = AgentPartyGenerator.extractAgentDetails(agentContent, entry.name, agentName);
// Apply config overrides if they exist
if (details) {
const configPath = path.join(agentConfigDir, `${entry.name}-${agentName}.md`);
if (await fs.pathExists(configPath)) {
const configContent = await fs.readFile(configPath, 'utf8');
details = AgentPartyGenerator.applyConfigOverrides(details, configContent);
}
agentDetails.push(details);
}
}
}
}
}
}
// Create config file for each agent
let createdCount = 0;
let skippedCount = 0;
// Load agent config template
const templatePath = getSourcePath('utility', 'models', 'agent-config-template.md');
const templateContent = await fs.readFile(templatePath, 'utf8');
for (const agent of agents) {
const configPath = path.join(agentConfigDir, `${agent.module}-${agent.name}.md`);
// Skip if config file already exists (preserve custom configurations)
if (await fs.pathExists(configPath)) {
skippedCount++;
continue;
}
// Build config content header
let configContent = `# Agent Config: ${agent.name}\n\n`;
// Process template and add agent-specific config nodes
let processedTemplate = templateContent;
// Replace {core:user_name} placeholder with actual user name if available
if (userInfo && userInfo.userName) {
processedTemplate = processedTemplate.replaceAll('{core:user_name}', userInfo.userName);
}
// Replace {core:communication_language} placeholder with actual language if available
if (userInfo && userInfo.responseLanguage) {
processedTemplate = processedTemplate.replaceAll('{core:communication_language}', userInfo.responseLanguage);
}
// If this agent has agentConfig nodes, add them after the existing comment
if (agent.agentConfigNodes && agent.agentConfigNodes.length > 0) {
// Find the agent-specific configuration nodes comment
const commentPattern = /(\s*<!-- Agent-specific configuration nodes -->)/;
const commentMatch = processedTemplate.match(commentPattern);
if (commentMatch) {
// Add nodes right after the comment
let agentSpecificNodes = '';
for (const node of agent.agentConfigNodes) {
agentSpecificNodes += `\n ${node}`;
}
processedTemplate = processedTemplate.replace(commentPattern, `$1${agentSpecificNodes}`);
}
}
configContent += processedTemplate;
// Ensure POSIX-compliant final newline
if (!configContent.endsWith('\n')) {
configContent += '\n';
}
await fs.writeFile(configPath, configContent, 'utf8');
this.installedFiles.add(configPath); // Track agent config files
createdCount++;
}
// Generate agent manifest with overrides applied
await this.generateAgentManifest(bmadDir, agentDetails);
return { total: agents.length, created: createdCount, skipped: skippedCount };
}
/**
* Generate agent manifest XML file
* @param {string} bmadDir - BMAD installation directory
* @param {Array} agentDetails - Array of agent details
*/
async generateAgentManifest(bmadDir, agentDetails) {
const manifestPath = path.join(bmadDir, '_config', 'agent-manifest.csv');
await AgentPartyGenerator.writeAgentParty(manifestPath, agentDetails, { forWeb: false });
}
/**
* Extract nodes with agentConfig="true" from agent content
* @param {string} content - Agent file content
* @returns {Array} Array of XML nodes that should be added to agent config
*/
extractAgentConfigNodes(content) {
const nodes = [];
try {
// Find all XML nodes with agentConfig="true"
// Match self-closing tags and tags with content
const selfClosingPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*\/>/g;
const withContentPattern = /<([a-zA-Z][a-zA-Z0-9_-]*)\s+[^>]*agentConfig="true"[^>]*>([\s\S]*?)<\/\1>/g;
// Extract self-closing tags
let match;
while ((match = selfClosingPattern.exec(content)) !== null) {
// Extract just the tag without children (structure only)
const tagMatch = match[0].match(/<([a-zA-Z][a-zA-Z0-9_-]*)([^>]*)\/>/);
if (tagMatch) {
const tagName = tagMatch[1];
const attributes = tagMatch[2].replace(/\s*agentConfig="true"/, ''); // Remove agentConfig attribute
nodes.push(`<${tagName}${attributes}></${tagName}>`);
}
}
// Extract tags with content
while ((match = withContentPattern.exec(content)) !== null) {
const fullMatch = match[0];
const tagName = match[1];
// Extract opening tag with attributes (removing agentConfig="true")
const openingTagMatch = fullMatch.match(new RegExp(`<${tagName}([^>]*)>`));
if (openingTagMatch) {
const attributes = openingTagMatch[1].replace(/\s*agentConfig="true"/, '');
// Add empty node structure (no children)
nodes.push(`<${tagName}${attributes}></${tagName}>`);
}
}
} catch (error) {
console.error('Error extracting agentConfig nodes:', error);
}
return nodes;
}
/**
* Handle missing custom module sources interactively
* @param {Map} customModuleSources - Map of custom module ID to info
@ -2999,7 +2547,7 @@ class Installer {
await this.manifest.addCustomModule(bmadDir, missing.info);
validCustomModules.push({
id: moduleId,
id: missing.id,
name: missing.name,
path: resolvedPath,
info: missing.info,
@ -3013,7 +2561,7 @@ class Installer {
case 'remove': {
// Extra confirmation for destructive remove
console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
console.log(chalk.red(` Module location: ${path.join(bmadDir, moduleId)}`));
console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`));
const { confirm } = await inquirer.prompt([
{

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, '_bmad', '_config', 'agents');
const cfgAgentsDir = path.join(bmadDir, '_config', 'agents');
// Check if agents directory exists in source
if (!(await fs.pathExists(sourceAgentsPath))) {