Compare commits

...

9 Commits

Author SHA1 Message Date
Mario Semper 3cc41490bb
Merge 4fd8b9018f into c24821b6ed 2025-12-16 10:50:04 +08:00
Brian Madison c24821b6ed menu wording updates 2025-12-16 01:25:49 +08:00
Brian Madison 2c4c2d9717 reduce installer log output 2025-12-15 23:53:26 +08: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
Murat K Ozcan 4fd8b9018f
Merge branch 'main' into feature/agent-preflight-protocol 2025-12-12 13:41:03 -06:00
Mario Semper 12e0840c62 feat: Agent Task Pre-Flight Protocol - Safety framework for high-risk tasks
Introduces mandatory safety checks for high-risk agent tasks
(marketing, legal, deployment) to prevent factual errors,
trademark violations, and privacy breaches.

Key features:
- Mandatory verification before high-risk outputs
- Cross-agent fact-checking
- Critical guidelines framework
- User approval gates

Origin: Lessons from tellingCube project
Prevents: Wrong pricing, trademark violations, privacy leaks, feature hallucinations

Includes: CRITICAL-GUIDELINES.md template and implementation examples
2025-11-23 02:24:24 +01: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
9 changed files with 1070 additions and 652 deletions

View File

@ -0,0 +1,383 @@
# BMad Method PR #2: Agent Task Pre-Flight Protocol
**Feature Type**: Safety & quality framework
**Status**: Draft for community review
**Origin**: tellingCube project learnings (masemIT e.U.)
**Author**: Mario Semper (@sempre)
**Date**: 2025-11-23
---
## Summary
**Agent Task Pre-Flight Protocol** establishes mandatory safety checks for high-risk agent tasks (marketing, legal, deployment) to prevent factual errors, trademark violations, privacy breaches, and assumption-based mistakes.
---
## Problem Statement
### Real-World Failure Case
**Scenario**: Marketing agent (Sophie) created LinkedIn launch posts for tellingCube without:
- Reading existing project documentation
- Verifying pricing against actual implementation
- Checking trademark compliance rules
- Reviewing privacy guidelines
**Result**: Multiple critical errors:
- ❌ Mentioned user's day job title (privacy/legal risk)
- ❌ Used family member's name (privacy violation)
- ❌ Claimed "60 seconds" generation time (factually wrong)
- ❌ Advertised "€9/month" pricing (doesn't exist - actual: €29-€999 ONE-TIME)
- ❌ Used "IBCS-compliant" (trademark violation - should be "inspired by IBCS©")
**Root Cause**: Agent operated independently without pre-task verification protocol.
---
## Current BMad Behavior (Risky)
```yaml
User: "Sophie, create LinkedIn launch posts"
Sophie:
1. Generates content based on general knowledge
2. Makes assumptions about features/pricing
3. Uses marketing best practices
4. Presents to user
❌ Problem: No verification step before creation
```
---
## Proposed Solution: Pre-Flight Protocol
### Mandatory Checks Before High-Risk Tasks
```yaml
Agent Task Pre-Flight Protocol:
BEFORE executing tasks with external impact:
1. Discover Critical Context
- Search for CRITICAL-GUIDELINES.md or similar
- Read recent related work in project
- Check actual implementation (code, configs, not assumptions)
2. Verify Assumptions
- Pricing: Read Stripe config / pricing components
- Features: Grep codebase for actual capabilities
- Legal/Trademark: Check documented compliance rules
- Privacy: Verify no personal info in public content
3. Cross-Agent Review (for high-risk outputs)
- Orchestrator reviews before user sees
- Fact-checker agent validates claims
- Minimum 2 agents verify before publishing
4. User Approval Gate
- Present content as DRAFT
- Highlight assumptions made
- Get explicit approval before finalizing
```
---
## High-Risk Task Categories
### 1. Marketing & Public Content
**Examples**: LinkedIn posts, press releases, demo videos, website copy
**Pre-Flight Required**:
- [ ] Read `CRITICAL-GUIDELINES.md` (legal, trademark, privacy rules)
- [ ] Verify pricing from actual Stripe/payment config
- [ ] Verify features from actual codebase (not roadmap ideas)
- [ ] Check trademark compliance (e.g., "IBCS©" usage rules)
- [ ] Privacy review (no personal identifiers without consent)
- [ ] Cross-agent fact-check before presenting to user
### 2. Legal & Compliance
**Examples**: Terms of service, privacy policy, license agreements
**Pre-Flight Required**:
- [ ] Read existing legal docs (don't start from scratch)
- [ ] Check jurisdiction-specific requirements
- [ ] Verify against actual product behavior (data handling, cookies, etc.)
- [ ] Legal expert review (human or specialized agent)
- [ ] User final approval required
### 3. Deployment & Infrastructure
**Examples**: Database migrations, production deployments, DNS changes
**Pre-Flight Required**:
- [ ] Read deployment runbooks/checklists
- [ ] Verify current production state
- [ ] Check for breaking changes
- [ ] Backup strategy confirmed
- [ ] Rollback plan documented
- [ ] User explicit approval with understanding of risks
### 4. Financial & Billing
**Examples**: Stripe configuration, pricing changes, refund policies
**Pre-Flight Required**:
- [ ] Read current Stripe dashboard state
- [ ] Verify tax/legal implications
- [ ] Check grandfather clause impacts
- [ ] Financial impact assessment
- [ ] User approval with revenue projections
---
## Implementation Guidelines
### For Agent Developers
**In agent YAML definition**:
```yaml
agent:
name: Sophie
id: marketing
high_risk_tasks: true # Triggers pre-flight protocol
pre_flight:
required_reads:
- docs/marketing/CRITICAL-GUIDELINES.md
- components/landing/PricingSection.tsx
- docs/_masemIT/readme.md
verification_steps:
- Grep for actual pricing tiers in codebase
- Check trademark compliance rules
- Privacy scan (no personal names/details)
cross_check:
agents: [river, mary]
approval_required: true
tasks:
create-linkedin-post:
pre_flight_mandatory: true
approval_gate: user
```
### For Orchestrators (River-like agents)
**Orchestrator responsibilities**:
```python
def execute_high_risk_task(agent, task, user_request):
# Step 1: Pre-flight checks
critical_docs = discover_critical_guidelines()
agent.read(critical_docs)
# Step 2: Agent executes with verification
draft_output = agent.execute_task(task)
# Step 3: Cross-agent review
fact_check_agent = get_agent("mary")
verification = fact_check_agent.verify(draft_output, codebase)
# Step 4: Present as DRAFT to user
if verification.has_issues:
present_issues_to_user(verification.issues)
present_as_draft(draft_output)
# Step 5: User approval gate
approval = get_user_approval()
if approval:
finalize(draft_output)
```
---
## Example: Correct Marketing Flow
### Before (Risky)
```
User: "Create LinkedIn launch posts"
Sophie: [Generates 3 posts with assumptions]
Sophie: "Here are your posts!"
❌ Contains errors user must catch
```
### After (Safe)
```
User: "Create LinkedIn launch posts"
River: "Sophie, this is a high-risk task. Running pre-flight..."
Sophie:
✅ Read CRITICAL-GUIDELINES.md
✅ Read PricingSection.tsx (actual pricing: €29-€999)
✅ Checked IBCS© compliance rules (must say "inspired by")
✅ Privacy check (no "Product Owner", no "brother")
Sophie: [Generates 3 posts with verified facts]
River: "Mary, fact-check Sophie's output..."
Mary:
✅ Pricing correct (€29-€999 lifetime)
✅ No trademark violations ("inspired by IBCS©")
✅ No privacy issues
✅ Generation time accurate ("minutes")
River: "Sempre, here's the DRAFT (pre-flight verified). Approve?"
User: [Reviews, approves]
✅ No errors, factually accurate
```
---
## Critical Guidelines Template
**Every project should have**: `docs/PROJECT-NAME/CRITICAL-GUIDELINES.md`
```markdown
# CRITICAL Guidelines for [Project Name]
## ❌ NEVER MENTION
- Confidential info (list specific items)
- Personal details (family, private life)
- Competitor names (if under NDA)
## ✅ ALWAYS VERIFY
- Pricing: Check [file path]
- Features: Grep [codebase location]
- Legal: Comply with [trademark/license rules]
## Trademark Compliance
- "IBCS©" → Always say "inspired by IBCS©" (not "compliant")
- [Other trademarks...]
## Privacy Rules
- No personal job titles in public content
- No family member names
- [Other privacy rules...]
## Approval Requirements
- Marketing content: River + Mary review
- Legal docs: Legal expert review
- Deployment: User explicit approval
```
---
## Benefits
**Prevents costly mistakes** - Catches errors before they're public
**Protects legal compliance** - Trademark, privacy, licensing
**Ensures factual accuracy** - Features/pricing match reality
**Builds user trust** - Agents don't hallucinate facts
**Scalable safety** - Works across all BMad projects
---
## Tradeoffs & Considerations
### Slower Task Execution
- **Before**: Agent outputs in 30 seconds
- **After**: Pre-flight adds 1-2 minutes
- **Worth it?**: YES for high-risk tasks (marketing, legal, deployment)
### More Agent Coordination
- Requires orchestrator (River) to manage pre-flight
- Cross-agent reviews add complexity
- **Mitigation**: Only for high-risk tasks, not every task
### User Approval Friction
- Adds approval gate before finalization
- **Mitigation**: Present as DRAFT with verification status
- User can fast-track if comfortable
---
## Rollout Strategy
### Phase 1: Opt-In (Recommended)
- Projects mark agents as `high_risk_tasks: true`
- Orchestrators enforce pre-flight for marked agents
- Community feedback on friction/benefits
### Phase 2: Default for Risky Categories
- Marketing, legal, deployment agents default to pre-flight
- Other agents opt-in if needed
### Phase 3: Configurable Per-Task
- Users set risk level per task
- `*create-post --risk high` triggers pre-flight
- `*create-post --risk low` skips for drafts
---
## Real-World Validation
**Origin Project**: tellingCube (masemIT e.U.)
**Failure Scenario**:
- Marketing agent created launch posts without verification
- 5 critical errors caught by user (should have been caught earlier)
- 30 minutes of rework to fix
**After Implementing Protocol**:
- CRITICAL-GUIDELINES.md created
- Pre-flight checklist enforced
- Cross-agent review (River → Sophie → Mary → User)
- **Result**: Zero errors in final content
**User Feedback (Mario Semper)**:
> "I love BMad, but I don't want to repeat the ChatGPT hallucination nightmare. This protocol gives me confidence that agents verify facts before presenting them."
---
## Open Questions for Community
1. **Scope**: Which task types should default to pre-flight?
2. **Performance**: Is 1-2 minute overhead acceptable for high-risk tasks?
3. **Configurability**: Per-project, per-agent, or per-task risk settings?
4. **Tooling**: Should pre-flight be a separate tool or built into agent execution?
5. **Enforcement**: Optional best practice or mandatory for certain agents?
---
## Next Steps
1. **Community feedback** on protocol design
2. **Reference implementation** in BMad core
3. **Agent template updates** to include pre-flight hooks
4. **Documentation** with examples for common scenarios
5. **Testing** across different project types
---
## Comparison to Similar Patterns
| Pattern | Focus | When to Use |
|---------|-------|-------------|
| **Pre-Flight Protocol** | Safety & accuracy | High-risk external outputs |
| **Code Review** | Code quality | Before merging code |
| **QA Gates** | Testing | Before production deployment |
| **Approval Workflows** | Governance | Multi-stakeholder decisions |
**Pre-Flight Protocol** = "Code review + QA gate" for **agent outputs**.
---
## References
- **Source Project**: tellingCube (https://github.com/masemIT/telling-cube) [if public]
- **Failure Case**: `docs/bmad-contributions/` (this document)
- **Implementation**: `docs/marketing/CRITICAL-GUIDELINES.md` (tellingCube)
---
**Contribution ready for review.** This came from painful real-world experience - let's make BMad safer for everyone! 🛡️

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

@ -21,15 +21,6 @@ module.exports = {
return;
}
// Handle agent compilation separately
if (config.actionType === 'compile') {
const result = await installer.compileAgents(config);
console.log(chalk.green('\n✨ Agent compilation complete!'));
console.log(chalk.cyan(`Rebuilt ${result.agentCount} agents and ${result.taskCount} tasks`));
process.exit(0);
return;
}
// Handle quick update separately
if (config.actionType === 'quick-update') {
const result = await installer.quickUpdate(config);

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

@ -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
}
@ -431,7 +431,41 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
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 {
// For regular updates (modify flow), check manifest for custom module sources
if (config._isUpdate && config._existingInstall && config._existingInstall.customModules) {
for (const customModule of config._existingInstall.customModules) {
// Ensure we have an absolute sourcePath
let absoluteSourcePath = customModule.sourcePath;
// Check if sourcePath is a cache-relative path (starts with _config)
if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) {
// Convert cache-relative path to absolute path
absoluteSourcePath = path.join(bmadDir, absoluteSourcePath);
}
// If no sourcePath but we have relativePath, convert it
else if (!absoluteSourcePath && customModule.relativePath) {
// relativePath is relative to the project root (parent of bmad dir)
absoluteSourcePath = path.resolve(projectDir, customModule.relativePath);
}
// Ensure sourcePath is absolute for anything else
else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
absoluteSourcePath = path.resolve(absoluteSourcePath);
}
if (absoluteSourcePath) {
customModulePaths.set(customModule.id, absoluteSourcePath);
}
}
}
// Build custom module paths map from customContent
// Handle selectedFiles (from existing install path or manual directory input)
@ -582,20 +616,39 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
const existingFilesManifest = await this.readFilesManifest(bmadDir);
console.log(chalk.dim(`DEBUG: Read ${existingFilesManifest.length} files from manifest`));
console.log(chalk.dim(`DEBUG: Manifest has hashes: ${existingFilesManifest.some((f) => f.hash)}`));
const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
console.log(chalk.dim(`DEBUG: Found ${customFiles.length} custom files, ${modifiedFiles.length} modified files`));
if (modifiedFiles.length > 0) {
console.log(chalk.yellow('DEBUG: Modified files:'));
for (const f of modifiedFiles) console.log(chalk.dim(` - ${f.path}`));
}
config._customFiles = customFiles;
config._modifiedFiles = modifiedFiles;
// Also check cache directory for custom modules (like quick update does)
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 (customModulePaths.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)) {
customModulePaths.set(moduleId, cachedPath);
}
}
}
// Update module manager with the new custom module paths from cache
this.moduleManager.setCustomModulePaths(customModulePaths);
}
// If there are custom files, back them up temporarily
if (customFiles.length > 0) {
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
@ -618,20 +671,16 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
await fs.ensureDir(tempModifiedBackupDir);
console.log(chalk.yellow(`\nDEBUG: Backing up ${modifiedFiles.length} modified files to temp location`));
spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
for (const modifiedFile of modifiedFiles) {
const relativePath = path.relative(bmadDir, modifiedFile.path);
const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
console.log(chalk.dim(`DEBUG: Backing up ${relativePath} to temp`));
await fs.ensureDir(path.dirname(tempBackupPath));
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
}
spinner.succeed(`Backed up ${modifiedFiles.length} modified files`);
config._tempModifiedBackupDir = tempModifiedBackupDir;
} else {
console.log(chalk.dim('DEBUG: No modified files detected'));
}
}
} else if (existingInstall.installed && config._quickUpdate) {
@ -647,6 +696,34 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
config._customFiles = customFiles;
config._modifiedFiles = modifiedFiles;
// Also check cache directory for custom modules (like quick update does)
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 (customModulePaths.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)) {
customModulePaths.set(moduleId, cachedPath);
}
}
}
// Update module manager with the new custom module paths from cache
this.moduleManager.setCustomModulePaths(customModulePaths);
}
// Back up custom files
if (customFiles.length > 0) {
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
@ -825,7 +902,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// 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({
scanProjectForModules: true,
bmadDir: bmadDir, // Pass bmadDir so we can check cache
});
@ -847,7 +923,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;
@ -900,103 +978,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: useCache ? `_config/custom/${customInfo.id}` : sourcePath,
installDate: new Date().toISOString(),
});
} else {
// Regular module installation
// Special case for core module
@ -1007,7 +1018,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)
@ -1029,69 +1040,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...');
@ -1104,12 +1053,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...');
@ -1133,19 +1080,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
customModules: config._customModulesToTrack || [], // Custom modules to exclude from regular modules list
});
// 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`,
@ -1186,7 +1126,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Pass pre-collected configuration to avoid re-prompting
await this.ideManager.setup(ide, projectDir, bmadDir, {
selectedModules: config.modules || [],
selectedModules: allModules || [],
preCollectedConfig: ideConfigurations[ide] || null,
verbose: config.verbose,
});
@ -1313,11 +1253,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Report custom and modified files if any were found
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 customFile of customFiles) {
const relativePath = path.relative(projectDir, customFile);
console.log(chalk.dim(`${relativePath}`));
}
}
if (modifiedFiles.length > 0) {
@ -1380,12 +1315,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...'));
@ -1578,7 +1545,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);
}
}
}
@ -1617,7 +1584,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
@ -1654,7 +1621,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 +1637,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 +1653,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);
}
}
}
@ -1702,7 +1669,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);
}
}
}
@ -1717,7 +1684,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);
}
}
}
@ -1816,7 +1783,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);
}
}
@ -2281,35 +2248,35 @@ 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 cache
const customModuleSources = new Map();
if (existingInstall.customModules) {
for (const customModule of existingInstall.customModules) {
// Ensure we have an absolute sourcePath
let absoluteSourcePath = customModule.sourcePath;
const cacheDir = path.join(bmadDir, '_config', 'custom');
if (await fs.pathExists(cacheDir)) {
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
// Check if sourcePath is a cache-relative path (starts with _config/)
if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) {
// Convert cache-relative path to absolute path
absoluteSourcePath = path.join(bmadDir, absoluteSourcePath);
}
// If no sourcePath but we have relativePath, convert it
else if (!absoluteSourcePath && customModule.relativePath) {
// relativePath is relative to the project root (parent of bmad dir)
absoluteSourcePath = path.resolve(projectRoot, customModule.relativePath);
}
// Ensure sourcePath is absolute for anything else
else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
absoluteSourcePath = path.resolve(absoluteSourcePath);
}
for (const cachedModule of cachedModules) {
if (cachedModule.isDirectory()) {
const moduleId = cachedModule.name;
// Update the custom module object with the absolute path
const updatedModule = {
...customModule,
sourcePath: absoluteSourcePath,
};
// Skip if we already have this module from manifest
if (customModuleSources.has(moduleId)) {
continue;
}
customModuleSources.set(customModule.id, updatedModule);
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
});
}
}
}
}
@ -2341,126 +2308,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
// Check for untracked custom modules (installed but not in manifest)
const untrackedCustomModules = [];
for (const installedModule of installedModules) {
// Skip standard modules and core
const standardModuleIds = ['bmb', 'bmgd', 'bmm', 'cis', 'core'];
if (standardModuleIds.includes(installedModule)) {
continue;
}
// Check if this installed module is not tracked in customModules
if (!customModuleSources.has(installedModule)) {
const modulePath = path.join(bmadDir, installedModule);
if (await fs.pathExists(modulePath)) {
untrackedCustomModules.push({
id: installedModule,
name: installedModule, // We don't have the original name
path: modulePath,
untracked: true,
});
}
}
}
// If we found untracked custom modules, offer to track them
if (untrackedCustomModules.length > 0) {
spinner.stop();
console.log(chalk.yellow(`\n⚠️ Found ${untrackedCustomModules.length} custom module(s) not tracked in manifest:`));
for (const untracked of untrackedCustomModules) {
console.log(chalk.dim(`${untracked.id} (installed at ${path.relative(projectRoot, untracked.path)})`));
}
const { trackModules } = await inquirer.prompt([
{
type: 'confirm',
name: 'trackModules',
message: chalk.cyan('Would you like to scan for their source locations?'),
default: true,
},
]);
if (trackModules) {
const { scanDirectory } = await inquirer.prompt([
{
type: 'input',
name: 'scanDirectory',
message: 'Enter directory to scan for custom module sources (or leave blank to skip):',
default: projectRoot,
validate: async (input) => {
if (input && input.trim() !== '') {
const expandedPath = path.resolve(input.trim());
if (!(await fs.pathExists(expandedPath))) {
return 'Directory does not exist';
}
const stats = await fs.stat(expandedPath);
if (!stats.isDirectory()) {
return 'Path must be a directory';
}
}
return true;
},
},
]);
if (scanDirectory && scanDirectory.trim() !== '') {
console.log(chalk.dim('\nScanning for custom module sources...'));
// Scan for all module.yaml files
const allModulePaths = await this.moduleManager.findModulesInProject(scanDirectory);
const { ModuleManager } = require('../modules/manager');
const mm = new ModuleManager({ scanProjectForModules: true });
for (const untracked of untrackedCustomModules) {
let foundSource = null;
// Try to find by module ID
for (const modulePath of allModulePaths) {
try {
const moduleInfo = await mm.getModuleInfo(modulePath);
if (moduleInfo && moduleInfo.id === untracked.id) {
foundSource = {
path: modulePath,
info: moduleInfo,
};
break;
}
} catch {
// Continue searching
}
}
if (foundSource) {
console.log(chalk.green(` ✓ Found source for ${untracked.id}: ${path.relative(projectRoot, foundSource.path)}`));
// Add to manifest
await this.manifest.addCustomModule(bmadDir, {
id: untracked.id,
name: foundSource.info.name || untracked.name,
sourcePath: path.resolve(foundSource.path),
installDate: new Date().toISOString(),
tracked: true,
});
// Add to customModuleSources for processing
customModuleSources.set(untracked.id, {
id: untracked.id,
name: foundSource.info.name || untracked.name,
sourcePath: path.resolve(foundSource.path),
});
} else {
console.log(chalk.yellow(` ⚠ Could not find source for ${untracked.id}`));
}
}
}
}
console.log(chalk.dim('\nUntracked custom modules will remain installed but cannot be updated without their source.'));
spinner.start('Preparing update...');
}
// Handle missing custom module sources using shared method
const customModuleResult = await this.handleMissingCustomSources(
customModuleSources,
@ -2478,18 +2325,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
hasUpdate: true,
}));
// Add untracked modules to the update list but mark them as untrackable
for (const untracked of untrackedCustomModules) {
if (!customModuleSources.has(untracked.id)) {
customModulesFromManifest.push({
...untracked,
isCustom: true,
hasUpdate: false, // Can't update without source
untracked: true,
});
}
}
const allAvailableModules = [...availableModules, ...customModulesFromManifest];
const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
@ -2762,14 +2597,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,
});
}
}
@ -2821,7 +2652,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;
}
@ -2844,8 +2674,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 {
@ -2975,7 +2803,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++;
}
@ -3065,13 +2893,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

@ -34,24 +34,21 @@ class ManifestGenerator {
// Store modules list (all modules including preserved ones)
const preservedModules = options.preservedModules || [];
const customModules = options.customModules || [];
// Scan the bmad directory to find all actually installed modules
const installedModules = await this.scanInstalledModules(bmadDir);
// Filter out custom modules from the regular modules list
const customModuleIds = new Set(customModules.map((cm) => cm.id));
const regularModules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])].filter(
(module) => !customModuleIds.has(module),
);
// 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 = regularModules;
this.updatedModules = [...new Set(['core', ...selectedModules, ...installedModules])].filter((module) => !customModuleIds.has(module)); // Also exclude custom modules from rescanning
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;
@ -460,29 +457,13 @@ 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,
};

View File

@ -62,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) {
@ -95,8 +95,7 @@ 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 || [],
};

View File

@ -28,7 +28,6 @@ class ModuleManager {
this.modulesSourcePath = getSourcePath('modules');
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
}
@ -116,76 +115,6 @@ class ModuleManager {
}
}
/**
* Find all modules in the project by searching for module.yaml files
* @returns {Array} List of module paths
*/
async findModulesInProject() {
const projectRoot = getProjectRoot();
const modulePaths = new Set();
// Helper function to recursively scan directories
async function scanDirectory(dir, excludePaths = []) {
try {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
// Skip hidden directories, node_modules, and literal placeholder directories
if (
entry.name.startsWith('.') ||
entry.name === 'node_modules' ||
entry.name === 'dist' ||
entry.name === 'build' ||
entry.name === '{project-root}'
) {
continue;
}
// Skip excluded paths
if (excludePaths.some((exclude) => fullPath.startsWith(exclude))) {
continue;
}
if (entry.isDirectory()) {
// Skip core module - it's always installed first and not selectable
if (entry.name === 'core') {
continue;
}
// Check if this directory contains a module (module.yaml OR custom.yaml)
const moduleConfigPath = path.join(fullPath, 'module.yaml');
const installerConfigPath = path.join(fullPath, '_module-installer', 'module.yaml');
const customConfigPath = path.join(fullPath, '_module-installer', 'custom.yaml');
const rootCustomConfigPath = path.join(fullPath, 'custom.yaml');
if (
(await fs.pathExists(moduleConfigPath)) ||
(await fs.pathExists(installerConfigPath)) ||
(await fs.pathExists(customConfigPath)) ||
(await fs.pathExists(rootCustomConfigPath))
) {
modulePaths.add(fullPath);
// Don't scan inside modules - they might have their own nested structures
continue;
}
// Recursively scan subdirectories
await scanDirectory(fullPath, excludePaths);
}
}
} catch {
// Ignore errors (e.g., permission denied)
}
}
// Scan the entire project, but exclude src/modules since we handle it separately
await scanDirectory(projectRoot, [this.modulesSourcePath]);
return [...modulePaths];
}
/**
* List all available modules (excluding core which is always installed)
* @returns {Object} Object with modules array and customModules array
@ -228,43 +157,19 @@ class ModuleManager {
}
}
// Then, find all other modules in the project (only if scanning is enabled)
if (this.scanProjectForModules) {
const otherModulePaths = await this.findModulesInProject();
for (const modulePath of otherModulePaths) {
const moduleName = path.basename(modulePath);
const relativePath = path.relative(getProjectRoot(), modulePath);
// Skip core module - it's always installed first and not selectable
if (moduleName === 'core') {
continue;
}
const moduleInfo = await this.getModuleInfo(modulePath, moduleName, relativePath);
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id) && !customModules.some((m) => m.id === moduleInfo.id)) {
// Avoid duplicates - skip if we already have this module ID
if (moduleInfo.isCustom) {
customModules.push(moduleInfo);
} else {
modules.push(moduleInfo);
}
}
}
// Also check for cached custom modules in _config/custom/
if (this.bmadDir) {
const customCacheDir = path.join(this.bmadDir, '_config', 'custom');
if (await fs.pathExists(customCacheDir)) {
const cacheEntries = await fs.readdir(customCacheDir, { withFileTypes: true });
for (const entry of cacheEntries) {
if (entry.isDirectory()) {
const cachePath = path.join(customCacheDir, entry.name);
const moduleInfo = await this.getModuleInfo(cachePath, entry.name, '_config/custom');
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id) && !customModules.some((m) => m.id === moduleInfo.id)) {
moduleInfo.isCustom = true;
moduleInfo.fromCache = true;
customModules.push(moduleInfo);
}
// Check for cached custom modules in _config/custom/
if (this.bmadDir) {
const customCacheDir = path.join(this.bmadDir, '_config', 'custom');
if (await fs.pathExists(customCacheDir)) {
const cacheEntries = await fs.readdir(customCacheDir, { withFileTypes: true });
for (const entry of cacheEntries) {
if (entry.isDirectory()) {
const cachePath = path.join(customCacheDir, entry.name);
const moduleInfo = await this.getModuleInfo(cachePath, entry.name, '_config/custom');
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id) && !customModules.some((m) => m.id === moduleInfo.id)) {
moduleInfo.isCustom = true;
moduleInfo.fromCache = true;
customModules.push(moduleInfo);
}
}
}
@ -343,71 +248,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 custom module paths if they exist
if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
return this.customModulePaths.get(moduleName);
if (this.customModulePaths && this.customModulePaths.has(moduleCode)) {
return this.customModulePaths.get(moduleCode);
}
// 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');
// 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);
if ((await fs.pathExists(moduleConfigPath)) || (await fs.pathExists(installerConfigPath))) {
return srcModulePath;
}
// 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');
// 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;
}
}
// 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;
}
}
// 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');
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}`);
}
}
}
@ -417,7 +301,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
@ -431,7 +315,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
@ -465,7 +352,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);
}

View File

@ -195,11 +195,7 @@ class UI {
}
// Common actions
choices.push(
{ name: 'Modify BMAD Installation', value: 'update' },
{ name: 'Add / Update Custom Content', value: 'add-custom' },
{ name: 'Rebuild Agents', value: 'compile' },
);
choices.push({ name: 'Modify BMAD Installation', value: 'update' });
const promptResult = await inquirer.prompt([
{
@ -224,64 +220,6 @@ 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 {
actionType: 'compile',
directory: confirmedDirectory,
};
}
// If actionType === 'update', handle it with the new flow
// Return early with modify configuration
if (actionType === 'update') {
@ -293,7 +231,7 @@ class UI {
{
type: 'confirm',
name: 'changeModuleSelection',
message: 'Change which modules are installed?',
message: 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?',
default: false,
},
]);
@ -307,6 +245,14 @@ class UI {
selectedModules = [...installedModuleIds];
}
// After module selection, ask about custom modules
const customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules);
// Merge any selected custom modules
if (customModuleResult.selectedCustomModules.length > 0) {
selectedModules.push(...customModuleResult.selectedCustomModules);
}
// Get tool selection
const toolSelection = await this.promptToolSelection(confirmedDirectory, selectedModules);
@ -337,7 +283,7 @@ class UI {
ides: toolSelection.ides,
skipIde: toolSelection.skipIde,
coreConfig: coreConfig,
customContent: { hasCustomContent: false },
customContent: customModuleResult.customContentConfig,
enableAgentVibes: enableTts,
agentVibesInstalled: false,
};
@ -352,7 +298,7 @@ class UI {
{
type: 'confirm',
name: 'wantsOfficialModules',
message: 'Will you be installing any official modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
message: 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
default: true,
},
]);
@ -368,7 +314,7 @@ class UI {
{
type: 'confirm',
name: 'wantsCustomContent',
message: 'Will you be installing any locally stored custom content?',
message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
default: false,
},
]);
@ -734,14 +680,8 @@ class UI {
// Add official modules
const { ModuleManager } = require('../installers/lib/modules/manager');
// For new installations, don't scan project yet (will do after custom content is discovered)
// For existing installations, scan if user selected custom content
const shouldScanProject =
!isNewInstallation && customContentConfig && customContentConfig.hasCustomContent && customContentConfig.selected;
const moduleManager = new ModuleManager({
scanProjectForModules: shouldScanProject,
});
const { modules: availableModules, customModules: customModulesFromProject } = await moduleManager.listAvailable();
const moduleManager = new ModuleManager();
const { modules: availableModules, customModules: customModulesFromCache } = await moduleManager.listAvailable();
// First, add all items to appropriate sections
const allCustomModules = [];
@ -749,14 +689,14 @@ class UI {
// Add custom content items from directory
allCustomModules.push(...customContentItems);
// Add custom modules from project scan (if scanning is enabled)
for (const mod of customModulesFromProject) {
// Add custom modules from cache
for (const mod of customModulesFromCache) {
// Skip if this module is already in customContentItems (by path)
const isDuplicate = allCustomModules.some((item) => item.path && mod.path && path.resolve(item.path) === path.resolve(mod.path));
if (!isDuplicate) {
allCustomModules.push({
name: `${chalk.cyan('✓')} ${mod.name} ${chalk.gray(`(${mod.source})`)}`,
name: `${chalk.cyan('✓')} ${mod.name} ${chalk.gray(`(cached)`)}`,
value: mod.id,
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
});
@ -803,7 +743,9 @@ class UI {
},
]);
return moduleAnswer.modules || [];
const selected = moduleAnswer.modules || [];
return selected;
}
/**
@ -1472,6 +1414,136 @@ class UI {
return customContentConfig;
}
/**
* Handle custom modules in the modify flow
* @param {string} directory - Installation directory
* @param {Array} selectedModules - Currently selected modules
* @returns {Object} Result with selected custom modules and custom content config
*/
async handleCustomModulesInModifyFlow(directory, selectedModules) {
// Get existing installation to find custom modules
const { existingInstall } = await this.getExistingInstallation(directory);
// Check if there are any custom modules in cache
const { Installer } = require('../installers/lib/core/installer');
const installer = new Installer();
const { bmadDir } = await installer.findBmadDir(directory);
const cacheDir = path.join(bmadDir, '_config', 'custom');
const cachedCustomModules = [];
if (await fs.pathExists(cacheDir)) {
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isDirectory()) {
const moduleYamlPath = path.join(cacheDir, entry.name, 'module.yaml');
if (await fs.pathExists(moduleYamlPath)) {
const yaml = require('yaml');
const content = await fs.readFile(moduleYamlPath, 'utf8');
const moduleData = yaml.parse(content);
cachedCustomModules.push({
id: entry.name,
name: moduleData.name || entry.name,
description: moduleData.description || 'Custom module from cache',
checked: selectedModules.includes(entry.name),
fromCache: true,
});
}
}
}
}
const result = {
selectedCustomModules: [],
customContentConfig: { hasCustomContent: false },
};
if (cachedCustomModules.length === 0) {
return result;
}
// Ask user about custom modules
console.log(chalk.cyan('\n⚙ Custom Modules'));
console.log(chalk.dim('Found custom modules in your installation:'));
const { customAction } = await inquirer.prompt([
{
type: 'list',
name: 'customAction',
message: 'What would you like to do with custom modules?',
choices: [
{ name: 'Keep all existing custom modules', value: 'keep' },
{ name: 'Select which custom modules to keep', value: 'select' },
{ name: 'Add new custom modules', value: 'add' },
{ name: 'Remove all custom modules', value: 'remove' },
],
default: 'keep',
},
]);
switch (customAction) {
case 'keep': {
// Keep all existing custom modules
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
console.log(chalk.dim(`Keeping ${result.selectedCustomModules.length} custom module(s)`));
break;
}
case 'select': {
// Let user choose which to keep
const choices = cachedCustomModules.map((m) => ({
name: `${m.name} ${chalk.gray(`(${m.id})`)}`,
value: m.id,
}));
const { keepModules } = await inquirer.prompt([
{
type: 'checkbox',
name: 'keepModules',
message: 'Select custom modules to keep:',
choices: choices,
default: cachedCustomModules.filter((m) => m.checked).map((m) => m.id),
},
]);
result.selectedCustomModules = keepModules;
break;
}
case 'add': {
// First ask to keep existing ones
const { keepExisting } = await inquirer.prompt([
{
type: 'confirm',
name: 'keepExisting',
message: 'Keep existing custom modules?',
default: true,
},
]);
if (keepExisting) {
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
}
// Then prompt for new ones (reuse existing method)
const newCustomContent = await this.promptCustomContentSource();
if (newCustomContent.hasCustomContent && newCustomContent.selected) {
result.selectedCustomModules.push(...newCustomContent.selectedModuleIds);
result.customContentConfig = newCustomContent;
}
break;
}
case 'remove': {
// Remove all custom modules
console.log(chalk.yellow('All custom modules will be removed from the installation'));
break;
}
}
return result;
}
}
module.exports = { UI };