Compare commits

...

18 Commits

Author SHA1 Message Date
Mario Semper ef12730dfe
Merge 59608d360a into 901b39de9a 2025-12-15 15:53:44 +00:00
Brian Madison 901b39de9a fixed duplicate entry in files manfest issue 2025-12-15 20:47:21 +08:00
Brian Madison 4d8d1f84f7 quick update works and retains custom content also 2025-12-15 19:54:40 +08:00
Brian Madison 48795d46de core and custom modules all install through the same flow now 2025-12-15 19:16:03 +08:00
Brian Madison bbda7171bd quick update output modified 2025-12-15 17:30:12 +08:00
Brian Madison 08f05cf9a4 update menu updated 2025-12-15 16:25:01 +08:00
Brian Madison c7827bf031 less verbose final output during install 2025-12-15 15:55:28 +08:00
Brian Madison 5716282898 roo installer had some bugs 2025-12-15 15:08:19 +08:00
Brian Madison 60238d2854 default accepted for installer quesitons 2025-12-15 12:55:57 +08:00
Brian Madison 6513c77d1b single install panel, no clearing disjointed between modules 2025-12-15 11:54:37 +08:00
Brian Madison 3cbe330b8e improved ui for initial install question intake 2025-12-15 11:33:01 +08:00
Brian Madison ecc2901649 remove header display before tool selection 2025-12-15 11:05:27 +08:00
Brian Madison d4eccf07cf reorganize order of questions to make more logical sense 2025-12-15 10:59:15 +08:00
Brian Madison 1da7705821 folder workflow naming alignment for consistency 2025-12-15 10:17:58 +08:00
Brian Madison 7f742d4af6 custom modules install after any non custom modules selected and after the core, manifest tracks custom modules separately to ensure always installed from the custom cache 2025-12-15 09:14:16 +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
82 changed files with 1343 additions and 758 deletions

View File

@ -73,7 +73,7 @@
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
"editor.defaultFormatter": "vscode.typescript-language-features"
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features"

View File

@ -21,6 +21,6 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
TRADEMARK NOTICE:
BMAD™, BMAD-CORE™ and BMAD-METHOD™ are trademarks of BMad Code, LLC. The use of these
BMad™ , BMAD-CORE™ and BMAD-METHOD™ are trademarks of BMad Code, LLC. The use of these
trademarks in this software does not grant any rights to use the trademarks
for any other purpose.

View File

@ -202,7 +202,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for full development guidelines.
MIT License - See [LICENSE](LICENSE) for details.
**Trademarks:** BMAD™ and BMAD-METHOD™ are trademarks of BMad Code, LLC.
**Trademarks:** BMad™ and BMAD-METHOD™ are trademarks of BMad Code, LLC.
---

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

@ -12,16 +12,14 @@ agent:
role: "Master Task Executor + BMad Expert + Guiding Facilitator Orchestrator"
identity: "Master-level expert in the BMAD Core Platform and all loaded modules with comprehensive knowledge of all resources, tasks, and workflows. Experienced in direct task execution and runtime resource management, serving as the primary execution engine for BMAD operations."
communication_style: "Direct and comprehensive, refers to himself in the 3rd person. Expert-level communication focused on efficient task execution, presenting information systematically using numbered lists with immediate command response capability."
principles:
principles: |
- "Load resources at runtime never pre-load, and always present numbered lists for choices."
# Agent-specific critical actions
critical_actions:
- "Load into memory {project-root}/_bmad/core/config.yaml and set variable project_name, output_folder, user_name, communication_language"
- "Remember the users name is {user_name}"
- "ALWAYS communicate in {communication_language}"
# Agent menu items
menu:
- trigger: "list-tasks"
action: "list all tasks from {project-root}/_bmad/_config/task-manifest.csv"
@ -34,5 +32,3 @@ agent:
- trigger: "party-mode"
exec: "{project-root}/_bmad/core/workflows/party-mode/workflow.md"
description: "Group chat with all agents"
prompts: []

View File

@ -1,8 +1,8 @@
code: core
name: "BMAD™ Core Module"
name: "BMad™ Core Module"
header: "BMAD™ Core Configuration"
subheader: "Configure the core settings for your BMAD™ installation.\nThese settings will be used across all modules and agents."
header: "BMad™ Core Configuration"
subheader: "Configure the core settings for your BMad™ installation.\nThese settings will be used across all modules and agents."
user_name:
prompt: "What shall the agents call you (TIP: Use a team name if using with a group)?"
@ -16,7 +16,7 @@ communication_language:
document_output_language:
prompt: "Preferred document output language?"
default: "{communication_language}"
default: "English"
result: "{value}"
output_folder:

View File

@ -71,7 +71,7 @@ Provides the **HOW** (universal knowledge) while agents provide the **WHAT** (do
### Example: Frame Expert (Technical Diagrams)
```yaml
# workflows/diagrams/create-flowchart/workflow.yaml
# workflows/excalidraw-diagrams/create-flowchart/workflow.yaml
helpers: '{project-root}/_bmad/core/resources/excalidraw/excalidraw-helpers.md'
json_validation: '{project-root}/_bmad/core/resources/excalidraw/validate-json-instructions.md'
```
@ -79,7 +79,7 @@ json_validation: '{project-root}/_bmad/core/resources/excalidraw/validate-json-i
**Domain-specific additions:**
```yaml
# workflows/diagrams/_shared/flowchart-templates.yaml
# workflows/excalidraw-diagrams/_shared/flowchart-templates.yaml
flowchart:
start_node:
type: ellipse

View File

@ -1,5 +1,5 @@
---
name: brainstorming-session
name: brainstorming
description: Facilitate interactive brainstorming sessions using diverse creative techniques and ideation methods
context_file: '' # Optional context file path for project-specific guidance
---

View File

@ -1,76 +0,0 @@
const fs = require('fs-extra');
const path = require('node:path');
const chalk = require('chalk');
/**
* BMB Module Installer
* Sets up custom agent and workflow locations for the BMad Builder module
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from module.yaml
* @param {Object} options.coreConfig - Core configuration containing user_name
* @param {Array<string>} options.installedIDEs - Array of IDE codes that were installed
* @param {Object} options.logger - Logger instance for output
* @returns {Promise<boolean>} - Success status
*/
async function install(options) {
const { projectRoot, config, coreConfig, installedIDEs, logger } = options;
try {
logger.log(chalk.blue('🔧 Setting up BMB Module...'));
// Generate custom.yaml in bmb_creations_output_folder
if (config['bmb_creations_output_folder']) {
// The config value contains {project-root} which needs to be resolved
const rawLocation = config['bmb_creations_output_folder'];
const customLocation = rawLocation.replace('{project-root}', projectRoot);
const customDestPath = path.join(customLocation, 'custom.yaml');
logger.log(chalk.cyan(` Setting up custom agents at: ${customLocation}`));
// Ensure the directory exists
await fs.ensureDir(customLocation);
// Generate the custom.yaml content
const userName = (coreConfig && coreConfig.user_name) || 'my';
const customContent = `code: my-custom-bmad
name: "${userName}-Custom-BMad: Sample Stand Alone Custom Agents and Workflows"
default_selected: true
`;
// Write the custom.yaml file (only if it doesn't exist to preserve user changes)
if (await fs.pathExists(customDestPath)) {
logger.log(chalk.yellow(` ✓ custom.yaml already exists at ${customDestPath}`));
} else {
await fs.writeFile(customDestPath, customContent, 'utf8');
logger.log(chalk.green(` ✓ Created custom.yaml at ${customDestPath}`));
}
}
// Set up custom module location if configured
if (config['bmb_creations_output_folder']) {
const rawModuleLocation = config['bmb_creations_output_folder'];
const moduleLocation = rawModuleLocation.replace('{project-root}', projectRoot);
logger.log(chalk.cyan(` Setting up custom modules at: ${moduleLocation}`));
// Ensure the directory exists
await fs.ensureDir(moduleLocation);
logger.log(chalk.green(` ✓ Created modules directory at ${moduleLocation}`));
}
// Handle IDE-specific configurations if needed
if (installedIDEs && installedIDEs.length > 0) {
logger.log(chalk.cyan(` Configuring BMB for IDEs: ${installedIDEs.join(', ')}`));
}
logger.log(chalk.green('✓ BMB Module setup complete'));
return true;
} catch (error) {
logger.error(chalk.red(`Error setting up BMB module: ${error.message}`));
return false;
}
}
module.exports = { install };

View File

@ -1,7 +1,7 @@
code: bmb
name: "BMB: BMad Builder - Agent, Workflow and Module Builder"
header: "BMad Optimized Builder (BoMB) Module Configuration"
subheader: "Configure the settings for the BoMB Factory!\nThe agent, workflow and module builder for BMAD™"
subheader: "Configure the settings for the BoMB Factory!\nThe agent, workflow and module builder for BMad™ "
default_selected: false # This module will not be selected by default for new installations
# Variables from Core Config inserted:

View File

@ -32,7 +32,7 @@ agent:
description: Guided Research scoped to market, domain, competitive analysis, or technical research (optional)
- trigger: product-brief
exec: "{project-root}/_bmad/bmm/workflows/1-analysis/product-brief/workflow.md"
exec: "{project-root}/_bmad/bmm/workflows/1-analysis/create-product-brief/workflow.md"
description: Create a Product Brief (recommended input for PRD)
- trigger: document-project

View File

@ -23,19 +23,19 @@ agent:
description: Get workflow status or initialize a workflow if not already done (optional)
- trigger: create-architecture
exec: "{project-root}/_bmad/bmm/workflows/3-solutioning/architecture/workflow.md"
exec: "{project-root}/_bmad/bmm/workflows/3-solutioning/create-architecture/workflow.md"
description: Create an Architecture Document to Guide Development of a PRD (required for BMad Method projects)
- trigger: implementation-readiness
exec: "{project-root}/_bmad/bmm/workflows/3-solutioning/implementation-readiness/workflow.md"
exec: "{project-root}/_bmad/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md"
description: Validate PRD, UX, Architecture, Epics and stories aligned (Optional but recommended before development)
- trigger: create-excalidraw-diagram
workflow: "{project-root}/_bmad/bmm/workflows/diagrams/create-diagram/workflow.yaml"
workflow: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml"
description: Create system architecture or technical diagram (Excalidraw) (Use any time you need a diagram)
- trigger: create-excalidraw-dataflow
workflow: "{project-root}/_bmad/bmm/workflows/diagrams/create-dataflow/workflow.yaml"
workflow: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml"
description: Create data flow diagram (Excalidraw) (Use any time you need a diagram)
- trigger: party-mode

View File

@ -32,7 +32,7 @@ agent:
description: Create Epics and User Stories from PRD (Required for BMad Method flow AFTER the Architecture is completed)
- trigger: implementation-readiness
exec: "{project-root}/_bmad/bmm/workflows/3-solutioning/implementation-readiness/workflow.md"
exec: "{project-root}/_bmad/bmm/workflows/3-solutioning/check-implementation-readiness/workflow.md"
description: Validate PRD, UX, Architecture, Epics and stories aligned (Optional but recommended before development)
- trigger: correct-course

View File

@ -30,15 +30,15 @@ agent:
description: Generate Mermaid diagrams (architecture, sequence, flow, ER, class, state)
- trigger: create-excalidraw-flowchart
workflow: "{project-root}/_bmad/bmm/workflows/diagrams/create-flowchart/workflow.yaml"
workflow: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-flowchart/workflow.yaml"
description: Create Excalidraw flowchart for processes and logic flows
- trigger: create-excalidraw-diagram
workflow: "{project-root}/_bmad/bmm/workflows/diagrams/create-diagram/workflow.yaml"
workflow: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-diagram/workflow.yaml"
description: Create Excalidraw system architecture or technical diagram
- trigger: create-excalidraw-dataflow
workflow: "{project-root}/_bmad/bmm/workflows/diagrams/create-dataflow/workflow.yaml"
workflow: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-dataflow/workflow.yaml"
description: Create Excalidraw data flow diagram
- trigger: validate-doc

View File

@ -32,7 +32,7 @@ agent:
description: Validate UX Specification and Design Artifacts
- trigger: create-excalidraw-wireframe
workflow: "{project-root}/_bmad/bmm/workflows/diagrams/create-wireframe/workflow.yaml"
workflow: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-wireframe/workflow.yaml"
description: Create website or app wireframe (Excalidraw)
- trigger: party-mode

View File

@ -35,23 +35,22 @@ planning_artifacts: # Phase 1-3 artifacts
default: "{output_folder}/project-planning-artifacts"
result: "{project-root}/{value}"
implementation_artifacts: # Phase 4 artifacts
prompt: "Where should implementation artifacts be stored?\n - Such as: (sprint status, individual story files and reviews, retrospectives, Quick Flow output)"
implementation_artifacts: # Phase 4 artifacts and quick-dev flow output
prompt: "Where should implementation artifacts be stored?\n(sprint status, individual story files and reviews, retrospectives, Quick Flow output)"
default: "{output_folder}/implementation-artifacts"
result: "{project-root}/{value}"
project_knowledge: # Artifacts from research, document-project output, other long lived accurate kn
prompt: "Where should non-ephemeral project knowledge be stored (docs, research, references)?"
project_knowledge: # Artifacts from research, document-project output, other long lived accurate knowledge
prompt: "Where should non-ephemeral project knowledge be searched for and stored\n(docs, research, references)?"
default: "docs"
result: "{project-root}/{value}"
# tea_use_mcp_enhancements:
# prompt: "Test Architect Playwright MCP capabilities (healing, exploratory, verification) are optionally available.\nYou will have to setup your MCPs yourself; refer to test-architecture.md for hints.\nWould you like to enable MCP enhancements in Test Architect?"
# default: false
# result: "{value}"
tea_use_mcp_enhancements:
prompt: "Enable Test Architect Playwright MCP capabilities (healing, exploratory, verification)?\nYou have to setup your MCPs yourself; refer to test-architecture.md for hints."
default: false
result: "{value}"
tea_use_playwright_utils:
prompt:
- "Are you using playwright-utils (@seontechnologies/playwright-utils) in your project?\nYou must install packages yourself, or use test architect's *framework command."
default: false
result: "{value}"
# tea_use_playwright_utils:
# prompt:
# - "Are you using playwright-utils (@seontechnologies/playwright-utils) in your project?\nYou must install packages yourself, or use test architect's *framework command."
# default: false
# result: "{value}"

View File

@ -1,4 +1,3 @@
# Powered by BMAD™ Core
name: bmmcc
short-title: BMM Claude Code Sub Module
author: Brian (BMad) Madison

View File

@ -4,16 +4,16 @@
#
# The installer will:
# 1. Ask users if they want to install subagents (all/selective/none)
# 2. Ask where to install (project-level .claude/agents/{bmad_folder}/ or user-level ~/.claude/agents/{bmad_folder}/)
# 2. Ask where to install (project-level .claude/agents/_bmad/ or user-level ~/.claude/agents/_bmad/)
# 3. Only inject content related to selected subagents
# 4. Templates stay in {bmad_folder}/ directory and are referenced from there
# 4. Templates stay in _bmad/ directory and are referenced from there
# 5. Injections are placed at specific sections where each subagent is most valuable
injections:
# ===== PRD WORKFLOW INJECTIONS =====
# PRD Subagent Instructions
- file: "{bmad_folder}/bmm/workflows/prd/instructions.md"
- file: "_bmad/bmm/workflows/prd/instructions.md"
point: "prd-subagent-instructions"
requires: "all-prd-subagents"
content: |
@ -25,7 +25,7 @@ injections:
- <CRITICAL>Use `bmm-technical-decisions-curator` to capture all technical mentions</CRITICAL>
# PRD Requirements Analysis
- file: "{bmad_folder}/bmm/workflows/prd/instructions.md"
- file: "_bmad/bmm/workflows/prd/instructions.md"
point: "prd-requirements-analysis"
requires: "requirements-analyst"
content: |
@ -33,7 +33,7 @@ injections:
**Subagent Hint**: Use `bmm-requirements-analyst` to validate requirements are testable and complete.
# PRD User Journey Mapping
- file: "{bmad_folder}/bmm/workflows/prd/instructions.md"
- file: "_bmad/bmm/workflows/prd/instructions.md"
point: "prd-user-journey"
requires: "user-journey-mapper"
content: |
@ -41,7 +41,7 @@ injections:
**Subagent Hint**: Use `bmm-user-journey-mapper` to map all user types and their value paths.
# PRD Epic Optimization
- file: "{bmad_folder}/bmm/workflows/prd/instructions.md"
- file: "_bmad/bmm/workflows/prd/instructions.md"
point: "prd-epic-optimization"
requires: "epic-optimizer"
content: |
@ -49,7 +49,7 @@ injections:
**Subagent Hint**: Use `bmm-epic-optimizer` to validate epic boundaries deliver coherent value.
# PRD Document Review
- file: "{bmad_folder}/bmm/workflows/prd/instructions.md"
- file: "_bmad/bmm/workflows/prd/instructions.md"
point: "prd-checklist-review"
requires: "document-reviewer"
content: |
@ -57,7 +57,7 @@ injections:
**Subagent Hint**: Use `bmm-document-reviewer` to validate PRD completeness before finalizing.
# Technical Decisions Curator
- file: "{bmad_folder}/bmm/workflows/prd/instructions.md"
- file: "_bmad/bmm/workflows/prd/instructions.md"
point: "technical-decisions-curator"
requires: "technical-decisions-curator"
content: |
@ -71,7 +71,7 @@ injections:
# ===== MARKET RESEARCH TEMPLATE INJECTIONS =====
# Market TAM/SAM/SOM Calculations
- file: "{bmad_folder}/bmm/templates/market.md"
- file: "_bmad/bmm/templates/market.md"
point: "market-tam-calculations"
requires: "data-analyst"
content: |
@ -82,7 +82,7 @@ injections:
</llm>
# Market Trends Analysis
- file: "{bmad_folder}/bmm/templates/market.md"
- file: "_bmad/bmm/templates/market.md"
point: "market-trends-analysis"
requires: "trend-spotter"
content: |
@ -93,7 +93,7 @@ injections:
</llm>
# Market Customer Personas
- file: "{bmad_folder}/bmm/templates/market.md"
- file: "_bmad/bmm/templates/market.md"
point: "market-customer-segments"
requires: "user-researcher"
content: |
@ -104,7 +104,7 @@ injections:
</llm>
# Market Research Review
- file: "{bmad_folder}/bmm/templates/market.md"
- file: "_bmad/bmm/templates/market.md"
point: "market-executive-summary"
requires: "document-reviewer"
content: |
@ -116,7 +116,7 @@ injections:
# ===== COMPETITOR ANALYSIS TEMPLATE INJECTIONS =====
# Competitor Intelligence Gathering
- file: "{bmad_folder}/bmm/templates/competitor.md"
- file: "_bmad/bmm/templates/competitor.md"
point: "competitor-intelligence"
requires: "market-researcher"
content: |
@ -127,7 +127,7 @@ injections:
</llm>
# Competitor Technical Analysis
- file: "{bmad_folder}/bmm/templates/competitor.md"
- file: "_bmad/bmm/templates/competitor.md"
point: "competitor-tech-stack"
requires: "technical-evaluator"
content: |
@ -138,7 +138,7 @@ injections:
</llm>
# Competitor Metrics Analysis
- file: "{bmad_folder}/bmm/templates/competitor.md"
- file: "_bmad/bmm/templates/competitor.md"
point: "competitor-metrics"
requires: "data-analyst"
content: |
@ -148,7 +148,7 @@ injections:
</llm>
# Competitor Analysis Review
- file: "{bmad_folder}/bmm/templates/competitor.md"
- file: "_bmad/bmm/templates/competitor.md"
point: "competitor-executive-summary"
requires: "document-reviewer"
content: |
@ -160,7 +160,7 @@ injections:
# ===== PROJECT BRIEF TEMPLATE INJECTIONS =====
# Brief Problem Validation
- file: "{bmad_folder}/bmm/templates/brief.md"
- file: "_bmad/bmm/templates/brief.md"
point: "brief-problem-validation"
requires: "market-researcher"
content: |
@ -170,7 +170,7 @@ injections:
</llm>
# Brief Target User Analysis
- file: "{bmad_folder}/bmm/templates/brief.md"
- file: "_bmad/bmm/templates/brief.md"
point: "brief-user-analysis"
requires: "user-researcher"
content: |
@ -180,7 +180,7 @@ injections:
</llm>
# Brief Success Metrics
- file: "{bmad_folder}/bmm/templates/brief.md"
- file: "_bmad/bmm/templates/brief.md"
point: "brief-success-metrics"
requires: "data-analyst"
content: |
@ -190,7 +190,7 @@ injections:
</llm>
# Brief Technical Feasibility
- file: "{bmad_folder}/bmm/templates/brief.md"
- file: "_bmad/bmm/templates/brief.md"
point: "brief-technical-feasibility"
requires: "technical-evaluator"
content: |
@ -200,7 +200,7 @@ injections:
</llm>
# Brief Requirements Extraction
- file: "{bmad_folder}/bmm/templates/brief.md"
- file: "_bmad/bmm/templates/brief.md"
point: "brief-requirements"
requires: "requirements-analyst"
content: |
@ -210,7 +210,7 @@ injections:
</llm>
# Brief Document Review
- file: "{bmad_folder}/bmm/templates/brief.md"
- file: "_bmad/bmm/templates/brief.md"
point: "brief-final-review"
requires: "document-reviewer"
content: |

View File

@ -84,4 +84,4 @@ To test subagent installation:
2. Select BMM module and Claude Code
3. Verify prompts appear for subagent selection
4. Check `.claude/agents/` for installed subagents
5. Verify injection points are replaced in `.claude/commands/{bmad_folder}/` and the various tasks and templates under `{bmad_folder}/...`
5. Verify injection points are replaced in `.claude/commands/_bmad/` and the various tasks and templates under `_bmad/...`

View File

@ -1,5 +1,5 @@
---
name: create-epics-stories
name: create-epics-and-stories
description: 'Transform PRD requirements and Architecture decisions into comprehensive stories organized by user value. This workflow requires completed PRD + Architecture documents (UX recommended if UI exists) and breaks down requirements into implementation-ready epics and user stories that incorporate all available technical and design context. Creates detailed, actionable stories with complete acceptance criteria for development teams.'
web_bundle: true
---

View File

@ -7,8 +7,8 @@ config_source: "{project-root}/_bmad/bmm/config.yaml"
output_folder: "{config_source}:output_folder"
# Workflow components
installed_path: "{project-root}/_bmad/bmm/workflows/diagrams/create-dataflow"
shared_path: "{project-root}/_bmad/bmm/workflows/diagrams/_shared"
installed_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-dataflow"
shared_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/_shared"
instructions: "{installed_path}/instructions.md"
validation: "{installed_path}/checklist.md"
@ -21,7 +21,7 @@ templates: "{shared_path}/excalidraw-templates.yaml"
library: "{shared_path}/excalidraw-library.json"
# Output file (respects user's configured output_folder)
default_output_file: "{output_folder}/diagrams/dataflow-{timestamp}.excalidraw"
default_output_file: "{output_folder}/excalidraw-diagrams/dataflow-{timestamp}.excalidraw"
standalone: true
web_bundle: false

View File

@ -7,8 +7,8 @@ config_source: "{project-root}/_bmad/bmm/config.yaml"
output_folder: "{config_source}:output_folder"
# Workflow components
installed_path: "{project-root}/_bmad/bmm/workflows/diagrams/create-diagram"
shared_path: "{project-root}/_bmad/bmm/workflows/diagrams/_shared"
installed_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-diagram"
shared_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/_shared"
instructions: "{installed_path}/instructions.md"
validation: "{installed_path}/checklist.md"
@ -21,7 +21,7 @@ templates: "{shared_path}/excalidraw-templates.yaml"
library: "{shared_path}/excalidraw-library.json"
# Output file (respects user's configured output_folder)
default_output_file: "{output_folder}/diagrams/diagram-{timestamp}.excalidraw"
default_output_file: "{output_folder}/excalidraw-diagrams/diagram-{timestamp}.excalidraw"
standalone: true
web_bundle: false

View File

@ -7,8 +7,8 @@ config_source: "{project-root}/_bmad/bmm/config.yaml"
output_folder: "{config_source}:output_folder"
# Workflow components
installed_path: "{project-root}/_bmad/bmm/workflows/diagrams/create-flowchart"
shared_path: "{project-root}/_bmad/bmm/workflows/diagrams/_shared"
installed_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-flowchart"
shared_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/_shared"
instructions: "{installed_path}/instructions.md"
validation: "{installed_path}/checklist.md"
@ -21,7 +21,7 @@ templates: "{shared_path}/excalidraw-templates.yaml"
library: "{shared_path}/excalidraw-library.json"
# Output file (respects user's configured output_folder)
default_output_file: "{output_folder}/diagrams/flowchart-{timestamp}.excalidraw"
default_output_file: "{output_folder}/excalidraw-diagrams/flowchart-{timestamp}.excalidraw"
standalone: true
web_bundle: false

View File

@ -7,8 +7,8 @@ config_source: "{project-root}/_bmad/bmm/config.yaml"
output_folder: "{config_source}:output_folder"
# Workflow components
installed_path: "{project-root}/_bmad/bmm/workflows/diagrams/create-wireframe"
shared_path: "{project-root}/_bmad/bmm/workflows/diagrams/_shared"
installed_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/create-wireframe"
shared_path: "{project-root}/_bmad/bmm/workflows/excalidraw-diagrams/_shared"
instructions: "{installed_path}/instructions.md"
validation: "{installed_path}/checklist.md"
@ -21,7 +21,7 @@ templates: "{shared_path}/excalidraw-templates.yaml"
library: "{shared_path}/excalidraw-library.json"
# Output file (respects user's configured output_folder)
default_output_file: "{output_folder}/diagrams/wireframe-{timestamp}.excalidraw"
default_output_file: "{output_folder}/excalidraw-diagrams/wireframe-{timestamp}.excalidraw"
standalone: true
web_bundle: false

View File

@ -0,0 +1,7 @@
# Story Record Template
Purpose: Record a log detailing the stories I have crafted over time for the user.
## Narratives Told Record
<!-- track stories created metadata with the user over time -->

View File

@ -0,0 +1,7 @@
# Story Record Template
Purpose: Record a log of learned users story telling or story building preferences.
## User Preferences
<!-- record any user preferences about story crafting the user prefers -->

View File

@ -14,6 +14,10 @@ agent:
communication_style: Speaks like a bard weaving an epic tale - flowery, whimsical, every sentence enraptures and draws you deeper
principles: Powerful narratives leverage timeless human truths. Find the authentic story. Make the abstract concrete through vivid details.
critical_actions:
- "Load COMPLETE file {agent_sidecar_folder}/storyteller-sidecar/story-preferences.md and review remember the User Preferences"
- "Load COMPLETE file {agent_sidecar_folder}/storyteller-sidecar/stories-told.md and review the history of stories created for this user"
menu:
- trigger: story
exec: "{project-root}/_bmad/cis/workflows/storytelling/workflow.yaml"

View File

@ -1,7 +1,7 @@
code: cis
name: "CIS: Creative Innovation Suite"
header: "Creative Innovation Suite (CIS) Module"
subheader: "No Configuration needed - uses Core Config only."
subheader: "No custom configuration required - uses Core settings only"
default_selected: false # This module will not be selected by default for new installations
# Variables from Core Config inserted:

View File

@ -39,11 +39,6 @@ module.exports = {
return;
}
// Handle reinstall by setting force flag
if (config.actionType === 'reinstall') {
config._requestedReinstall = true;
}
// Regular install/update flow
const result = await installer.install(config);
@ -55,14 +50,11 @@ module.exports = {
// Check if installation succeeded
if (result && result.success) {
console.log(chalk.green('\n✨ Installation complete!'));
console.log(chalk.cyan('BMAD Core and Selected Modules have been installed to:'), chalk.bold(result.path));
console.log(chalk.yellow('\nThank you for helping test the early release version of the new BMad Core and BMad Method!'));
console.log(chalk.cyan('Stable Beta coming soon - please read the full README.md and linked documentation to get started!'));
// Run AgentVibes installer if needed
if (result.needsAgentVibes) {
console.log(chalk.magenta('\n🎙 AgentVibes TTS Setup'));
// Add some spacing before AgentVibes setup
console.log('');
console.log(chalk.magenta('🎙️ AgentVibes TTS Setup'));
console.log(chalk.cyan('AgentVibes provides voice synthesis for BMAD agents with:'));
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
@ -91,6 +83,7 @@ module.exports = {
shell: true,
});
console.log(chalk.green('\n✓ AgentVibes installation complete'));
console.log(chalk.cyan('\n✨ BMAD with TTS is ready to use!'));
} catch {
console.log(chalk.yellow('\n⚠ AgentVibes installation was interrupted or failed'));
console.log(chalk.cyan('You can run it manually later with:'));

View File

@ -9,7 +9,9 @@ module.exports = {
options: [],
action: async () => {
try {
const modules = await installer.getAvailableModules();
const result = await installer.getAvailableModules();
const { modules, customModules } = result;
console.log(chalk.cyan('\n📦 Available BMAD Modules:\n'));
for (const module of modules) {
@ -19,6 +21,16 @@ module.exports = {
console.log();
}
if (customModules && customModules.length > 0) {
console.log(chalk.cyan('\n🔧 Custom Modules:\n'));
for (const module of customModules) {
console.log(chalk.bold(` ${module.id}`));
console.log(chalk.dim(` ${module.description}`));
console.log(chalk.dim(` Version: ${module.version}`));
console.log();
}
}
process.exit(0);
} catch (error) {
console.error(chalk.red('Error:'), error.message);

View File

@ -127,10 +127,6 @@ class ConfigCollector {
}
}
if (foundAny) {
console.log(chalk.cyan('\n📋 Found existing BMAD module configurations'));
}
return foundAny;
}
@ -254,6 +250,26 @@ class ConfigCollector {
const configKeys = Object.keys(moduleConfig).filter((key) => key !== 'prompt');
const existingKeys = this.existingConfig && this.existingConfig[moduleName] ? Object.keys(this.existingConfig[moduleName]) : [];
// Check if this module has no configuration keys at all (like CIS)
// Filter out metadata fields and only count actual config objects
const metadataFields = new Set(['code', 'name', 'header', 'subheader', 'default_selected']);
const actualConfigKeys = configKeys.filter((key) => !metadataFields.has(key));
const hasNoConfig = actualConfigKeys.length === 0;
// If module has no config keys at all, handle it specially
if (hasNoConfig && moduleConfig.subheader) {
// Add blank line for better readability (matches other modules)
console.log();
const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
// Display the module name in color first (matches other modules)
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
// Show the subheader since there's no configuration to ask about
console.log(chalk.dim(`${moduleConfig.subheader}`));
return false; // No new fields
}
// Find new interactive fields (with prompt)
const newKeys = configKeys.filter((key) => {
const item = moduleConfig[key];
@ -302,11 +318,12 @@ class ConfigCollector {
this.allAnswers[`${moduleName}_user_name`] = this.getDefaultUsername();
}
}
// Show "no config" message for modules with no new questions
CLIUtils.displayModuleNoConfig(moduleName, moduleConfig.header, moduleConfig.subheader);
return false; // No new fields
}
// Show "no config" message for modules with no new questions (that have config keys)
console.log(chalk.dim(`${moduleName.toUpperCase()} module already up to date`));
return false; // No new fields
// If we have new fields (interactive or static), process them
if (newKeys.length > 0 || newStaticKeys.length > 0) {
const questions = [];
@ -339,7 +356,7 @@ class ConfigCollector {
Object.assign(allAnswers, promptedAnswers);
} else if (newStaticKeys.length > 0) {
// Only static fields, no questions - show no config message
CLIUtils.displayModuleNoConfig(moduleName, moduleConfig.header, moduleConfig.subheader);
console.log(chalk.dim(`${moduleName.toUpperCase()} module configuration updated`));
}
// Store all answers for cross-referencing
@ -558,21 +575,60 @@ class ConfigCollector {
// Collect all answers (static + prompted)
let allAnswers = { ...staticAnswers };
// Display appropriate header based on whether there are questions
// If there are questions to ask, prompt for accepting defaults vs customizing
if (questions.length > 0) {
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
console.log(); // Line break before questions
const promptedAnswers = await inquirer.prompt(questions);
// Get friendly module name from config or use uppercase module name
const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
// Merge prompted answers with static answers
Object.assign(allAnswers, promptedAnswers);
// Add blank line for better readability
console.log();
// Display the module name in color first
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
// Ask user if they want to accept defaults or customize on the next line
const { customize } = await inquirer.prompt([
{
type: 'confirm',
name: 'customize',
message: 'Accept Defaults (no to customize)?',
default: true,
},
]);
if (customize) {
// Accept defaults - only ask questions that have NO default value
const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');
if (questionsWithoutDefaults.length > 0) {
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
const promptedAnswers = await inquirer.prompt(questionsWithoutDefaults);
Object.assign(allAnswers, promptedAnswers);
}
// For questions with defaults that weren't asked, we need to process them with their default values
const questionsWithDefaults = questions.filter((q) => q.default !== undefined && q.default !== null && q.default !== '');
for (const question of questionsWithDefaults) {
// Skip function defaults - these are dynamic and will be evaluated later
if (typeof question.default === 'function') {
continue;
}
allAnswers[question.name] = question.default;
}
} else {
// Customize - ask all questions
console.log(chalk.dim(`\n Configuring ${moduleName.toUpperCase()}...`));
const promptedAnswers = await inquirer.prompt(questions);
Object.assign(allAnswers, promptedAnswers);
}
}
// Store all answers for cross-referencing
Object.assign(this.allAnswers, allAnswers);
// Process all answers (both static and prompted)
if (Object.keys(allAnswers).length > 0) {
// Always process if we have any answers or static answers
if (Object.keys(allAnswers).length > 0 || Object.keys(staticAnswers).length > 0) {
const answers = allAnswers;
// Process answers and build result values
@ -671,8 +727,68 @@ class ConfigCollector {
// No longer display completion boxes - keep output clean
} else {
// No questions for this module - show completion message
CLIUtils.displayModuleNoConfig(moduleName, moduleConfig.header, moduleConfig.subheader);
// No questions for this module - show completion message with header if available
const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
// Check if this module has NO configuration keys at all (like CIS)
// Filter out metadata fields and only count actual config objects
const metadataFields = new Set(['code', 'name', 'header', 'subheader', 'default_selected']);
const actualConfigKeys = configKeys.filter((key) => !metadataFields.has(key));
const hasNoConfig = actualConfigKeys.length === 0;
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
// Module explicitly has no configuration - show with special styling
// Add blank line for better readability (matches other modules)
console.log();
// Display the module name in color first (matches other modules)
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
// Ask user if they want to accept defaults or customize on the next line
const { customize } = await inquirer.prompt([
{
type: 'confirm',
name: 'customize',
message: 'Accept Defaults (no to customize)?',
default: true,
},
]);
// Show the subheader if available, otherwise show a default message
if (moduleConfig.subheader) {
console.log(chalk.dim(`${moduleConfig.subheader}`));
} else {
console.log(chalk.dim(` ✓ No custom configuration required`));
}
} else {
// Module has config but just no questions to ask
console.log(chalk.dim(`${moduleName.toUpperCase()} module configured`));
}
}
// If we have no collected config for this module, but we have a module schema,
// ensure we have at least an empty object
if (!this.collectedConfig[moduleName]) {
this.collectedConfig[moduleName] = {};
// If we accepted defaults and have no answers, we still need to check
// if there are any static values in the schema that should be applied
if (moduleConfig) {
for (const key of Object.keys(moduleConfig)) {
if (key !== 'prompt' && moduleConfig[key] && typeof moduleConfig[key] === 'object') {
const item = moduleConfig[key];
// For static items (no prompt, just result), apply the result
if (!item.prompt && item.result) {
// Apply any placeholder replacements to the result
let result = item.result;
if (typeof result === 'string') {
result = this.replacePlaceholders(result, moduleName, moduleConfig);
}
this.collectedConfig[moduleName][key] = result;
}
}
}
}
}
}

View File

@ -40,7 +40,10 @@ class CustomModuleCache {
*/
async updateCacheManifest(manifest) {
const yaml = require('yaml');
const content = yaml.stringify(manifest, {
// Clean the manifest to remove any non-serializable values
const cleanManifest = structuredClone(manifest);
const content = yaml.stringify(cleanManifest, {
indent: 2,
lineWidth: 0,
sortKeys: false,
@ -144,12 +147,18 @@ class CustomModuleCache {
const sourceHash = await this.calculateHash(sourcePath);
const cacheHash = await this.calculateHash(cacheDir);
// Update manifest - don't store originalPath for source control friendliness
// Update manifest - don't store absolute paths for portability
// Clean metadata to remove absolute paths
const cleanMetadata = { ...metadata };
if (cleanMetadata.sourcePath) {
delete cleanMetadata.sourcePath;
}
cacheManifest[moduleId] = {
originalHash: sourceHash,
cacheHash: cacheHash,
cachedAt: new Date().toISOString(),
...metadata,
...cleanMetadata,
};
await this.updateCacheManifest(cacheManifest);

View File

@ -61,7 +61,10 @@ class IdeConfigManager {
configuration: configuration || {},
};
const yamlContent = yaml.stringify(configData, {
// Clean the config to remove any non-serializable values (like functions)
const cleanConfig = structuredClone(configData);
const yamlContent = yaml.stringify(cleanConfig, {
indent: 2,
lineWidth: 0,
sortKeys: false,

View File

@ -32,7 +32,7 @@ class Installer {
this.dependencyResolver = new DependencyResolver();
this.configCollector = new ConfigCollector();
this.ideConfigManager = new IdeConfigManager();
this.installedFiles = []; // Track all installed files
this.installedFiles = new Set(); // Track all installed files
this.ttsInjectedFiles = []; // Track files with TTS injection applied
}
@ -394,11 +394,20 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Clone config to avoid mutating the caller's object
const config = { ...originalConfig };
// Display BMAD logo
CLIUtils.displayLogo();
// Check if core config was already collected in UI
const hasCoreConfig = config.coreConfig && Object.keys(config.coreConfig).length > 0;
// Display welcome message
CLIUtils.displaySection('BMAD™ Installation', 'Version ' + require(path.join(getProjectRoot(), 'package.json')).version);
// Only display logo if core config wasn't already collected (meaning we're not continuing from UI)
if (hasCoreConfig) {
// Core config was already collected in UI, show smooth continuation
// Don't clear screen, just continue flow
} else {
// Display BMAD logo
CLIUtils.displayLogo();
// Display welcome message
CLIUtils.displaySection('BMad™ Installation', 'Version ' + require(path.join(getProjectRoot(), 'package.json')).version);
}
// Note: Legacy V4 detection now happens earlier in UI.promptInstall()
// before any config collection, so we don't need to check again here
@ -406,7 +415,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const projectDir = path.resolve(config.directory);
// If core config was pre-collected (from interactive mode), use it
if (config.coreConfig) {
if (config.coreConfig && Object.keys(config.coreConfig).length > 0) {
this.configCollector.collectedConfig.core = config.coreConfig;
// Also store in allAnswers for cross-referencing
this.configCollector.allAnswers = {};
@ -417,12 +426,20 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Collect configurations for modules (skip if quick update already collected them)
let moduleConfigs;
let customModulePaths = new Map();
if (config._quickUpdate) {
// Quick update already collected all configs, use them directly
moduleConfigs = this.configCollector.collectedConfig;
// For quick update, populate customModulePaths from _customModuleSources
if (config._customModuleSources) {
for (const [moduleId, customInfo] of config._customModuleSources) {
customModulePaths.set(moduleId, customInfo.sourcePath);
}
}
} else {
// Build custom module paths map from customContent
const customModulePaths = new Map();
// Handle selectedFiles (from existing install path or manual directory input)
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
@ -435,6 +452,13 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
// Handle new custom content sources from UI
if (config.customContent && config.customContent.sources) {
for (const source of config.customContent.sources) {
customModulePaths.set(source.id, source.path);
}
}
// Handle cachedModules (from new install path where modules are cached)
// Only include modules that were actually selected for installation
if (config.customContent && config.customContent.cachedModules) {
@ -456,17 +480,33 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
// Get list of all modules including custom modules
const allModulesForConfig = [...(config.modules || [])];
// Order: core first, then official modules, then custom modules
const allModulesForConfig = ['core'];
// Add official modules (excluding core and any custom modules)
const officialModules = (config.modules || []).filter((m) => m !== 'core' && !customModulePaths.has(m));
allModulesForConfig.push(...officialModules);
// Add custom modules at the end
for (const [moduleId] of customModulePaths) {
if (!allModulesForConfig.includes(moduleId)) {
allModulesForConfig.push(moduleId);
}
}
// Regular install - collect configurations (core was already collected in UI.promptInstall if interactive)
moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), {
customModulePaths,
});
// Check if core was already collected in UI
if (config.coreConfig && Object.keys(config.coreConfig).length > 0) {
// Core already collected, skip it in config collection
const modulesWithoutCore = allModulesForConfig.filter((m) => m !== 'core');
moduleConfigs = await this.configCollector.collectAllConfigurations(modulesWithoutCore, path.resolve(config.directory), {
customModulePaths,
});
} else {
// Core not collected yet, include it
moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), {
customModulePaths,
});
}
}
// Always use _bmad as the folder name
@ -479,6 +519,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Set bmad folder name on module manager and IDE manager for placeholder replacement
this.moduleManager.setBmadFolderName(bmadFolderName);
this.moduleManager.setCoreConfig(moduleConfigs.core || {});
this.moduleManager.setCustomModulePaths(customModulePaths);
this.ideManager.setBmadFolderName(bmadFolderName);
// Tool selection will be collected after we determine if it's a reinstall/update/new install
@ -529,9 +570,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Check if user already decided what to do (from early menu in ui.js)
let action = null;
if (config._requestedReinstall) {
action = 'reinstall';
} else if (config.actionType === 'update') {
if (config.actionType === 'update') {
action = 'update';
} else {
// Fallback: Ask the user (backwards compatibility for other code paths)
@ -543,41 +582,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
action = promptResult.action;
}
if (action === 'cancel') {
console.log('Installation cancelled.');
return { success: false, cancelled: true };
}
if (action === 'reinstall') {
// Warn about destructive operation
console.log(chalk.red.bold('\n⚠ WARNING: This is a destructive operation!'));
console.log(chalk.red('All custom files and modifications in the bmad directory will be lost.'));
const inquirer = require('inquirer');
const { confirmReinstall } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirmReinstall',
message: chalk.yellow('Are you sure you want to delete and reinstall?'),
default: false,
},
]);
if (!confirmReinstall) {
console.log('Installation cancelled.');
return { success: false, cancelled: true };
}
// Remember previously configured IDEs before deleting
config._previouslyConfiguredIdes = existingInstall.ides || [];
// Remove existing installation
await fs.remove(bmadDir);
console.log(chalk.green('✓ Removed existing installation\n'));
// Mark this as a full reinstall so we re-collect IDE configurations
config._isFullReinstall = true;
} else if (action === 'update') {
if (action === 'update') {
// Store that we're updating for later processing
config._isUpdate = true;
config._existingInstall = existingInstall;
@ -733,6 +738,26 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
spinner.text = 'Creating directory structure...';
await this.createDirectoryStructure(bmadDir);
// Cache custom modules if any
if (customModulePaths && customModulePaths.size > 0) {
spinner.text = 'Caching custom modules...';
const { CustomModuleCache } = require('./custom-module-cache');
const customCache = new CustomModuleCache(bmadDir);
for (const [moduleId, sourcePath] of customModulePaths) {
const cachedInfo = await customCache.cacheModule(moduleId, sourcePath, {
sourcePath: sourcePath, // Store original path for updates
});
// Update the customModulePaths to use the cached location
customModulePaths.set(moduleId, cachedInfo.cachePath);
}
// Update module manager with the cached paths
this.moduleManager.setCustomModulePaths(customModulePaths);
spinner.succeed('Custom modules cached');
}
// Get project root
const projectRoot = getProjectRoot();
@ -790,6 +815,20 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const modulesToInstall = allModules;
// For dependency resolution, we only need regular modules (not custom modules)
// Custom modules are already installed in _bmad and don't need dependency resolution from source
const regularModulesForResolution = allModules.filter((module) => {
// Check if this is a custom module
const isCustom =
customModulePaths.has(module) ||
(finalCustomContent && finalCustomContent.cachedModules && finalCustomContent.cachedModules.some((cm) => cm.id === module)) ||
(finalCustomContent &&
finalCustomContent.selected &&
finalCustomContent.selectedFiles &&
finalCustomContent.selectedFiles.some((f) => f.includes(module)));
return !isCustom;
});
// For dependency resolution, we need to pass the project root
// Create a temporary module manager that knows about custom content locations
const tempModuleManager = new ModuleManager({
@ -797,24 +836,12 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
bmadDir: bmadDir, // Pass bmadDir so we can check cache
});
// Make sure custom modules are discoverable
if (config.customContent && config.customContent.selected && config.customContent.selectedFiles) {
// The dependency resolver needs to know about these modules
// We'll handle custom modules separately in the installation loop
}
const resolution = await this.dependencyResolver.resolve(projectRoot, allModules, {
const resolution = await this.dependencyResolver.resolve(projectRoot, regularModulesForResolution, {
verbose: config.verbose,
moduleManager: tempModuleManager,
});
if (config.verbose) {
spinner.succeed('Dependencies resolved');
} else {
spinner.succeed('Dependencies resolved');
}
// Core is already installed above, skip if included in resolution
spinner.succeed('Dependencies resolved');
// Install modules with their dependencies
if (allModules && allModules.length > 0) {
@ -827,7 +854,9 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
installedModuleNames.add(moduleName);
spinner.start(`Installing module: ${moduleName}...`);
// Show appropriate message based on whether this is a quick update
const isQuickUpdate = config._quickUpdate || false;
spinner.start(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`);
// Check if this is a custom module
let isCustomModule = false;
@ -880,103 +909,36 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
if (isCustomModule && customInfo) {
// Install custom module using CustomHandler but as a proper module
const customHandler = new CustomHandler();
// Install to module directory instead of custom directory
const moduleTargetPath = path.join(bmadDir, moduleName);
await fs.ensureDir(moduleTargetPath);
// Custom modules are now installed via ModuleManager just like standard modules
// The custom module path should already be in customModulePaths from earlier setup
if (!customModulePaths.has(moduleName) && customInfo.path) {
customModulePaths.set(moduleName, customInfo.path);
this.moduleManager.setCustomModulePaths(customModulePaths);
}
// Get collected config for this custom module (from module.yaml prompts)
const collectedModuleConfig = moduleConfigs[moduleName] || {};
const result = await customHandler.install(
customInfo.path,
path.join(bmadDir, 'temp-custom'),
{ ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig, _bmadDir: bmadDir },
// Use ModuleManager to install the custom module
await this.moduleManager.install(
moduleName,
bmadDir,
(filePath) => {
// Track installed files with correct path
const relativePath = path.relative(path.join(bmadDir, 'temp-custom'), filePath);
const finalPath = path.join(moduleTargetPath, relativePath);
this.installedFiles.push(finalPath);
this.installedFiles.add(filePath);
},
{
isCustom: true,
moduleConfig: collectedModuleConfig,
isQuickUpdate: config._quickUpdate || false,
},
);
// Move from temp-custom to actual module directory
const tempCustomPath = path.join(bmadDir, 'temp-custom');
if (await fs.pathExists(tempCustomPath)) {
const customDir = path.join(tempCustomPath, 'custom');
if (await fs.pathExists(customDir)) {
// Move contents to module directory
const items = await fs.readdir(customDir);
const movedItems = [];
try {
for (const item of items) {
const srcPath = path.join(customDir, item);
const destPath = path.join(moduleTargetPath, item);
// If destination exists, remove it first (or we could merge)
if (await fs.pathExists(destPath)) {
await fs.remove(destPath);
}
await fs.move(srcPath, destPath);
movedItems.push({ src: srcPath, dest: destPath });
}
} catch (moveError) {
// Rollback: restore any successfully moved items
for (const moved of movedItems) {
try {
await fs.move(moved.dest, moved.src);
} catch {
// Best-effort rollback - log if it fails
console.error(`Failed to rollback ${moved.dest} during cleanup`);
}
}
throw new Error(`Failed to move custom module files: ${moveError.message}`);
}
}
try {
await fs.remove(tempCustomPath);
} catch (cleanupError) {
// Non-fatal: temp directory cleanup failed but files were moved successfully
console.warn(`Warning: Could not clean up temp directory: ${cleanupError.message}`);
}
}
// ModuleManager installs directly to the target directory, no need to move files
// Create module config (include collected config from module.yaml prompts)
await this.generateModuleConfigs(bmadDir, {
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
});
// Store custom module info for later manifest update
if (!config._customModulesToTrack) {
config._customModulesToTrack = [];
}
// For cached modules, use appropriate path handling
let sourcePath;
if (useCache) {
// Check if we have cached modules info (from initial install)
if (finalCustomContent && finalCustomContent.cachedModules) {
sourcePath = finalCustomContent.cachedModules.find((m) => m.id === moduleName)?.relativePath;
} else {
// During update, the sourcePath is already cache-relative if it starts with _config
sourcePath =
customInfo.sourcePath && customInfo.sourcePath.startsWith('_config')
? customInfo.sourcePath
: path.relative(bmadDir, customInfo.path || customInfo.sourcePath);
}
} else {
sourcePath = path.resolve(customInfo.path || customInfo.sourcePath);
}
config._customModulesToTrack.push({
id: customInfo.id,
name: customInfo.name,
sourcePath: sourcePath,
installDate: new Date().toISOString(),
});
} else {
// Regular module installation
// Special case for core module
@ -987,7 +949,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
spinner.succeed(`Module installed: ${moduleName}`);
spinner.succeed(`Module ${isQuickUpdate ? 'updated' : 'installed'}: ${moduleName}`);
}
// Install partial modules (only dependencies)
@ -1009,69 +971,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
// Install custom content if provided AND selected
// Process custom content that wasn't installed as modules
// This is now handled in the module installation loop above
// This section is kept for backward compatibility with any custom content
// that doesn't have a module structure
const remainingCustomContent = [];
if (
config.customContent &&
config.customContent.hasCustomContent &&
config.customContent.customPath &&
config.customContent.selected &&
config.customContent.selectedFiles
) {
// Filter out custom modules that were already installed
const customHandler = new CustomHandler();
for (const customFile of config.customContent.selectedFiles) {
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
// Skip if this was installed as a module
if (!customInfo || !customInfo.id || !allModules.includes(customInfo.id)) {
remainingCustomContent.push(customFile);
}
}
}
if (remainingCustomContent.length > 0) {
spinner.start('Installing remaining custom content...');
const customHandler = new CustomHandler();
// Use the remaining files
const customFiles = remainingCustomContent;
if (customFiles.length > 0) {
console.log(chalk.cyan(`\n Found ${customFiles.length} custom content file(s):`));
for (const customFile of customFiles) {
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
if (customInfo) {
console.log(chalk.dim(`${customInfo.name} (${customInfo.relativePath})`));
// Install the custom content
const result = await customHandler.install(
customInfo.path,
bmadDir,
{ ...config.coreConfig, ...customInfo.config },
(filePath) => {
// Track installed files
this.installedFiles.push(filePath);
},
);
if (result.errors.length > 0) {
console.log(chalk.yellow(` ⚠️ ${result.errors.length} error(s) occurred`));
for (const error of result.errors) {
console.log(chalk.dim(` - ${error}`));
}
} else {
console.log(chalk.green(` ✓ Installed ${result.agentsInstalled} agents, ${result.workflowsInstalled} workflows`));
}
}
}
}
spinner.succeed('Custom content installed');
}
// All content is now installed as modules - no separate custom content handling needed
// Generate clean config.yaml files for each installed module
spinner.start('Generating module configurations...');
@ -1084,12 +984,10 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Pre-register manifest files that will be created (except files-manifest.csv to avoid recursion)
const cfgDir = path.join(bmadDir, '_config');
this.installedFiles.push(
path.join(cfgDir, 'manifest.yaml'),
path.join(cfgDir, 'workflow-manifest.csv'),
path.join(cfgDir, 'agent-manifest.csv'),
path.join(cfgDir, 'task-manifest.csv'),
);
this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv'));
// Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup
spinner.start('Generating workflow and agent manifests...');
@ -1113,18 +1011,12 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
}
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, this.installedFiles, {
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
ides: config.ides || [],
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
});
// Add custom modules to manifest (now that it exists)
if (config._customModulesToTrack && config._customModulesToTrack.length > 0) {
spinner.text = 'Storing custom module sources...';
for (const customModule of config._customModulesToTrack) {
await this.manifest.addCustomModule(bmadDir, customModule);
}
}
// Custom modules are now included in the main modules list - no separate tracking needed
spinner.succeed(
`Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`,
@ -1185,24 +1077,24 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
console.log = originalLog;
if (spinner.isSpinning) {
spinner.succeed(`Configured ${validIdes.length} IDE${validIdes.length > 1 ? 's' : ''}`);
spinner.succeed(`Configured: ${validIdes.join(', ')}`);
} else {
console.log(chalk.green(`✓ Configured ${validIdes.length} IDE${validIdes.length > 1 ? 's' : ''}`));
console.log(chalk.green(`✓ Configured: ${validIdes.join(', ')}`));
}
}
// Copy IDE-specific documentation (only for valid IDEs)
const validIdesForDocs = (config.ides || []).filter((ide) => ide && typeof ide === 'string');
if (validIdesForDocs.length > 0) {
spinner.start('Copying IDE documentation...');
await this.copyIdeDocumentation(validIdesForDocs, bmadDir);
spinner.succeed('IDE documentation copied');
}
}
// Run module-specific installers after IDE setup
spinner.start('Running module-specific installers...');
// Create a conditional logger based on verbose mode
const verboseMode = process.env.BMAD_VERBOSE_INSTALL === 'true' || config.verbose;
const moduleLogger = {
log: (msg) => (verboseMode ? console.log(msg) : {}), // Only log in verbose mode
error: (msg) => console.error(msg), // Always show errors
warn: (msg) => console.warn(msg), // Always show warnings
};
// Run core module installer if core was installed
if (config.installCore || resolution.byModule.core) {
spinner.text = 'Running core module installer...';
@ -1211,11 +1103,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
installedIDEs: config.ides || [],
moduleConfig: moduleConfigs.core || {},
coreConfig: moduleConfigs.core || {},
logger: {
log: (msg) => console.log(msg),
error: (msg) => console.error(msg),
warn: (msg) => console.warn(msg),
},
logger: moduleLogger,
});
}
@ -1229,11 +1117,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
installedIDEs: config.ides || [],
moduleConfig: moduleConfigs[moduleName] || {},
coreConfig: moduleConfigs.core || {},
logger: {
log: (msg) => console.log(msg),
error: (msg) => console.error(msg),
warn: (msg) => console.warn(msg),
},
logger: moduleLogger,
});
}
}
@ -1301,20 +1185,20 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
if (customFiles.length > 0) {
console.log(chalk.cyan(`\n📁 Custom files preserved: ${customFiles.length}`));
console.log(chalk.dim('The following custom files were found and restored:\n'));
for (const file of customFiles) {
console.log(chalk.dim(` - ${path.relative(bmadDir, file)}`));
for (const customFile of customFiles) {
const relativePath = path.relative(projectDir, customFile);
console.log(chalk.dim(`${relativePath}`));
}
console.log('');
}
if (modifiedFiles.length > 0) {
console.log(chalk.yellow(`\n⚠️ Modified files detected: ${modifiedFiles.length}`));
console.log(chalk.dim('The following files were modified and backed up with .bak extension:\n'));
for (const file of modifiedFiles) {
console.log(chalk.dim(` - ${file.relativePath}${file.relativePath}.bak`));
}
console.log(chalk.dim('\nThese files have been updated with the new version.'));
console.log(chalk.dim('Review the .bak files to see your changes and merge if needed.\n'));
console.log(chalk.yellow(`\n⚠️ User modified files detected: ${modifiedFiles.length}`));
console.log(
chalk.dim(
'\nThese user modified files have been updated with the new version, search the project for .bak files that had your customizations.',
),
);
console.log(chalk.dim('Remove these .bak files it no longer needed\n'));
}
// Display completion message
@ -1367,12 +1251,44 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Check for custom modules with missing sources before update
const customModuleSources = new Map();
// Check manifest for backward compatibility
if (existingInstall.customModules) {
for (const customModule of existingInstall.customModules) {
customModuleSources.set(customModule.id, customModule);
}
}
// Also check cache directory
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;
// Skip if we already have this module
if (customModuleSources.has(moduleId)) {
continue;
}
const cachedPath = path.join(cacheDir, moduleId);
// Check if this is actually a custom module (has module.yaml)
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
if (await fs.pathExists(moduleYamlPath)) {
customModuleSources.set(moduleId, {
id: moduleId,
name: moduleId,
sourcePath: path.join('_config', 'custom', moduleId), // Relative path
cached: true,
});
}
}
}
}
if (customModuleSources.size > 0) {
spinner.stop();
console.log(chalk.yellow('\nChecking custom module sources before update...'));
@ -1526,8 +1442,11 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
coreSection = '\n# Core Configuration Values\n';
}
// Clean the config to remove any non-serializable values (like functions)
const cleanConfig = structuredClone(finalConfig);
// Convert config to YAML
let yamlContent = yaml.stringify(finalConfig, {
let yamlContent = yaml.stringify(cleanConfig, {
indent: 2,
lineWidth: 0,
minContentWidth: 0,
@ -1562,7 +1481,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
await fs.writeFile(configPath, content.endsWith('\n') ? content : content + '\n', 'utf8');
// Track the config file in installedFiles
this.installedFiles.push(configPath);
this.installedFiles.add(configPath);
}
}
}
@ -1601,7 +1520,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
moduleName,
bmadDir,
(filePath) => {
this.installedFiles.push(filePath);
this.installedFiles.add(filePath);
},
{
skipModuleInstaller: true, // We'll run it later after IDE setup
@ -1638,7 +1557,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
if (await fs.pathExists(sourcePath)) {
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
this.installedFiles.push(targetPath);
this.installedFiles.add(targetPath);
}
}
}
@ -1654,7 +1573,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
if (await fs.pathExists(sourcePath)) {
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
this.installedFiles.push(targetPath);
this.installedFiles.add(targetPath);
}
}
}
@ -1670,7 +1589,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
if (await fs.pathExists(sourcePath)) {
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
this.installedFiles.push(targetPath);
this.installedFiles.add(targetPath);
}
}
}
@ -1686,7 +1605,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
if (await fs.pathExists(sourcePath)) {
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
this.installedFiles.push(targetPath);
this.installedFiles.add(targetPath);
}
}
}
@ -1701,7 +1620,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
if (await fs.pathExists(dataPath)) {
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad');
this.installedFiles.push(targetPath);
this.installedFiles.add(targetPath);
}
}
}
@ -1800,7 +1719,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
// Track the installed file
this.installedFiles.push(targetFile);
this.installedFiles.add(targetFile);
}
}
@ -1876,7 +1795,9 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
if (await fs.pathExists(genericTemplatePath)) {
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath, this.bmadFolderName || 'bmad');
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
}
}
}
}
@ -2086,12 +2007,17 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Compile using the same compiler as initial installation
const { compileAgent } = require('../../../lib/agent/compiler');
const { xml } = await compileAgent(yamlContent, answers, agentName, path.relative(bmadDir, targetMdPath), {
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 = xml.replaceAll('_bmad', path.basename(bmadDir));
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';
@ -2222,6 +2148,12 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
} else {
// Selective update - preserve user modifications
await this.fileOps.syncDirectory(sourcePath, targetPath);
// Recompile agents (#1133)
const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager();
await moduleManager.compileModuleAgents(sourcePath, targetPath, 'core', bmadDir);
await this.processAgentFiles(targetPath, 'core');
}
}
@ -2252,8 +2184,10 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const configuredIdes = existingInstall.ides || [];
const projectRoot = path.dirname(bmadDir);
// Get custom module sources from manifest
// Get custom module sources from manifest and cache
const customModuleSources = new Map();
// First check manifest for backward compatibility
if (existingInstall.customModules) {
for (const customModule of existingInstall.customModules) {
// Ensure we have an absolute sourcePath
@ -2284,6 +2218,37 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
// Also check cache directory for any modules not in manifest
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;
// Skip if we already have this module from manifest
if (customModuleSources.has(moduleId)) {
continue;
}
const cachedPath = path.join(cacheDir, moduleId);
// Check if this is actually a custom module (has module.yaml)
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
if (await fs.pathExists(moduleYamlPath)) {
// For quick update, we always rebuild from cache
customModuleSources.set(moduleId, {
id: moduleId,
name: moduleId, // We'll read the actual name if needed
sourcePath: cachedPath,
cached: true, // Flag to indicate this is from cache
});
}
}
}
}
// Load saved IDE configurations
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
@ -2565,11 +2530,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
type: 'list',
name: 'action',
message: 'What would you like to do?',
choices: [
{ name: 'Update existing installation', value: 'update' },
{ name: 'Remove and reinstall', value: 'reinstall' },
{ name: 'Cancel', value: 'cancel' },
],
choices: [{ name: 'Update existing installation', value: 'update' }],
},
]);
}
@ -2737,14 +2698,10 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const installedFilesMap = new Map();
for (const fileEntry of existingFilesManifest) {
if (fileEntry.path) {
// Paths are relative to bmadDir. Legacy manifests incorrectly prefixed 'bmad/' -
// strip it if present. This is safe because no real path inside bmadDir would
// start with 'bmad/' (you'd never have _bmad/bmad/... as an actual structure).
const relativePath = fileEntry.path.startsWith('bmad/') ? fileEntry.path.slice(5) : fileEntry.path;
const absolutePath = path.join(bmadDir, relativePath);
const absolutePath = path.join(bmadDir, fileEntry.path);
installedFilesMap.set(path.normalize(absolutePath), {
hash: fileEntry.hash,
relativePath: relativePath,
relativePath: fileEntry.path,
});
}
}
@ -2796,7 +2753,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
// Skip config.yaml files - these are regenerated on each install/update
// Users should use _config/agents/ override files instead
if (fileName === 'config.yaml') {
continue;
}
@ -2819,8 +2775,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
});
}
}
// If manifest doesn't have hashes, we can't detect modifications
// so we just skip files that are in the manifest
}
}
} catch {
@ -2950,7 +2904,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
await fs.writeFile(configPath, configContent, 'utf8');
this.installedFiles.push(configPath); // Track agent config files
this.installedFiles.add(configPath); // Track agent config files
createdCount++;
}
@ -3016,25 +2970,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
return nodes;
}
/**
* Copy IDE-specific documentation to BMAD docs
* @param {Array} ides - List of selected IDEs
* @param {string} bmadDir - BMAD installation directory
*/
async copyIdeDocumentation(ides, bmadDir) {
const docsDir = path.join(bmadDir, 'docs');
await fs.ensureDir(docsDir);
for (const ide of ides) {
const sourceDocPath = path.join(getProjectRoot(), 'docs', 'ide-info', `${ide}.md`);
const targetDocPath = path.join(docsDir, `${ide}-instructions.md`);
if (await fs.pathExists(sourceDocPath)) {
await this.copyFileWithPlaceholderReplacement(sourceDocPath, targetDocPath, this.bmadFolderName || 'bmad');
}
}
}
/**
* Handle missing custom module sources interactively
* @param {Map} customModuleSources - Map of custom module ID to info
@ -3059,13 +2994,23 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
info: customInfo,
});
} else {
customModulesWithMissingSources.push({
id: moduleId,
name: customInfo.name,
sourcePath: customInfo.sourcePath,
relativePath: customInfo.relativePath,
info: customInfo,
});
// For cached modules that are missing, we just skip them without prompting
if (customInfo.cached) {
// Skip cached modules without prompting
keptModulesWithoutSources.push({
id: moduleId,
name: customInfo.name,
cached: true,
});
} else {
customModulesWithMissingSources.push({
id: moduleId,
name: customInfo.name,
sourcePath: customInfo.sourcePath,
relativePath: customInfo.relativePath,
info: customInfo,
});
}
}
}

View File

@ -38,14 +38,17 @@ class ManifestGenerator {
// Scan the bmad directory to find all actually installed modules
const installedModules = await this.scanInstalledModules(bmadDir);
// Deduplicate modules list to prevent duplicates
this.modules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])];
this.updatedModules = [...new Set(['core', ...selectedModules, ...installedModules])]; // All installed modules get rescanned
// Since custom modules are now installed the same way as regular modules,
// we don't need to exclude them from manifest generation
const allModules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])];
this.modules = allModules;
this.updatedModules = allModules; // Include ALL modules (including custom) for scanning
// For CSV manifests, we need to include ALL modules that are installed
// preservedModules controls which modules stay as-is in the CSV (don't get rescanned)
// But all modules should be included in the final manifest
this.preservedModules = [...new Set([...preservedModules, ...selectedModules, ...installedModules])]; // Include all installed modules
this.preservedModules = allModules; // Include ALL modules (including custom)
this.bmadDir = bmadDir;
this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '_bmad' or 'bmad')
this.allInstalledFiles = installedFiles;
@ -454,33 +457,20 @@ class ManifestGenerator {
async writeMainManifest(cfgDir) {
const manifestPath = path.join(cfgDir, 'manifest.yaml');
// Read existing manifest to preserve custom modules
let existingCustomModules = [];
if (await fs.pathExists(manifestPath)) {
try {
const existingContent = await fs.readFile(manifestPath, 'utf8');
const existingManifest = yaml.parse(existingContent);
if (existingManifest && existingManifest.customModules) {
existingCustomModules = existingManifest.customModules;
}
} catch {
// If we can't read the existing manifest, continue without preserving custom modules
console.warn('Warning: Could not read existing manifest to preserve custom modules');
}
}
const manifest = {
installation: {
version: packageJson.version,
installDate: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
},
modules: this.modules,
customModules: existingCustomModules, // Preserve custom modules
modules: this.modules, // Include ALL modules (standard and custom)
ides: this.selectedIdes,
};
const yamlStr = yaml.stringify(manifest, {
// Clean the manifest to remove any non-serializable values
const cleanManifest = structuredClone(manifest);
const yamlStr = yaml.stringify(cleanManifest, {
indent: 2,
lineWidth: 0,
sortKeys: false,

View File

@ -28,7 +28,10 @@ class Manifest {
};
// Write YAML manifest
const yamlContent = yaml.stringify(manifestData, {
// Clean the manifest data to remove any non-serializable values
const cleanManifestData = structuredClone(manifestData);
const yamlContent = yaml.stringify(cleanManifestData, {
indent: 2,
lineWidth: 0,
sortKeys: false,
@ -59,8 +62,8 @@ class Manifest {
version: manifestData.installation?.version,
installDate: manifestData.installation?.installDate,
lastUpdated: manifestData.installation?.lastUpdated,
modules: manifestData.modules || [],
customModules: manifestData.customModules || [],
modules: manifestData.modules || [], // All modules (standard and custom)
customModules: manifestData.customModules || [], // Keep for backward compatibility
ides: manifestData.ides || [],
};
} catch (error) {
@ -92,15 +95,17 @@ class Manifest {
installDate: manifest.installDate,
lastUpdated: manifest.lastUpdated,
},
modules: manifest.modules || [],
customModules: manifest.customModules || [],
modules: manifest.modules || [], // All modules (standard and custom)
ides: manifest.ides || [],
};
const manifestPath = path.join(bmadDir, '_config', 'manifest.yaml');
await fs.ensureDir(path.dirname(manifestPath));
const yamlContent = yaml.stringify(manifestData, {
// Clean the manifest data to remove any non-serializable values
const cleanManifestData = structuredClone(manifestData);
const yamlContent = yaml.stringify(cleanManifestData, {
indent: 2,
lineWidth: 0,
sortKeys: false,

View File

@ -320,7 +320,10 @@ class CustomHandler {
if (await fs.pathExists(genericTemplatePath)) {
let templateContent = await fs.readFile(genericTemplatePath, 'utf8');
await fs.writeFile(customizePath, templateContent, 'utf8');
console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`));
// Only show customize creation in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`));
}
}
}
@ -341,11 +344,14 @@ class CustomHandler {
fileTrackingCallback(targetMdPath);
}
console.log(
chalk.dim(
` Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
),
);
// Only show compilation details in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(
chalk.dim(
` Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
),
);
}
} catch (error) {
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`);

View File

@ -34,7 +34,7 @@ class BaseIdeSetup {
* @returns {string} The activation header text
*/
async getAgentCommandHeader() {
const headerPath = path.join(getSourcePath(), 'src', 'utility', 'agent-components', 'agent-command-header.md');
const headerPath = getSourcePath('utility', 'agent-components', 'agent-command-header.md');
return await fs.readFile(headerPath, 'utf8');
}

View File

@ -45,12 +45,22 @@ class RooSetup extends BaseIdeSetup {
continue;
}
// Read the actual agent file from _bmad for metadata extraction (installed agents are .md files)
const agentPath = path.join(bmadDir, artifact.module, 'agents', `${artifact.name}.md`);
const content = await this.readFile(agentPath);
// artifact.sourcePath contains the full path to the agent file
if (!artifact.sourcePath) {
console.error(`Error: Missing sourcePath for artifact ${artifact.name} from module ${artifact.module}`);
console.error(`Artifact object:`, artifact);
throw new Error(`Missing sourcePath for agent: ${artifact.name}`);
}
const content = await this.readFile(artifact.sourcePath);
// Create command file that references the actual _bmad agent
await this.createCommandFile({ module: artifact.module, name: artifact.name, path: agentPath }, content, commandPath, projectDir);
await this.createCommandFile(
{ module: artifact.module, name: artifact.name, path: artifact.sourcePath },
content,
commandPath,
projectDir,
);
addedCount++;
console.log(chalk.green(` ✓ Added command: ${commandName}`));

View File

@ -29,6 +29,7 @@ class ModuleManager {
this.xmlHandler = new XmlHandler();
this.bmadFolderName = 'bmad'; // Default, can be overridden
this.scanProjectForModules = options.scanProjectForModules !== false; // Default to true for backward compatibility
this.customModulePaths = new Map(); // Initialize custom module paths
}
/**
@ -47,6 +48,14 @@ class ModuleManager {
this.coreConfig = coreConfig;
}
/**
* Set custom module paths for priority lookup
* @param {Map<string, string>} customModulePaths - Map of module ID to source path
*/
setCustomModulePaths(customModulePaths) {
this.customModulePaths = customModulePaths;
}
/**
* Copy a file and replace _bmad placeholder with actual folder name
* @param {string} sourcePath - Source file path
@ -334,66 +343,50 @@ class ModuleManager {
/**
* Find the source path for a module by searching all possible locations
* @param {string} moduleName - Name of the module to find
* @param {string} moduleCode - Code of the module to find (from module.yaml)
* @returns {string|null} Path to the module source or null if not found
*/
async findModuleSource(moduleName) {
async findModuleSource(moduleCode) {
const projectRoot = getProjectRoot();
// First, check src/modules
const srcModulePath = path.join(this.modulesSourcePath, moduleName);
if (await fs.pathExists(srcModulePath)) {
// Check if this looks like a module (has module.yaml)
const moduleConfigPath = path.join(srcModulePath, 'module.yaml');
const installerConfigPath = path.join(srcModulePath, '_module-installer', 'module.yaml');
if ((await fs.pathExists(moduleConfigPath)) || (await fs.pathExists(installerConfigPath))) {
return srcModulePath;
}
// Also check for custom.yaml in src/modules/_module-installer
const customConfigPath = path.join(srcModulePath, '_module-installer', 'custom.yaml');
if (await fs.pathExists(customConfigPath)) {
return srcModulePath;
}
// First check custom module paths if they exist
if (this.customModulePaths && this.customModulePaths.has(moduleCode)) {
return this.customModulePaths.get(moduleCode);
}
// If not found in src/modules, search the entire project
const allModulePaths = await this.findModulesInProject();
for (const modulePath of allModulePaths) {
if (path.basename(modulePath) === moduleName) {
return modulePath;
}
}
// Search in src/modules by READING module.yaml files to match by code
if (await fs.pathExists(this.modulesSourcePath)) {
const entries = await fs.readdir(this.modulesSourcePath, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const modulePath = path.join(this.modulesSourcePath, entry.name);
// Also check by module ID (not just folder name)
// Need to read configs to match by ID
for (const modulePath of allModulePaths) {
const moduleConfigPath = path.join(modulePath, 'module.yaml');
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
// Read module.yaml to get the code
const moduleConfigPath = path.join(modulePath, 'module.yaml');
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
let configPath = null;
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else if (await fs.pathExists(customConfigPath)) {
configPath = customConfigPath;
} else if (await fs.pathExists(rootCustomConfigPath)) {
configPath = rootCustomConfigPath;
}
if (configPath) {
try {
const configContent = await fs.readFile(configPath, 'utf8');
const config = yaml.parse(configContent);
if (config.code === moduleName) {
return modulePath;
let configPath = null;
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else if (await fs.pathExists(customConfigPath)) {
configPath = customConfigPath;
}
if (configPath) {
try {
const configContent = await fs.readFile(configPath, 'utf8');
const config = yaml.parse(configContent);
if (config.code === moduleCode) {
return modulePath;
}
} catch (error) {
// Continue to next module if parse fails
console.warn(`Warning: Failed to parse module config at ${configPath}: ${error.message}`);
}
}
} catch (error) {
throw new Error(`Failed to parse module.yaml at ${configPath}: ${error.message}`);
}
}
}
@ -403,7 +396,7 @@ class ModuleManager {
/**
* Install a module
* @param {string} moduleName - Name of the module to install
* @param {string} moduleName - Code of the module to install (from module.yaml)
* @param {string} bmadDir - Target bmad directory
* @param {Function} fileTrackingCallback - Optional callback to track installed files
* @param {Object} options - Additional installation options
@ -417,7 +410,10 @@ class ModuleManager {
// Check if source module exists
if (!sourcePath) {
throw new Error(`Module '${moduleName}' not found in any source location`);
// Provide a more user-friendly error message
throw new Error(
`Source for module '${moduleName}' is not available. It will be retained but cannot be updated without its source files.`,
);
}
// Check if this is a custom module and read its custom.yaml values
@ -451,7 +447,6 @@ class ModuleManager {
// Check if already installed
if (await fs.pathExists(targetPath)) {
console.log(chalk.yellow(`Module '${moduleName}' already installed, updating...`));
await fs.remove(targetPath);
}
@ -507,6 +502,10 @@ class ModuleManager {
} else {
// Selective update - preserve user modifications
await this.syncModule(sourcePath, targetPath);
// Recompile agents (#1133)
await this.compileModuleAgents(sourcePath, targetPath, moduleName, bmadDir);
await this.processAgentFiles(targetPath, moduleName);
}
return {
@ -819,7 +818,10 @@ class ModuleManager {
const genericTemplatePath = getSourcePath('utility', 'agent-components', 'agent.customize.template.yaml');
if (await fs.pathExists(genericTemplatePath)) {
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath);
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
// Only show customize creation in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
}
// Store original hash for modification detection
const crypto = require('node:crypto');
@ -841,7 +843,10 @@ class ModuleManager {
// Write back to manifest
const yaml = require('yaml');
const updatedContent = yaml.stringify(manifestData, {
// Clean the manifest data to remove any non-serializable values
const cleanManifestData = structuredClone(manifestData);
const updatedContent = yaml.stringify(cleanManifestData, {
indent: 2,
lineWidth: 0,
});
@ -905,9 +910,14 @@ class ModuleManager {
await fs.writeFile(targetMdPath, xml, 'utf8');
}
console.log(
chalk.dim(` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`),
);
// Only show compilation details in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
console.log(
chalk.dim(
` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
),
);
}
} catch (error) {
console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
}

View File

@ -346,6 +346,12 @@ async function compileAgent(yamlContent, answers = {}, agentName = '', targetPat
// Replace {bmad_memory} in XML content
let xml = await compileToXml(cleanYaml, agentName, targetPath);
// Ensure xml is a string before attempting replaceAll
if (typeof xml !== 'string') {
throw new TypeError('compileToXml did not return a string');
}
if (finalAnswers.bmad_memory) {
xml = xml.replaceAll('{bmad_memory}', finalAnswers.bmad_memory);
}

View File

@ -18,8 +18,6 @@ class UI {
*/
async promptInstall() {
CLIUtils.displayLogo();
const version = CLIUtils.getVersion();
CLIUtils.displaySection('BMAD™ Setup', `Build More, Architect Dreams v${version}`);
const confirmedDirectory = await this.getConfirmedDirectory();
@ -40,19 +38,22 @@ class UI {
let legacyBmadPath = null;
// First check for legacy .bmad folder (instead of _bmad)
const entries = await fs.readdir(confirmedDirectory, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name === '.bmad') {
hasLegacyBmadFolder = true;
legacyBmadPath = path.join(confirmedDirectory, '.bmad');
bmadDir = legacyBmadPath;
// Only check if directory exists
if (await fs.pathExists(confirmedDirectory)) {
const entries = await fs.readdir(confirmedDirectory, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory() && entry.name === '.bmad') {
hasLegacyBmadFolder = true;
legacyBmadPath = path.join(confirmedDirectory, '.bmad');
bmadDir = legacyBmadPath;
// Check if it has _cfg folder
const cfgPath = path.join(legacyBmadPath, '_cfg');
if (await fs.pathExists(cfgPath)) {
hasLegacyCfg = true;
// Check if it has _cfg folder
const cfgPath = path.join(legacyBmadPath, '_cfg');
if (await fs.pathExists(cfgPath)) {
hasLegacyCfg = true;
}
break;
}
break;
}
}
@ -130,6 +131,36 @@ class UI {
// Check if there's an existing BMAD installation (after any folder renames)
const hasExistingInstall = await fs.pathExists(bmadDir);
// Collect IDE tool selection early - we need this to know if we should ask about TTS
let toolSelection;
let agentVibesConfig = { enabled: false, alreadyInstalled: false };
let claudeCodeSelected = false;
if (!hasExistingInstall) {
// For new installations, collect IDE selection first
// We don't have modules yet, so pass empty array
toolSelection = await this.promptToolSelection(confirmedDirectory, []);
// Check if Claude Code was selected
claudeCodeSelected = toolSelection.ides && toolSelection.ides.includes('claude-code');
// If Claude Code was selected, ask about TTS
if (claudeCodeSelected) {
const { enableTts } = await inquirer.prompt([
{
type: 'confirm',
name: 'enableTts',
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
default: false,
},
]);
if (enableTts) {
agentVibesConfig = { enabled: true, alreadyInstalled: false };
}
}
}
// Always ask for custom content, but we'll handle it differently for new installs
let customContentConfig = { hasCustomContent: false };
if (hasExistingInstall) {
@ -146,19 +177,37 @@ class UI {
// Only show action menu if there's an existing installation
if (hasExistingInstall) {
// Get version information
const { existingInstall } = await this.getExistingInstallation(confirmedDirectory);
const packageJsonPath = path.join(__dirname, '../../../package.json');
const currentVersion = require(packageJsonPath).version;
const installedVersion = existingInstall.version || 'unknown';
// Build menu choices dynamically
const choices = [];
// Always show Quick Update first (allows refreshing installation even on same version)
if (installedVersion !== 'unknown') {
choices.push({
name: `Quick Update (v${installedVersion} → v${currentVersion})`,
value: 'quick-update',
});
}
// Common actions
choices.push(
{ name: 'Modify BMAD Installation', value: 'update' },
{ name: 'Add / Update Custom Content', value: 'add-custom' },
{ name: 'Rebuild Agents', value: 'compile' },
);
const promptResult = await inquirer.prompt([
{
type: 'list',
name: 'actionType',
message: 'What would you like to do?',
choices: [
{ name: 'Quick Update (Settings Preserved)', value: 'quick-update' },
{ name: 'Modify BMAD Installation (Confirm or change each setting)', value: 'update' },
{ name: 'Remove BMad Folder and Reinstall (Full clean install - BMad Customization Will Be Lost)', value: 'reinstall' },
{ name: 'Compile Agents (Quick rebuild of all agent .md files)', value: 'compile' },
{ name: 'Cancel', value: 'cancel' },
],
default: 'quick-update',
choices: choices,
default: choices[0].value, // Use the first option as default
},
]);
@ -175,6 +224,56 @@ class UI {
};
}
// Handle add custom content separately
if (actionType === 'add-custom') {
customContentConfig = await this.promptCustomContentSource();
// After adding custom content, continue to select additional modules
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
// Ask if user wants to add additional modules
const { wantsMoreModules } = await inquirer.prompt([
{
type: 'confirm',
name: 'wantsMoreModules',
message: 'Do you want to add any additional modules?',
default: false,
},
]);
let selectedModules = [];
if (wantsMoreModules) {
const moduleChoices = await this.getModuleChoices(installedModuleIds, customContentConfig);
selectedModules = await this.selectModules(moduleChoices);
// Process custom content selection
const selectedCustomContent = selectedModules.filter((mod) => mod.startsWith('__CUSTOM_CONTENT__'));
if (selectedCustomContent.length > 0) {
customContentConfig.selected = true;
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
// Convert to module IDs
const customContentModuleIds = [];
const customHandler = new CustomHandler();
for (const customFile of customContentConfig.selectedFiles) {
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo) {
customContentModuleIds.push(customInfo.id);
}
}
selectedModules = [...selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__')), ...customContentModuleIds];
}
}
return {
actionType: 'update',
directory: confirmedDirectory,
installCore: false, // Don't reinstall core
modules: selectedModules,
customContent: customContentConfig,
};
}
// Handle agent compilation separately
if (actionType === 'compile') {
return {
@ -183,86 +282,133 @@ class UI {
};
}
// Handle cancel
if (actionType === 'cancel') {
// If actionType === 'update', handle it with the new flow
// Return early with modify configuration
if (actionType === 'update') {
// Get existing installation info
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
const { changeModuleSelection } = await inquirer.prompt([
{
type: 'confirm',
name: 'changeModuleSelection',
message: 'Change which modules are installed?',
default: false,
},
]);
let selectedModules = [];
if (changeModuleSelection) {
// Show module selection with existing modules pre-selected
const moduleChoices = await this.getModuleChoices(new Set(installedModuleIds), { hasCustomContent: false });
selectedModules = await this.selectModules(moduleChoices, [...installedModuleIds]);
} else {
selectedModules = [...installedModuleIds];
}
// Get tool selection
const toolSelection = await this.promptToolSelection(confirmedDirectory, selectedModules);
// TTS configuration - ask right after tool selection (matches new install flow)
const hasClaudeCode = toolSelection.ides && toolSelection.ides.includes('claude-code');
let enableTts = false;
if (hasClaudeCode) {
const { enableTts: enable } = await inquirer.prompt([
{
type: 'confirm',
name: 'enableTts',
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
default: false,
},
]);
enableTts = enable;
}
// Core config with existing defaults (ask after TTS)
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
return {
actionType: 'cancel',
actionType: 'update',
directory: confirmedDirectory,
installCore: true,
modules: selectedModules,
ides: toolSelection.ides,
skipIde: toolSelection.skipIde,
coreConfig: coreConfig,
customContent: { hasCustomContent: false },
enableAgentVibes: enableTts,
agentVibesInstalled: false,
};
}
// Handle reinstall - DON'T return early, let it flow through configuration collection
// The installer will handle deletion when it sees actionType === 'reinstall'
// For now, just note that we're in reinstall mode and continue below
// If actionType === 'update' or 'reinstall', continue with normal flow below
}
// This section is only for new installations (update returns early above)
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
// Ask about official modules for new installations
const { wantsOfficialModules } = await inquirer.prompt([
{
type: 'confirm',
name: 'wantsOfficialModules',
message: 'Will you be installing any official modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
default: true,
},
]);
let selectedOfficialModules = [];
if (wantsOfficialModules) {
const moduleChoices = await this.getModuleChoices(installedModuleIds, { hasCustomContent: false });
selectedOfficialModules = await this.selectModules(moduleChoices);
}
// Ask about custom content
const { wantsCustomContent } = await inquirer.prompt([
{
type: 'confirm',
name: 'wantsCustomContent',
message: 'Will you be installing any locally stored custom content?',
default: false,
},
]);
if (wantsCustomContent) {
customContentConfig = await this.promptCustomContentSource();
}
// Store the selected modules for later
customContentConfig._selectedOfficialModules = selectedOfficialModules;
// Build the final list of selected modules
let selectedModules = customContentConfig._selectedOfficialModules || [];
// Add custom content modules if any were selected
if (customContentConfig && customContentConfig.selectedModuleIds) {
selectedModules = [...selectedModules, ...customContentConfig.selectedModuleIds];
}
// Remove core if it's in the list (it's always installed)
selectedModules = selectedModules.filter((m) => m !== 'core');
// Tool selection (already done for new installs at the beginning)
if (!toolSelection) {
toolSelection = await this.promptToolSelection(confirmedDirectory, selectedModules);
}
// Collect configurations for new installations
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
// Custom content will be handled during installation phase
// Store the custom content config for later use
if (customContentConfig._shouldAsk) {
delete customContentConfig._shouldAsk;
}
// Skip module selection during update/reinstall - keep existing modules
let selectedModules;
if (actionType === 'update' || actionType === 'reinstall') {
// Keep all existing installed modules during update/reinstall
selectedModules = [...installedModuleIds];
console.log(chalk.cyan('\n📦 Keeping existing modules: ') + selectedModules.join(', '));
} else {
// Only show module selection for new installs
const moduleChoices = await this.getModuleChoices(installedModuleIds, customContentConfig);
selectedModules = await this.selectModules(moduleChoices);
// Check which custom content items were selected
const selectedCustomContent = selectedModules.filter((mod) => mod.startsWith('__CUSTOM_CONTENT__'));
if (selectedCustomContent.length > 0) {
customContentConfig.selected = true;
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
// Convert custom content to module IDs for installation
const customContentModuleIds = [];
const customHandler = new CustomHandler();
for (const customFile of customContentConfig.selectedFiles) {
// Get the module info to extract the ID
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo) {
customContentModuleIds.push(customInfo.id);
}
}
// Filter out custom content markers and add module IDs
selectedModules = [...selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__')), ...customContentModuleIds];
} else if (customContentConfig.hasCustomContent) {
// User provided custom content but didn't select any
customContentConfig.selected = false;
customContentConfig.selectedFiles = [];
}
}
// Prompt for AgentVibes TTS integration
const agentVibesConfig = await this.promptAgentVibes(confirmedDirectory);
// Collect IDE tool selection AFTER configuration prompts (fixes Windows/PowerShell hang)
// This allows text-based prompts to complete before the checkbox prompt
const toolSelection = await this.promptToolSelection(confirmedDirectory, selectedModules);
// No more screen clearing - keep output flowing
// TTS already handled at the beginning for new installs
return {
actionType: actionType || 'update', // Preserve reinstall or update action
actionType: 'install',
directory: confirmedDirectory,
installCore: true, // Always install core
installCore: true,
modules: selectedModules,
// IDE selection collected after config, will be configured later
ides: toolSelection.ides,
skipIde: toolSelection.skipIde,
coreConfig: coreConfig, // Pass collected core config to installer
// Custom content configuration
coreConfig: coreConfig,
customContent: customContentConfig,
enableAgentVibes: agentVibesConfig.enabled,
agentVibesInstalled: agentVibesConfig.alreadyInstalled,
@ -352,8 +498,6 @@ class UI {
}
}
CLIUtils.displaySection('Tool Integration', 'Select AI coding assistants and IDEs to configure');
let answers;
let userConfirmedNoTools = false;
@ -365,7 +509,7 @@ class UI {
name: 'ides',
message: 'Select tools to configure:',
choices: ideChoices,
pageSize: 15,
pageSize: 30,
},
]);
@ -391,9 +535,8 @@ class UI {
]);
if (goBack) {
// Re-display the section header before looping back
// Re-display a message before looping back
console.log();
CLIUtils.displaySection('Tool Integration', 'Select AI coding assistants and IDEs to configure');
} else {
// User explicitly chose to proceed without tools
userConfirmedNoTools = true;
@ -483,69 +626,26 @@ class UI {
* @param {Object} result - Installation result
*/
showInstallSummary(result) {
CLIUtils.displaySection('Installation Complete', 'BMAD™ has been successfully installed');
const summary = [
`📁 Installation Path: ${result.path}`,
`📦 Modules Installed: ${result.modules?.length > 0 ? result.modules.join(', ') : 'core only'}`,
`🔧 Tools Configured: ${result.ides?.length > 0 ? result.ides.join(', ') : 'none'}`,
];
// Add AgentVibes TTS info if enabled
if (result.agentVibesEnabled) {
summary.push(`🎤 AgentVibes TTS: Enabled`);
}
CLIUtils.displayBox(summary.join('\n\n'), {
borderColor: 'green',
borderStyle: 'round',
});
// Display TTS injection details if present
if (result.ttsInjectedFiles && result.ttsInjectedFiles.length > 0) {
console.log('\n' + chalk.cyan.bold('═══════════════════════════════════════════════════'));
console.log(chalk.cyan.bold(' AgentVibes TTS Injection Summary'));
console.log(chalk.cyan.bold('═══════════════════════════════════════════════════\n'));
// Explain what TTS injection is
console.log(chalk.white.bold('What is TTS Injection?\n'));
console.log(chalk.dim(' TTS (Text-to-Speech) injection adds voice instructions to BMAD agents,'));
console.log(chalk.dim(' enabling them to speak their responses aloud using AgentVibes.\n'));
console.log(chalk.dim(' Example: When you activate the PM agent, it will greet you with'));
console.log(chalk.dim(' spoken audio like "Hey! I\'m your Project Manager. How can I help?"\n'));
console.log(chalk.green(`✅ TTS injection applied to ${result.ttsInjectedFiles.length} file(s):\n`));
// Group by type
const partyModeFiles = result.ttsInjectedFiles.filter((f) => f.type === 'party-mode');
const agentTTSFiles = result.ttsInjectedFiles.filter((f) => f.type === 'agent-tts');
if (partyModeFiles.length > 0) {
console.log(chalk.yellow(' Party Mode (multi-agent conversations):'));
for (const file of partyModeFiles) {
console.log(chalk.dim(`${file.path}`));
}
}
if (agentTTSFiles.length > 0) {
console.log(chalk.yellow(' Agent TTS (individual agent voices):'));
for (const file of agentTTSFiles) {
console.log(chalk.dim(`${file.path}`));
}
}
// Show backup info and restore command
console.log('\n' + chalk.white.bold('Backups & Recovery:\n'));
console.log(chalk.dim(' Pre-injection backups are stored in:'));
console.log(chalk.cyan(' ~/_bmad-tts-backups/\n'));
console.log(chalk.dim(' To restore original files (removes TTS instructions):'));
console.log(chalk.cyan(` bmad-tts-injector.sh --restore ${result.path}\n`));
console.log(chalk.cyan('💡 BMAD agents will now speak when activated!'));
console.log(chalk.dim(' Ensure AgentVibes is installed: https://agentvibes.org'));
}
// Clean, simple completion message
console.log('\n' + chalk.green.bold('✨ BMAD is ready to use!'));
// Show installation summary in a simple format
console.log(chalk.dim(`Installed to: ${result.path}`));
if (result.modules && result.modules.length > 0) {
console.log(chalk.dim(`Modules: ${result.modules.join(', ')}`));
}
if (result.agentVibesEnabled) {
console.log(chalk.dim(`TTS: Enabled`));
}
// TTS injection info (simplified)
if (result.ttsInjectedFiles && result.ttsInjectedFiles.length > 0) {
console.log(chalk.dim(`\n💡 TTS enabled for ${result.ttsInjectedFiles.length} agent(s)`));
console.log(chalk.dim(' Agents will now speak when using AgentVibes'));
}
console.log(chalk.yellow('\nThank you for helping test the early release version of the new BMad Core and BMad Method!'));
console.log(chalk.cyan('Stable Beta coming soon - please read the full README.md and linked documentation to get started!'));
}
/**
@ -575,8 +675,8 @@ class UI {
const { Installer } = require('../installers/lib/core/installer');
const detector = new Detector();
const installer = new Installer();
const bmadDir = await installer.findBmadDir(directory);
const existingInstall = await detector.detect(bmadDir);
const bmadDirResult = await installer.findBmadDir(directory);
const existingInstall = await detector.detect(bmadDirResult.bmadDir);
const installedModuleIds = new Set(existingInstall.modules.map((mod) => mod.id));
return { existingInstall, installedModuleIds };
@ -595,7 +695,9 @@ class UI {
// Now collect with existing values as defaults (false = don't skip loading, true = skip completion message)
await configCollector.collectModuleConfig('core', directory, false, true);
return configCollector.collectedConfig.core;
const coreConfig = configCollector.collectedConfig.core;
// Ensure we always have a core config object, even if empty
return coreConfig || {};
}
/**
@ -690,15 +792,14 @@ class UI {
* @param {Array} moduleChoices - Available module choices
* @returns {Array} Selected module IDs
*/
async selectModules(moduleChoices) {
CLIUtils.displaySection('Module Selection', 'Choose the BMAD modules to install');
async selectModules(moduleChoices, defaultSelections = []) {
const moduleAnswer = await inquirer.prompt([
{
type: 'checkbox',
name: 'modules',
message: 'Select modules to install:',
choices: moduleChoices,
default: defaultSelections,
},
]);
@ -745,12 +846,13 @@ class UI {
// Check for any bmad installation (any folder with _config/manifest.yaml)
const { Installer } = require('../installers/lib/core/installer');
const installer = new Installer();
const bmadDir = await installer.findBmadDir(directory);
const hasBmadInstall = (await fs.pathExists(bmadDir)) && (await fs.pathExists(path.join(bmadDir, '_config', 'manifest.yaml')));
const bmadResult = await installer.findBmadDir(directory);
const hasBmadInstall =
(await fs.pathExists(bmadResult.bmadDir)) && (await fs.pathExists(path.join(bmadResult.bmadDir, '_config', 'manifest.yaml')));
console.log(
chalk.gray(`Directory exists and contains ${files.length} item(s)`) +
(hasBmadInstall ? chalk.yellow(` including existing BMAD installation (${path.basename(bmadDir)})`) : ''),
(hasBmadInstall ? chalk.yellow(` including existing BMAD installation (${path.basename(bmadResult.bmadDir)})`) : ''),
);
} else {
console.log(chalk.gray('Directory exists and is empty'));
@ -1045,6 +1147,57 @@ class UI {
return (await fs.pathExists(hookPath)) && (await fs.pathExists(playTtsPath));
}
/**
* Load existing configurations to use as defaults
* @param {string} directory - Installation directory
* @returns {Object} Existing configurations
*/
async loadExistingConfigurations(directory) {
const configs = {
hasCustomContent: false,
coreConfig: {},
ideConfig: { ides: [], skipIde: false },
agentVibesConfig: { enabled: false, alreadyInstalled: false },
};
try {
// Load core config
configs.coreConfig = await this.collectCoreConfig(directory);
// Load IDE configuration
const configuredIdes = await this.getConfiguredIdes(directory);
if (configuredIdes.length > 0) {
configs.ideConfig.ides = configuredIdes;
configs.ideConfig.skipIde = false;
}
// Load AgentVibes configuration
const agentVibesInstalled = await this.checkAgentVibesInstalled(directory);
configs.agentVibesConfig = { enabled: agentVibesInstalled, alreadyInstalled: agentVibesInstalled };
return configs;
} catch {
// If loading fails, return empty configs
console.warn('Warning: Could not load existing configurations');
return configs;
}
}
/**
* Get configured IDEs from existing installation
* @param {string} directory - Installation directory
* @returns {Array} List of configured IDEs
*/
async getConfiguredIdes(directory) {
const { Detector } = require('../installers/lib/core/detector');
const { Installer } = require('../installers/lib/core/installer');
const detector = new Detector();
const installer = new Installer();
const bmadResult = await installer.findBmadDir(directory);
const existingInstall = await detector.detect(bmadResult.bmadDir);
return existingInstall.ides || [];
}
/**
* Prompt for custom content for existing installations
* @returns {Object} Custom content configuration
@ -1184,6 +1337,141 @@ class UI {
return { hasCustomContent: false };
}
}
/**
* Prompt user for custom content source location
* @returns {Object} Custom content configuration
*/
async promptCustomContentSource() {
const customContentConfig = { hasCustomContent: true, sources: [] };
// Keep asking for more sources until user is done
while (true) {
// First ask if user wants to add another module or continue
if (customContentConfig.sources.length > 0) {
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: 'Would you like to:',
choices: [
{ name: 'Add another custom module', value: 'add' },
{ name: 'Continue with installation', value: 'continue' },
],
default: 'continue',
},
]);
if (action === 'continue') {
break;
}
}
let sourcePath;
let isValid = false;
while (!isValid) {
const { path: inputPath } = await inquirer.prompt([
{
type: 'input',
name: 'path',
message: 'Enter the path to your custom content folder (or press Enter to cancel):',
validate: async (input) => {
// Allow empty input to cancel
if (!input || input.trim() === '') {
return true; // Allow empty to exit
}
try {
// Expand the path
const expandedPath = this.expandUserPath(input.trim());
// Check if path exists
if (!(await fs.pathExists(expandedPath))) {
return 'Path does not exist';
}
// Check if it's a directory
const stat = await fs.stat(expandedPath);
if (!stat.isDirectory()) {
return 'Path must be a directory';
}
// Check for module.yaml in the root
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
if (!(await fs.pathExists(moduleYamlPath))) {
return 'Directory must contain a module.yaml file in the root';
}
// Try to parse the module.yaml to get the module ID
try {
const yaml = require('yaml');
const content = await fs.readFile(moduleYamlPath, 'utf8');
const moduleData = yaml.parse(content);
if (!moduleData.code) {
return 'module.yaml must contain a "code" field for the module ID';
}
} catch (error) {
return 'Invalid module.yaml file: ' + error.message;
}
return true;
} catch (error) {
return 'Error validating path: ' + error.message;
}
},
},
]);
// If user pressed Enter without typing anything, exit the loop
if (!inputPath || inputPath.trim() === '') {
// If we have no modules yet, return false for no custom content
if (customContentConfig.sources.length === 0) {
return { hasCustomContent: false };
}
return customContentConfig;
}
sourcePath = this.expandUserPath(inputPath);
isValid = true;
}
// Read module.yaml to get module info
const yaml = require('yaml');
const moduleYamlPath = path.join(sourcePath, 'module.yaml');
const moduleContent = await fs.readFile(moduleYamlPath, 'utf8');
const moduleData = yaml.parse(moduleContent);
// Add to sources
customContentConfig.sources.push({
path: sourcePath,
id: moduleData.code,
name: moduleData.name || moduleData.code,
});
console.log(chalk.green(`✓ Confirmed local custom module: ${moduleData.name || moduleData.code}`));
}
// Ask if user wants to add these to the installation
const { shouldInstall } = await inquirer.prompt([
{
type: 'confirm',
name: 'shouldInstall',
message: `Install ${customContentConfig.sources.length} custom module(s) now?`,
default: true,
},
]);
if (shouldInstall) {
customContentConfig.selected = true;
// Store paths to module.yaml files, not directories
customContentConfig.selectedFiles = customContentConfig.sources.map((s) => path.join(s.path, 'module.yaml'));
// Also include module IDs for installation
customContentConfig.selectedModuleIds = customContentConfig.sources.map((s) => s.id);
}
return customContentConfig;
}
}
module.exports = { UI };