Compare commits

...

15 Commits

Author SHA1 Message Date
Scott Jennings c0a49bcafe fix: use bmad_folder variable instead of hardcoded .bmad path
Allows users to customize the config location via bmad_folder setting.
2025-12-08 11:22:54 -06:00
Scott Jennings dcba8e5e59 feat: simplify external agent config and extract prompt to separate file
- Add installer prompt for external review agent selection (Codex, Gemini, Claude, None)
- Extract adversarial review prompt to external-agent-prompt.md for easier maintenance
- Simplify detection logic: check for "none" first, then verify CLI availability
- Add read-only sandbox flag to Codex invocation for safety
- Update workflow.yaml to reference new config variable
2025-12-08 10:57:37 -06:00
Scott Jennings 7509b0cbc2 feat: add external agent support for code reviews
Adds support for delegating adversarial code reviews to external CLI agents
(Codex, Gemini, or Claude) when available. This provides independent, unbiased
code reviews from a different AI model.

Changes:
- Add invoke-bash and set-var tags to workflow.xml execution engine
- Add external_review_agents configuration to install-config.yaml
- Rewrite code-review workflow to detect and invoke external agents
- Cache agent detection in config.yaml to avoid repeated CLI checks
- Add fallback to built-in review if external agents unavailable/fail
- Update checklist to reflect new external agent workflow

External agent invocation:
- Codex: codex exec --full-auto "prompt"
- Gemini: gemini -p "prompt" --yolo
- Claude: claude -p "prompt" --dangerously-skip-permissions
2025-12-08 10:36:39 -06:00
Brian Madison 55cb4681bc party mode and brainstorming had bmm config instead of core config listed causing loading error when bmm is not an installed module - fixed. 2025-12-08 08:11:39 -06:00
Brian Madison eb4325fab9 restore bmm as default selected module. 2025-12-08 08:04:39 -06:00
Brian Madison 57ceaf9fa9 conflict marker removed from docs 2025-12-08 08:01:00 -06:00
OhSeungWan 1513b2d478
fix: collect module.yaml prompts for custom modules (#1065)
Custom modules with module.yaml configuration prompts were not being
collected during installation. Added customModulePaths option to
ConfigCollector to resolve custom module paths from selectedFiles
and cachedModules sources.
2025-12-08 07:33:53 -06:00
Brian Madison 2da016f797 chore: bump version to alpha.15
- Module installation standardization with module.yaml
- Enhanced custom content installation with interactive search
- Added CodeRabbit AI and Raven's Verdict integrations
- Documentation improvements and cleanup
- Breaking change: _module-installer/install-config.yaml → module.yaml
2025-12-07 22:16:42 -06:00
Brian Madison 6947851393 module updates 2025-12-07 22:00:52 -06:00
Brian Madison 9d7b09d065 bmad_folder replacement working properly with custom and defauly modules 2025-12-07 21:58:44 -06:00
Brian Madison 86f2786dde remove hardcoded .bmad folders from demo content 2025-12-07 21:41:37 -06:00
Brian Madison a638f062b9 some debug output when installer errors 2025-12-07 21:03:05 -06:00
Brian Madison 738237b4ae custom install module cached 2025-12-07 20:46:09 -06:00
Brian Madison 6430173738 all modules custom or core use the same installer and have consistent behavior now. 2025-12-07 17:17:50 -06:00
Brian Madison baaa984a90 almost working installer updates 2025-12-07 15:38:49 -06:00
77 changed files with 2565 additions and 1083 deletions

View File

@ -1,5 +1,75 @@
# Changelog
## [6.0.0-alpha.15]
**Release: December 7, 2025**
### 🔧 Module Installation Standardization
**Unified Module Configuration:**
- **module.yaml Standard**: All modules now use `module.yaml` instead of `_module-installer/install-config.yaml` for consistent configuration (BREAKING CHANGE)
- **Universal Installer**: Both core and custom modules now use the same installer with consistent behavior
- **Streamlined Module Creation**: Module builder templates updated to use new module.yaml standard
- **Enhanced Module Discovery**: Improved module caching and discovery mechanisms
**Custom Content Installation Revolution:**
- **Interactive Custom Content Search**: Installer now proactively asks if you have custom content to install
- **Flexible Location Specification**: Users can indicate custom content location during installation
- **Improved Custom Module Handler**: Enhanced error handling and debug output for custom installations
- **Comprehensive Documentation**: New custom-content-installation.md guide (245 lines) replacing custom-agent-installation.md
### 🤖 Code Review Integration Expansion
**AI Review Tools:**
- **CodeRabbit AI Integration**: Added .coderabbit.yaml configuration for automated code review
- **Raven's Verdict PR Review Tool**: New PR review automation tool (297 lines of documentation)
- **Review Path Configuration**: Proper exclusion patterns for node_modules and generated files
- **Review Documentation**: Comprehensive usage guidance and skip conditions for PRs
### 📚 Documentation Improvements
**Documentation Restructuring:**
- **Code of Conduct**: Moved to .github/ folder following GitHub standards
- **Gem Creation Link**: Updated to point to Gemini Gem manager instead of deprecated interface
- **Example Custom Content**: Improved README files and disabled example modules to prevent accidental installation
- **Custom Module Documentation**: Enhanced module installation guides with new YAML structure
### 🧹 Cleanup & Optimization
**Memory Management:**
- **Removed Hardcoded .bmad Folders**: Cleaned up demo content to use configurable paths
- **Sidecar File Cleanup**: Removed old .bmad-user-memory folders from wellness modules
- **Example Content Organization**: Better organization of example-custom-content directory
**Installer Improvements:**
- **Debug Output Enhancement**: Added informative debug output when installer encounters errors
- **Custom Module Caching**: Improved caching mechanism for custom module installations
- **Consistent Behavior**: All modules now behave consistently regardless of custom or core status
### 📊 Statistics
- **77 files changed** with 2,852 additions and 607 deletions
- **15 commits** since alpha.14
### ⚠️ Breaking Changes
1. **module.yaml Configuration**: All modules must now use `module.yaml` instead of `_module-installer/install-config.yaml`
- Core modules updated automatically
- Custom modules will need to rename their configuration file
- Module builder templates generate new format
### 📦 New Dependencies
- No new dependencies added in this release
---
## [6.0.0-alpha.14]
**Release: December 7, 2025**
@ -101,159 +171,29 @@
### 🏗️ Revolutionary Workflow Architecture
**Granular Step-File Workflow System (NEW in alpha.13):**
- **Multi-Menu Support**: Workflows now support granular step-file architecture with dynamic menu generation
- **Sharded Workflows**: Complete conversion of Phase 1 and 2 workflows to stepwise sharded architecture
- **Improved Performance**: Reduced file loading times and eliminated time-based estimates throughout
- **Workflow Builder**: New dedicated workflow builder for creating stepwise workflows
- **PRD Workflow**: First completely reworked sharded workflow resolving Sonnet compatibility issues
**Core Workflow Transformations:**
- Phase 1 and 2 workflows completely converted to sharded step-flow architecture
- UX Design workflow converted to sharded step workflow
- Brainstorming, Research, and Party Mode updated to use sharded step-flow workflows
- Architecture workflows enhanced with step sharding and performance improvements
### 🎯 Code Review & Development Enhancement
**Advanced Code Review System:**
- **Adversarial Code Review**: Quick-dev workflow now recommends adversarial review approach for higher quality
- **Multi-LLM Strategy**: Dev-story workflow recommends different LLM models for code review tasks
- **Agent Compiler Optimization**: Complete handler cleanup and performance improvements
- **Step-File System**: Complete conversion to granular step-file architecture with dynamic menu generation
- **Phase 4 Transformation**: Simplified architecture with sprint planning integration (Jira, Linear, Trello)
- **Performance Improvements**: Eliminated time-based estimates, reduced file loading times
- **Legacy Cleanup**: Removed all deprecated workflows for cleaner system
### 🤖 Agent System Revolution
**Universal Custom Agent Support:**
- **Universal Custom Agent Support**: Extended to ALL IDEs including Antigravity and Rovo Dev
- **Agent Creation Workflow**: Enhanced with better documentation and parameter clarity
- **Multi-Source Discovery**: Agents now check multiple source locations for better discovery
- **GitHub Migration**: Integration moved from chatmodes to agents folder
- **Complete IDE Coverage**: Custom agent support extended to ALL remaining IDEs
- **Antigravity IDE Integration**: Added custom agent support with proper gitignore configuration
- **Multiple Source Locations**: Compile agents now checks multiple source locations for better discovery
- **Persona Name Display**: Fixed proper persona names display in custom agent manifests
- **New IDE Support**: Added support for Rovo Dev IDE
### 🧪 Testing Infrastructure
**Agent Creation & Management:**
- **Improved Creation Workflow**: Enhanced agent creation workflow with better documentation
- **Parameter Clarity**: Renamed agent-install parameters for better understanding
- **Menu Organization**: BMad Agents menu items logically ordered with optional/recommended/required tags
- **GitHub Migration**: GitHub integration now uses agents folder instead of chatmodes
### 🔧 Phase 4 & Sprint Evolution
**Complete Phase 4 Transformation:**
- **Simplified Architecture**: Phase 4 workflows completely transformed - simpler, faster, better results
- **Sprint Planning Integration**: Unified sprint planning with placeholders for Jira, Linear, and Trello integration
- **Status Management**: Better status loading and updating for Phase 4 artifacts
- **Workflow Reduction**: Phase 4 streamlined to single sprint planning item with clear validation
- **Dynamic Workflows**: All Level 1-3 workflows now dynamically suggest next steps based on context
### 🧪 Testing Infrastructure Expansion
**Playwright Utils Integration:**
- Test Architect now supports `@seontechnologies/playwright-utils` integration
- Installation prompt with `use_playwright_utils` configuration flag
- 11 comprehensive knowledge fragments covering ALL utilities
- Adaptive workflow recommendations across 6 testing workflows
- Production-ready utilities from SEON Technologies integrated with TEA patterns
**Testing Environment:**
- **Web Bundle Support**: Enabled web bundles for test and development environments
- **Test Architecture**: Enhanced test design for architecture level (Phase 3) testing
### 📦 Installation & Configuration
**Installer Improvements:**
- **Cleanup Options**: Installer now allows cleanup of unneeded files during upgrades
- **Username Default**: Installer now defaults to system username for better UX
- **IDE Selection**: Added empty IDE selection warning and promoted Antigravity to recommended
- **NPM Vulnerabilities**: Resolved all npm vulnerabilities for enhanced security
- **Documentation Installation**: Made documentation installation optional to reduce footprint
**Text-to-Speech from AgentVibes optional Integration:**
- **TTS_INJECTION System**: Complete text-to-speech integration via injection system
- **Agent Vibes**: Enhanced with TTS capabilities for voice feedback
### 🛠️ Tool & IDE Updates
**IDE Tool Enhancements:**
- **GitHub Copilot**: Fixed tool names consistency across workflows
- **KiloCode Integration**: Gave kilocode tool proper access to bmad modes
- **Code Quality**: Added radix parameter to parseInt() calls for better reliability
- **Agent Menu Optimization**: Improved agent performance in Claude Code slash commands
### 📚 Documentation & Standards
**Documentation Cleanup:**
- **Installation Guide**: Removed fluff and updated with npx support
- **Workflow Documentation**: Fixed documentation by removing non-existent workflows and Mermaid diagrams
- **Phase Numbering**: Fixed phase numbering consistency throughout documentation
- **Package References**: Corrected incorrect npm package references
**Workflow Compliance:**
- **Validation Checks**: Enhanced workflow validation checks for compliance
- **Product Brief**: Updated to comply with documented workflow standards
- **Status Integration**: Workflow-status can now call workflow-init for better integration
### 🔍 Legacy Workflow Cleanup
**Deprecated Workflows Removed:**
- **Audit Workflow**: Completely removed audit workflow and all associated files
- **Convert Legacy**: Removed legacy conversion utilities
- **Create/Edit Workflows**: Removed old workflow creation and editing workflows
- **Clean Architecture**: Simplified workflow structure by removing deprecated legacy workflows
### 🐛 Technical Fixes
**System Improvements:**
- **File Path Handling**: Fixed various file path issues across workflows
- **Manifest Updates**: Updated manifest to use agents folder structure
- **Web Bundle Configuration**: Fixed web bundle configurations for better compatibility
- **CSV Column Mismatch**: Fixed manifest schema upgrade issues
- **Playwright Utils Integration**: @seontechnologies/playwright-utils across all testing workflows
- **TTS Injection System**: Complete text-to-speech integration for voice feedback
- **Web Bundle Test Support**: Enabled web bundles for test environments
### ⚠️ Breaking Changes
**Workflow Architecture:**
- All legacy workflows have been removed - ensure you're using the new stepwise sharded workflows
- Phase 4 completely restructured - update any automation expecting old Phase 4 structure
- Epic creation now requires architectural context (moved to Phase 3 in previous release)
**Agent System:**
- Custom agents now require proper compilation - use the new agent creation workflow
- GitHub integration moved from chatmodes to agents folder - update any references
### 📊 Impact Summary
**New in alpha.13:**
- **Stepwise Workflow Architecture**: Complete transformation of all workflows to granular step-file system
- **Universal Custom Agent Support**: Extended to ALL IDEs with improved creation workflow
- **Phase 4 Revolution**: Completely restructured with sprint planning integration
- **Legacy Cleanup**: Removed all deprecated workflows for cleaner system
- **Advanced Code Review**: New adversarial review approach with multi-LLM strategy
- **Text-to-Speech**: Full TTS integration for voice feedback
- **Testing Expansion**: Playwright utils integration across all testing workflows
**Enhanced from alpha.12:**
- **Performance**: Improved file loading and removed time-based estimates
- **Documentation**: Complete cleanup with accurate references
- **Installer**: Better UX with cleanup options and improved defaults
- **Agent System**: More reliable compilation and better persona handling
1. **Legacy Workflows Removed**: Migrate to new stepwise sharded workflows
2. **Phase 4 Restructured**: Update automation expecting old Phase 4 structure
3. **Agent Compilation Required**: Custom agents must use new creation workflow
## [6.0.0-alpha.12]
@ -267,313 +207,101 @@
**Release: November 18, 2025**
This alpha release introduces a complete agent installation system with the new `bmad agent-install` command, vastly improves the BMB agent builder capabilities with comprehensive documentation and reference agents, and refines diagram distribution to better align with BMad Method's core principle: **BMad agents mirror real agile teams**.
### 🚀 Agent Installation Revolution
### 🎨 Diagram Capabilities Refined and Distributed
**Excalidraw Integration Evolution:**
Building on the excellent Excalidraw integration introduced with the Frame Expert agent, we've refined how diagram capabilities are distributed across the BMad Method ecosystem to better reflect real agile team dynamics.
**The Refinement:**
- The valuable Excalidraw diagramming capabilities have been distributed to the agents who naturally create these artifacts in real teams
- **Architect**: System architecture diagrams, data flow visualizations
- **Product Manager**: Process flowcharts and workflow diagrams
- **UX Designer**: Wireframe creation capabilities
- **Tech Writer**: All diagram types for documentation needs
- **New CIS Agent**: presentation-master for specialized visual communication
**Shared Infrastructure Enhancement:**
- Excalidraw templates, component libraries, and validation patterns elevated to core resources
- Available to both BMM agents AND CIS presentation specialists
- Preserves all the excellent Excalidraw functionality while aligning with natural team roles
### 🚀 New Agent Installation System
**Agent Installation Infrastructure (NEW in alpha.11):**
- `bmad agent-install` CLI command with interactive persona customization
- **YAML → XML compilation engine** with smart handler injection
- Supports Simple (single file), Expert (with sidecars), and Module agents
- Handlebars-style template variable processing
- Automatic manifest tracking and IDE integration
- Source preservation in `_cfg/custom/agents/` for reinstallation
**New Reference Agents Added:**
- **commit-poet**: Poetic git commit message generator (Simple agent example)
- **journal-keeper**: Daily journaling agent with templates (Expert agent example)
- **security-engineer & trend-analyst**: Module agent examples with ecosystem integration
**Critical Persona Field Guidance Added:**
New documentation explaining how LLMs interpret persona fields for better agent quality:
- **role** → "What knowledge, skills, and capabilities do I possess?"
- **identity** → "What background, experience, and context shape my responses?"
- **communication_style** → "What verbal patterns, word choice, and phrasing do I use?"
- **principles** → "What beliefs and operating philosophy drive my choices?"
Key insight: `communication_style` should ONLY describe HOW the agent talks, not WHAT they do
**BMM Agent Voice Enhancement:**
All 9 existing BMM agents enhanced with distinct, memorable communication voices:
- **Mary (analyst)**: "Treats analysis like a treasure hunt - excited by every clue"
- **John (PM)**: "Asks 'WHY?' relentlessly like a detective on a case"
- **Winston (architect)**: "Champions boring technology that actually works"
- **Amelia (dev)**: "Ultra-succinct. Speaks in file paths and AC IDs"
- **Sally (UX)**: "Paints pictures with words, telling user stories that make you FEEL"
### 🔧 Edit-Agent Workflow Comprehensive Enhancement
**Expert Agent Sidecar Support (NEW):**
- Automatically detects and handles Expert agents with multiple files
- Loads and manages templates, data files, knowledge bases
- Smart sidecar analysis: maps references, finds orphans, validates paths
- 5 complete sidecar editing patterns with warm, educational feedback
**7-Step Communication Style Refinement Pattern:**
1. Diagnose current style with red flag word detection
2. Extract non-style content to working copy
3. Discover TRUE communication style through interview questions
4. Craft pure style using presets and reference agents
5. Show before/after transformation with full context
6. Validate against standards (zero red flags)
7. Confirm with user through dramatic reading
**Unified Validation Checklist:**
- Single source of truth: `agent-validation-checklist.md` (160 lines)
- Shared between create-agent and edit-agent workflows
- Comprehensive persona field separation validation
- Expert agent sidecar validation (9 specific checks)
- Common issues and fixes with real examples
- **bmad agent-install CLI**: Interactive agent installation with persona customization
- **4 Reference Agents**: commit-poet, journal-keeper, security-engineer, trend-analyst
- **Agent Compilation Engine**: YAML → XML with smart handler injection
- **60 Communication Presets**: Pure communication styles for agent personas
### 📚 BMB Agent Builder Enhancement
**Vastly Improved Agent Creation & Editing Capabilities:**
- **Complete Documentation Suite**: 7 new guides for agent architecture and creation
- **Expert Agent Sidecar Support**: Multi-file agents with templates and knowledge bases
- **Unified Validation**: 160-line checklist shared across workflows
- **BMM Agent Voices**: All 9 agents enhanced with distinct communication styles
- Create-agent and edit-agent workflows now have accurate, comprehensive documentation
- All context references updated and validated for consistency
- Workflows can now properly guide users through complex agent design decisions
### 🎯 Workflow Architecture Change
**New Agent Documentation Suite:**
- `understanding-agent-types.md` - Architecture vs capability distinction
- `simple-agent-architecture.md` - Self-contained agents guide
- `expert-agent-architecture.md` - Agents with sidecar files
- `module-agent-architecture.md` - Workflow-integrated agents
- `agent-compilation.md` - YAML → XML transformation process
- `agent-menu-patterns.md` - Menu design patterns
- `communication-presets.csv` - 60 pure communication styles for reference
**New Reference Agents for Learning:**
- Complete working examples of Simple, Expert, and Module agents
- Can be installed directly via the new `bmad agent-install` command
- Serve as both learning resources and ready-to-use agents
### 🎯 Epic Creation Moved to Phase 3 (After Architecture)
**Workflow Sequence Corrected:**
```
Phase 2: PRD → UX Design
Phase 3: Architecture → Epics & Stories ← NOW HERE (technically informed)
```
**Why This Fundamental Change:**
- Epics need architectural context: API contracts, data models, technical decisions
- Stories can reference actual architectural patterns and constraints
- Reduces rewrites when architecture reveals complexity
- Better complexity-based estimation (not time-based)
### 🖥️ New IDE Support
**Google Antigravity IDE Installer:**
- Flattened file naming for proper slash commands (bmad-module-agents-name.md)
- Namespace isolation prevents module conflicts
- Subagent installation support (project or user level)
- Module-specific injection configuration
**Codex CLI Enhancement:**
- Now supports both global and project-specific installation
- CODEX_HOME configuration for multi-project workflows
- OS-specific setup instructions (Unix/Mac/Windows)
### 🏗️ Reference Agents & Standards
**New Reference Agents Provide Clear Examples:**
- **commit-poet.agent.yaml**: Simple agent with pure communication style
- **journal-keeper.agent.yaml**: Expert agent with sidecar file structure
- **security-engineer.agent.yaml**: Module agent for ecosystem integration
- **trend-analyst.agent.yaml**: Module agent with cross-workflow capabilities
**Agent Type Clarification:**
- Clear documentation that agent types (Simple/Expert/Module) describe architecture, not capability
- Module = designed for ecosystem integration, not limited in function
### 🐛 Technical Improvements
**Linting Compliance:**
- Fixed all ESLint warnings across agent tooling
- `'utf-8'``'utf8'` (unicorn/text-encoding-identifier-case)
- `hasOwnProperty``Object.hasOwn` (unicorn/prefer-object-has-own)
- `JSON.parse(JSON.stringify(...))``structuredClone(...)`
**Agent Compilation Engine:**
- Auto-injects frontmatter, activation, handlers, help/exit menu items
- Smart handler inclusion (only includes handlers actually used)
- Proper XML escaping and formatting
- Persona name customization support
### 📊 Impact Summary
**New in alpha.11:**
- **Agent installation system** with `bmad agent-install` CLI command
- **4 new reference agents** (commit-poet, journal-keeper, security-engineer, trend-analyst)
- **Complete agent documentation suite** with 7 new focused guides
- **Expert agent sidecar support** in edit-agent workflow
- **2 new IDE installers** (Google Antigravity, enhanced Codex)
- **Unified validation checklist** (160 lines) for consistent quality standards
- **60 pure communication style presets** for agent persona design
**Enhanced from alpha.10:**
- **BMB agent builder workflows** with accurate context and comprehensive guidance
- **All 9 BMM agents** enhanced with distinct, memorable communication voices
- **Excalidraw capabilities** refined and distributed to role-appropriate agents
- **Epic creation** moved to Phase 3 (after Architecture) for technical context
- **Epic Creation Moved**: Now in Phase 3 after Architecture for technical context
- **Excalidraw Distribution**: Diagram capabilities moved to role-appropriate agents
- **Google Antigravity IDE**: New installer with flattened file naming
### ⚠️ Breaking Changes
**Agent Changes:**
- Frame Expert agent retired - diagram capabilities now available through role-appropriate agents:
- Architecture diagrams → `/architect`
- Process flows → `/pm`
- Wireframes → `/ux-designer`
- Documentation visuals → `/tech-writer`
**Workflow Changes:**
- Epic creation moved from Phase 2 to Phase 3 (after Architecture)
- Excalidraw workflows redistributed to appropriate agents
**Installation Changes:**
- New `bmad agent-install` command replaces manual agent installation
- Agent YAML files must be compiled to XML for use
### 🔄 Migration Notes
**For Existing Projects:**
1. **Frame Expert Users:**
- Transition to role-appropriate agents for diagrams
- All Excalidraw functionality preserved and enhanced
- Shared templates now in core resources for wider access
2. **Agent Installation:**
- Use `bmad agent-install` for all agent installations
- Existing manual installations still work but won't have customization
3. **Epic Creation Timing:**
- Epics now created in Phase 3 after Architecture
- Update any automation expecting epics in Phase 2
4. **Communication Styles:**
- Review agent communication_style fields
- Remove any role/identity/principle content
- Use communication-presets.csv for pure styles
5. **Expert Agents:**
- Edit-agent workflow now fully supports sidecar files
- Organize templates and data files in agent folder
1. **Frame Expert Retired**: Use role-appropriate agents for diagrams
2. **Agent Installation**: New bmad agent-install command replaces manual installation
3. **Epic Creation Phase**: Moved from Phase 2 to Phase 3
## [6.0.0-alpha.10]
**Release: November 16, 2025**
- **🎯 Epics Generated AFTER Architecture**: Major milestone - epics/stories now created after architecture for technically-informed user stories with better acceptance criteria
- **🎨 Frame Expert Agent**: New Excalidraw specialist with 4 diagram workflows (flowchart, diagram, dataflow, wireframe) for visual documentation
- **⏰ Time Estimate Prohibition**: Critical warnings added across 33 workflows - acknowledges AI has fundamentally changed development speed
- **🎯 Platform-Specific Commands**: New `ide-only`/`web-only` fields filter menu items based on environment (IDE vs web bundle)
- **🔧 Agent Customization**: Enhanced memory/prompts merging via `*.customize.yaml` files for persistent agent personalization
- **Epics After Architecture**: Major milestone - technically-informed user stories created post-architecture
- **Frame Expert Agent**: New Excalidraw specialist with 4 diagram workflows
- **Time Estimate Prohibition**: Warnings across 33 workflows acknowledging AI's impact on development speed
- **Platform-Specific Commands**: ide-only/web-only fields filter menu items by environment
- **Agent Customization**: Enhanced memory/prompts merging via \*.customize.yaml files
## [6.0.0-alpha.9]
**Release: November 12, 2025**
- **🚀 Intelligent File Discovery Protocol**: New `discover_inputs` with FULL_LOAD, SELECTIVE_LOAD, and INDEX_GUIDED strategies for automatic context loading
- **📚 3-Track System**: Simplified from 5 levels to 3 intuitive tracks: quick-flow, bmad-method, and enterprise-bmad-method
- **🌐 Web Bundles Guide**: Comprehensive documentation for Gemini Gems and Custom GPTs with 60-80% cost savings strategies
- **🏗️ Unified Output Structure**: Eliminated `.ephemeral/` folders - all artifacts now in single configurable output folder
- **🎮 BMGD Phase 4**: Added 10 game development workflows following BMM patterns with game-specific adaptations
- **Intelligent File Discovery**: discover_inputs with FULL_LOAD, SELECTIVE_LOAD, INDEX_GUIDED strategies
- **3-Track System**: Simplified from 5 levels to 3 intuitive tracks
- **Web Bundles Guide**: Comprehensive documentation with 60-80% cost savings strategies
- **Unified Output Structure**: Eliminated .ephemeral/ folders - single configurable output folder
- **BMGD Phase 4**: Added 10 game development workflows with BMM patterns
## [6.0.0-alpha.8]
**Release: November 9, 2025**
- **🎯 Configurable Installation**: Custom directories with `.bmad` hidden folder default for cleaner project structure
- **🚀 Optimized Agent Loading**: CLI loads from installed files eliminating duplication and maintenance burden
- **🌐 Party Mode Everywhere**: All web bundles include multi-agent collaboration with customizable party configurations
- **🔧 Phase 4 Artifact Separation**: Stories, code reviews, sprint plans now configurable outside docs folder
- **📦 Expanded Web Bundles**: All BMM, BMGD, and CIS agents bundled with advanced elicitation integration
- **Configurable Installation**: Custom directories with .bmad hidden folder default
- **Optimized Agent Loading**: CLI loads from installed files, eliminating duplication
- **Party Mode Everywhere**: All web bundles include multi-agent collaboration
- **Phase 4 Artifact Separation**: Stories, code reviews, sprint plans configurable outside docs
- **Expanded Web Bundles**: All BMM, BMGD, CIS agents bundled with elicitation integration
## [6.0.0-alpha.7]
**Release: November 7, 2025**
- **🌐 Workflow Vendoring**: Web bundler performs automatic workflow vendoring for cross-module dependencies
- **🎮 BMGD Module Extraction**: Game development split into standalone module with 4-phase industry-standard structure
- **🔧 Enhanced Dependency Resolution**: Better handling of `web_bundle: false` workflows with positive resolution messages
- **📚 Advanced Elicitation Fix**: Added missing CSV files to workflow bundles fixing runtime failures
- **🐛 Claude Code Fix**: Resolved README slash command installation regression
- **Workflow Vendoring**: Web bundler performs automatic cross-module dependency vendoring
- **BMGD Module Extraction**: Game development split into standalone 4-phase structure
- **Enhanced Dependency Resolution**: Better handling of web_bundle: false workflows
- **Advanced Elicitation Fix**: Added missing CSV files to workflow bundles
- **Claude Code Fix**: Resolved README slash command installation regression
## [6.0.0-alpha.6]
**Release: November 4, 2025**
- **🐛 Critical Installer Fixes**: Fixed manifestPath error and option display issues blocking installation
- **📖 Conditional Docs Installation**: Optional documentation installation to reduce footprint in production
- **🎨 Improved Installer UX**: Better formatting with descriptive labels and clearer feedback
- **🧹 Issue Tracker Cleanup**: Closed 54 legacy v4 issues for focused v6 development
- **📝 Contributing Updates**: Removed references to non-existent branches in documentation
- **Critical Installer Fixes**: Fixed manifestPath error and option display issues
- **Conditional Docs Installation**: Optional documentation to reduce production footprint
- **Improved Installer UX**: Better formatting with descriptive labels and clearer feedback
- **Issue Tracker Cleanup**: Closed 54 legacy v4 issues for focused v6 development
- **Contributing Updates**: Removed references to non-existent branches
## [6.0.0-alpha.5]
**Release: November 4, 2025**
- **🎯 3-Track Scale System**: Revolutionary simplification from 5 confusing levels to 3 intuitive preference-driven tracks
- **✨ Elicitation Modernization**: Replaced legacy XML tags with explicit `invoke-task` pattern at strategic decision points
- **📚 PM/UX Evolution Section**: Added November 2025 industry research on AI Agent PMs and Full-Stack Product Leads
- **🏗️ Brownfield Reality Check**: Rewrote Phase 0 with 4 real-world scenarios for messy existing codebases
- **📖 Documentation Accuracy**: All agent capabilities now match YAML source of truth with zero hallucination risk
- **3-Track Scale System**: Simplified from 5 levels to 3 intuitive preference-driven tracks
- **Elicitation Modernization**: Replaced legacy XML tags with explicit invoke-task pattern
- **PM/UX Evolution**: Added November 2025 industry research on AI Agent PMs
- **Brownfield Reality Check**: Rewrote Phase 0 with 4 real-world scenarios
- **Documentation Accuracy**: All agent capabilities now match YAML source of truth
## [6.0.0-alpha.4]
**Release: November 2, 2025**
- **📚 Documentation Hub**: Created 18 comprehensive guides (7000+ lines) with professional technical writing standards
- **🤖 Paige Agent**: New technical documentation specialist available across all BMM phases
- **🚀 Quick Spec Flow**: Intelligent Level 0-1 planning with auto-stack detection and brownfield analysis
- **📦 Universal Shard-Doc**: Split large markdown documents into organized sections with dual-strategy loading
- **🔧 Intent-Driven Planning**: PRD and Product Brief transformed from template-filling to natural conversation
- **Documentation Hub**: Created 18 comprehensive guides (7000+ lines) with professional standards
- **Paige Agent**: New technical documentation specialist across all BMM phases
- **Quick Spec Flow**: Intelligent Level 0-1 planning with auto-stack detection
- **Universal Shard-Doc**: Split large markdown documents with dual-strategy loading
- **Intent-Driven Planning**: PRD and Product Brief transformed from template-filling to conversation
## [6.0.0-alpha.3]

Binary file not shown.

View File

@ -19,7 +19,7 @@ A custom agents and workflows package follows this structure:
```
my-custom-agents/
├── custom.yaml # Package configuration
├── module.yaml # Package configuration
├── agents/ # Agent definitions
│ └── my-agent/
│ └── agent.md
@ -30,7 +30,7 @@ my-custom-agents/
#### Configuration
Create a `custom.yaml` file in your package root:
Create a `module.yaml` file in your package root:
```yaml
code: my-custom-agents
@ -42,11 +42,6 @@ default_selected: true
See `/example-custom-content` for a working example of a folder with multiple random custom agents and workflows. Technically its also just a module, but you will be able to further pick and choose from this folders contents of what you do and do not want to include in a destination folder. This way, you can store all custom content source in one location and easily install it to different locations.
```bash
# The example is ready to use - just rename the config file:
mv example-custom-content/custom.bak example-custom-content/custom.yaml
```
### 2. Custom Modules
Custom modules are complete BMAD modules that can include their own configuration, documentation, along with agents and workflows that all compliment each other. Additionally they will have their own installation scripts, data, and potentially other tools. Modules can be used for:
@ -64,7 +59,7 @@ A custom module follows this structure:
my-module/
├── _module-installer/
│ ├── installer.js # optional, when it exists it will run with module installation
│ └── install-config.yaml # Module installation configuration with custom question and answer capture
├── module.yaml # Module installation configuration with custom question and answer capture
├── docs/ # Module documentation
├── agents/ # Module-specific agents
├── workflows/ # Module-specific workflows
@ -77,7 +72,7 @@ my-module/
#### Module Configuration
The `_module-installer/install-config.yaml` file defines how your module is installed:
The `module.yaml` file defines how your module is installed:
```yaml
# Module metadata
@ -99,12 +94,6 @@ my_setting:
See `/example-custom-module` for a complete example:
```bash
# The example is ready to use - just rename the _module-installer/install-config file:
mv example-custom-module/mwm/_module-installer/install-config.bak \
example-custom-module/mwm/_module-installer/install-config.yaml
```
## Installation Process
### Step 1: Running the Installer
@ -128,8 +117,7 @@ If you select "Enter a directory path", the installer will prompt for the locati
The installer will:
- Scan the directory and all subdirectories for the presence of a `custom.yaml` file (standalone content such as agents and workflows)
- Scan for `_module-installer/install-config.yaml` files (modules)
- Scan for `module.yaml` files (modules)
- Display an indication of how many installable folders it has found. Note that a project with stand along agents and workflows all under a single folder like the example will just list the count as 1 for that directory.
### Step 3: Selecting Content
@ -230,7 +218,7 @@ Custom content can be distributed:
### No Custom Content Found
- Ensure your `custom.yaml` or `install-config.yaml` files are properly named
- Ensure your `module.yaml` files are properly named
- Check file permissions
- Verify the directory path is correct

View File

@ -59,6 +59,7 @@ project-root/
### Key Exclusions
- `_module-installer/` directories are never copied to destination
- module.yaml
- `localskip="true"` agents are filtered out
- Source `config.yaml` templates are replaced with generated configs
@ -92,8 +93,8 @@ Creative Innovation Studio for design workflows
```
src/modules/{module}/
├── _module-installer/ # Not copied to destination
│ ├── installer.js # Post-install logic
│ └── install-config.yaml
│ ├── installer.js # Post-install logic
├── module.yaml
├── agents/
├── tasks/
├── templates/
@ -107,7 +108,7 @@ src/modules/{module}/
### Collection Process
Modules define prompts in `install-config.yaml`:
Modules define prompts in `module.yaml`:
```yaml
project_name:
@ -218,12 +219,12 @@ Platform-specific content without source modification:
src/modules/mymod/
├── _module-installer/
│ ├── installer.js
│ └── install-config.yaml
├── module.yaml
├── agents/
└── tasks/
```
2. **Configuration** (`install-config.yaml`)
2. **Configuration** (`module.yaml`)
```yaml
code: mymod

View File

@ -1,6 +1,6 @@
agent:
metadata:
id: .bmad/agents/commit-poet/commit-poet.md
id: "{bmad_folder}/agents/commit-poet/commit-poet.md"
name: "Inkwell Von Comitizen"
title: "Commit Message Artisan"
icon: "📜"

View File

@ -41,7 +41,7 @@ CLI uses Commander.js, commands auto-loaded from `tools/cli/commands/`:
### Core Architecture Patterns
1. **IDE Handlers**: Each IDE extends BaseIdeSetup class
2. **Module Installers**: Modules can have `_module-installer/installer.js`
2. **Module Installers**: Modules can have `module.yaml` and `_module-installer/installer.js`
3. **Sub-modules**: IDE-specific customizations in `sub-modules/{ide-name}/`
4. **Shared Utilities**: `tools/cli/installers/lib/ide/shared/` contains generators

View File

@ -117,7 +117,7 @@ Contains:
- Add new IDE handler: Create file in /tools/cli/installers/lib/ide/, extend BaseIdeSetup
- Fix installer bug: Check installer.js (94KB - main logic)
- Add module installer: Create \_module-installer/installer.js in module
- Add module installer: Create \_module-installer/installer.js if custom installer logic needed
- Update shared generators: Modify files in /shared/ directory
## Relationships

View File

@ -27,7 +27,7 @@ src/modules/{module-name}/
│ ├── injections.yaml
│ ├── config.yaml
│ └── sub-agents/
├── install-config.yaml # Module install configuration
├── module.yaml # Module install configuration
└── README.md # Module documentation
```
@ -145,7 +145,7 @@ Defined in @/tools/cli/lib/platform-codes.js
- Create new module installer: Add \_module-installer/installer.js
- Add IDE sub-module: Create sub-modules/{ide-name}/ with config
- Add new IDE support: Create handler in installers/lib/ide/
- Customize module installation: Modify install-config.yaml
- Customize module installation: Modify module.yaml
## Relationships

View File

@ -1,6 +1,6 @@
agent:
metadata:
id: custom/agents/toolsmith/toolsmith.md
id: "{bmad_folder}/agents/toolsmith/toolsmith.md"
name: Vexor
title: Infernal Toolsmith + Guardian of the BMAD Forge
icon: ⚒️

View File

@ -1,3 +1,4 @@
code: bmad-custom
name: "BMAD-Custom: Sample Stand Alone Custom Agents and Workflows"
default_selected: true
type: custom

View File

@ -3,7 +3,7 @@ name: 'step-01-init'
description: 'Initialize quiz game with mode selection and category choice'
# Path Definitions
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
# File References
thisStepFile: '{workflow_path}/steps/step-01-init.md'
@ -66,7 +66,7 @@ To set up the quiz game by selecting game mode, choosing a category, and prepari
### 1. Welcome and Configuration Loading
Load config from {project-root}/.bmad/bmb/config.yaml to get user_name.
Load config from {project-root}/{bmad_folder}/bmb/config.yaml to get user_name.
Present dramatic welcome:
"🎺 _DRAMATIC MUSIC PLAYS_ 🎺

View File

@ -3,7 +3,7 @@ name: 'step-02-q1'
description: 'Question 1 - Level 1 difficulty'
# Path Definitions
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
# File References
thisStepFile: '{workflow_path}/steps/step-02-q1.md'

View File

@ -3,7 +3,7 @@ name: 'step-03-q2'
description: 'Question 2 - Level 2 difficulty'
# Path Definitions
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
# File References
thisStepFile: '{workflow_path}/steps/step-03-q2.md'

View File

@ -3,7 +3,7 @@ name: 'step-04-q3'
description: 'Question 3 - Level 3 difficulty'
# Path Definitions
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
# File References
thisStepFile: '{workflow_path}/steps/step-04-q3.md'

View File

@ -3,7 +3,7 @@ name: 'step-05-q4'
description: 'Question 4 - Level 4 difficulty'
# Path Definitions
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
# File References
thisStepFile: '{workflow_path}/steps/step-05-q4.md'

View File

@ -3,7 +3,7 @@ name: 'step-06-q5'
description: 'Question 5 - Level 5 difficulty'
# Path Definitions
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
# File References
thisStepFile: '{workflow_path}/steps/step-06-q5.md'

View File

@ -3,7 +3,7 @@ name: 'step-07-q6'
description: 'Question 6 - Level 6 difficulty'
# Path Definitions
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
# File References
thisStepFile: '{workflow_path}/steps/step-07-q6.md'

View File

@ -3,7 +3,7 @@ name: 'step-08-q7'
description: 'Question 7 - Level 7 difficulty'
# Path Definitions
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
# File References
thisStepFile: '{workflow_path}/steps/step-08-q7.md'

View File

@ -3,7 +3,7 @@ name: 'step-09-q8'
description: 'Question 8 - Level 8 difficulty'
# Path Definitions
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
# File References
thisStepFile: '{workflow_path}/steps/step-09-q8.md'

View File

@ -3,7 +3,7 @@ name: 'step-10-q9'
description: 'Question 9 - Level 9 difficulty'
# Path Definitions
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
# File References
thisStepFile: '{workflow_path}/steps/step-10-q9.md'

View File

@ -3,7 +3,7 @@ name: 'step-11-q10'
description: 'Question 10 - Level 10 difficulty'
# Path Definitions
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
# File References
thisStepFile: '{workflow_path}/steps/step-11-q10.md'

View File

@ -3,7 +3,7 @@ name: 'step-12-results'
description: 'Final results and celebration'
# Path Definitions
workflow_path: '{project-root}/.bmad/custom/src/workflows/quiz-master'
workflow_path: '{project-root}/{bmad_folder}/custom/src/workflows/quiz-master'
# File References
thisStepFile: '{workflow_path}/steps/step-12-results.md'

View File

@ -1,269 +0,0 @@
---
stepsCompleted: [1, 2, 3, 4, 5, 6, 7]
---
## Build Summary
**Date:** 2025-12-04
**Status:** Build Complete
### Files Generated
**Main Workflow:**
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/workflow.md`
**Step Files (12 total):**
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-01-init.md` - Game setup and mode selection
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-02-q1.md` - Question 1 (Level 1)
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-03-q2.md` - Question 2 (Level 2)
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-04-q3.md` - Question 3 (Level 3)
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-05-q4.md` - Question 4 (Level 4)
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-06-q5.md` - Question 5 (Level 5)
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-07-q6.md` - Question 6 (Level 6)
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-08-q7.md` - Question 7 (Level 7)
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-09-q8.md` - Question 8 (Level 8)
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-10-q9.md` - Question 9 (Level 9)
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-11-q10.md` - Question 10 (Level 10)
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/steps/step-12-results.md` - Final results and celebration
**Templates:**
- `/Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master/templates/csv-headers.template` - CSV column headers
### Key Features Implemented
1. **Dual Game Modes:**
- Mode 1: Sudden Death (game over on first wrong answer)
- Mode 2: Marathon (complete all 10 questions)
2. **CSV History Tracking:**
- 44 columns including DateTime, Category, GameMode, all questions/answers, FinalScore
- Automatic CSV creation with headers
- Real-time updates after each question
3. **Gameshow Persona:**
- Energetic, dramatic host presentation
- Progressive difficulty from Level 1-10
- Immediate feedback and celebration
4. **Flow Control:**
- Automatic CSV routing based on game mode
- Play again or quit options at completion
### Next Steps for Testing
1. Run the workflow: `/bmad:bmb:workflows:quiz-master`
2. Test both game modes
3. Verify CSV file creation and updates
4. Check question progression and difficulty
5. Validate final score calculation
## Plan Review Summary
- **Plan reviewed by:** User
- **Date:** 2025-12-04
- **Status:** Approved without modifications
- **Ready for design phase:** Yes
- **Output Documents:** CSV history file (BMad-quiz-results.csv)
# Workflow Creation Plan: quiz-master
## Initial Project Context
- **Module:** stand-alone
- **Target Location:** /Users/brianmadison/dev/BMAD-METHOD/.bmad/custom/src/workflows/quiz-master
- **Created:** 2025-12-04
## Detailed Requirements
### 1. Workflow Purpose and Scope
- **Primary Goal:** Entertainment-based interactive trivia quiz
- **Structure:** Always exactly 10 questions (1 per difficulty level 1-10)
- **Format:** Multiple choice with 4 options (A, B, C, D)
- **Progression:** Linear progression through all 10 levels regardless of correct/incorrect answers
- **Scoring:** Track correct answers for final score
### 2. Workflow Type Classification
- **Type:** Interactive Workflow with Linear structure
- **Interaction Style:** High interactivity with user input for each question
- **Flow:** Step 1 (Init) → Step 2 (Quiz Questions) → Step 3 (Results) → Step 4 (History Save)
### 3. Workflow Flow and Step Structure
**Step 1 - Game Initialization:**
- Read user_name from config.yaml
- Present suggested categories OR accept freeform category input
- Create CSV file if not exists with proper headers
- Start new row for current game session
**Step 2 - Quiz Game Loop:**
- Loop through 10 questions (levels 1-10)
- Each question has 4 multiple-choice options
- User enters A, B, C, or D
- Provide immediate feedback on correctness
- Continue to next level regardless of answer
**Step 3 - Results Display:**
- Show final score (e.g., "You got 7 out of 10!")
- Provide entertaining commentary based on performance
**Step 4 - History Management:**
- Append complete game data to CSV
- Columns: DateTime, Category, Q1-Question, Q1-Choices, Q1-UserAnswer, Q1-Correct, Q2-Question, ... Q10-Correct, FinalScore
### 4. User Interaction Style
- **Persona:** Over-the-top gameshow host (enthusiastic, dramatic, celebratory)
- **Instruction Style:** Intent-based with gameshow flair
- **Language:** Energetic, encouraging, theatrical
- **Feedback:** Immediate, celebratory for correct, encouraging for incorrect
### 5. Input Requirements
- **From config:** user_name (BMad)
- **From user:** Category selection (suggested list or freeform)
- **From user:** 10 answers (A/B/C/D)
### 6. Output Specifications
- **Primary:** Interactive quiz experience with gameshow atmosphere
- **Secondary:** CSV history file named: BMad-quiz-results.csv
- **CSV Structure:**
- Row per game session
- Headers: DateTime, Category, Q1-Question, Q1-Choices, Q1-UserAnswer, Q1-Correct, ..., Q10-Correct, FinalScore
### 7. Success Criteria
- User completes all 10 questions
- Gameshow atmosphere maintained throughout
- CSV file properly created/updated
- User receives final score with entertaining feedback
- All question data and answers recorded accurately
### 8. Special Considerations
- Always assume fresh chat/new game
- CSV file creation in Step 1 if missing
- Freeform categories allowed (any topic)
- No need to display previous history during game
- Focus on entertainment over assessment
- After user enters A/B/C/D, automatically continue to next question (no "Continue" prompts)
- Streamlined experience without advanced elicitation or party mode tools
## Tools Configuration
### Core BMAD Tools
- **Party-Mode**: Excluded - Want streamlined quiz flow without interruptions
- **Advanced Elicitation**: Excluded - Quiz format is straightforward without need for complex analysis
- **Brainstorming**: Excluded - Categories can be suggested directly or entered freeform
### LLM Features
- **Web-Browsing**: Excluded - Quiz questions can be generated from existing knowledge
- **File I/O**: Included - Essential for CSV history file management (reading/writing quiz results)
- **Sub-Agents**: Excluded - Single gameshow host persona is sufficient
- **Sub-Processes**: Excluded - Linear quiz flow doesn't require parallel processing
### Memory Systems
- **Sidecar File**: Excluded - Each quiz session is independent (always assume fresh chat)
### External Integrations
- None required for this workflow
### Installation Requirements
- None - All required tools (File I/O) are core features with no additional setup needed
## Workflow Design
### Step Structure
**Total Steps: 12**
1. Step 01 - Init: Mode selection, category choice, CSV setup
2. Steps 02-11: Individual questions (1-10) with CSV updates
3. Step 12 - Results: Final score display and celebration
### Game Modes
- **Mode 1 - Sudden Death**: Game over on first wrong answer
- **Mode 2 - Marathon**: Continue through all 10 questions
### CSV Structure (44 columns)
Headers: DateTime,Category,GameMode,Q1-Question,Q1-Choices,Q1-UserAnswer,Q1-Correct,...,Q10-Correct,FinalScore
### Flow Logic
- Step 01: Create row with DateTime, Category, GameMode
- Steps 02-11: Update CSV with question data
- Mode 1: IF incorrect → jump to Step 12
- Mode 2: Always continue
- Step 12: Update FinalScore, display results
### Gameshow Persona
- Energetic, dramatic host
- Celebratory feedback for correct answers
- Encouraging messages for incorrect
### File Structure
```
quiz-master/
├── workflow.md
├── steps/
│ ├── step-01-init.md
│ ├── step-02-q1.md
│ ├── ...
│ └── step-12-results.md
└── templates/
└── csv-headers.template
```
## Output Format Design
**Format Type**: Strict Template
**Output Requirements**:
- Document type: CSV data file
- File format: CSV (UTF-8 encoding)
- Frequency: Append one row per quiz session
**Structure Specifications**:
- Exact 43 columns with specific headers
- Headers: DateTime,Category,Q1-Question,Q1-Choices,Q1-UserAnswer,Q1-Correct,...,Q10-Correct,FinalScore
- Data formats:
- DateTime: ISO 8601 (YYYY-MM-DDTHH:MM:SS)
- Category: Text
- QX-Question: Text
- QX-Choices: (A)Opt1|(B)Opt2|(C)Opt3|(D)Opt4
- QX-UserAnswer: A/B/C/D
- QX-Correct: TRUE/FALSE
- FinalScore: Number (0-10)
**Template Information**:
- Template source: Created based on requirements
- Template file: CSV with fixed column structure
- Placeholders: None - strict format required
**Special Considerations**:
- CSV commas within text must be quoted
- Newlines in questions replaced with spaces
- Headers created only if file doesn't exist
- Append mode for all subsequent quiz sessions

View File

@ -45,7 +45,7 @@ web_bundle: true
### 1. Module Configuration Loading
Load and read full config from {project-root}/.bmad/bmb/config.yaml and resolve:
Load and read full config from {project-root}/{bmad_folder}/bmb/config.yaml and resolve:
- `user_name`, `output_folder`, `communication_language`, `document_output_language`

View File

@ -3,9 +3,6 @@
This module is an example and is not at all recommended for any usage, this module was not vetted by any medical professionals and should
be considered at best for entertainment purposes only.
IF you want to see how a custom module installation works, copy this whole folder to where you will be installing from with npx, and rename
"\_module-installer/install-config.bak" to "\_module-installer/install-config.yaml".
You should see the option in the module selector when installing.
If you have received a module from someone else that is not in the official installation - you can install it similarly by running the

View File

@ -1,5 +1,6 @@
agent:
metadata:
id: "{bmad_folder}/mwm/agents/cbt-coach/cbt-coach.md"
name: "Dr. Alexis, M.D."
title: "CBT Coach"
icon: "🧠"

View File

@ -1,5 +1,6 @@
agent:
metadata:
id: "{bmad_folder}/mwm/agents/crisis-navigator.md"
name: "Beacon"
title: "Crisis Navigator"
icon: "🆘"
@ -95,7 +96,7 @@ agent:
triggers:
- trigger: party-mode
input: SPM or fuzzy match start party mode
route: "{project-root}/.bmad/core/workflows/edit-agent/workflow.md"
route: "{project-root}/{bmad_folder}/core/workflows/edit-agent/workflow.md"
data: crisis navigator agent discussion
type: exec
- trigger: expert-chat
@ -117,7 +118,7 @@ agent:
type: action
- trigger: "safety-plan"
route: "{project-root}/.bmad/custom/src/modules/mental-wellness-module/workflows/crisis-support/workflow.md"
route: "{project-root}/{bmad_folder}/custom/src/modules/mental-wellness-module/workflows/crisis-support/workflow.md"
description: "Create safety plan 🛡️"
type: workflow

View File

@ -1,5 +1,6 @@
agent:
metadata:
id: "{bmad_folder}/mwm/agents/meditation-guide.md"
name: "Serenity"
title: "Meditation Guide"
icon: "🧘"
@ -92,7 +93,7 @@ agent:
triggers:
- trigger: party-mode
input: SPM or fuzzy match start party mode
route: "{project-root}/.bmad/core/workflows/edit-agent/workflow.md"
route: "{project-root}/{bmad_folder}/core/workflows/edit-agent/workflow.md"
data: meditation guide agent discussion
type: exec
- trigger: expert-chat
@ -104,7 +105,7 @@ agent:
triggers:
- trigger: guided-meditation
input: GM or fuzzy match guided meditation
route: "{project-root}/.bmad/custom/src/modules/mental-wellness-module/workflows/guided-meditation/workflow.md"
route: "{project-root}/{bmad_folder}/custom/src/modules/mental-wellness-module/workflows/guided-meditation/workflow.md"
description: "Full meditation session 🧘"
type: workflow
- trigger: body-scan

View File

@ -1,5 +1,6 @@
agent:
metadata:
id: "{bmad_folder}/mwm/agents/wellness-companion/wellness-companion.md"
name: "Riley"
title: "Wellness Companion"
icon: "🌱"

View File

@ -4,6 +4,7 @@
code: mwm
name: "MWM: Mental Wellness Module"
default_selected: false
type: module
header: "MWM™: Custom Wellness Module"
subheader: "Demo of Potential Non Coding Custom Module Use case"

View File

@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package.json",
"name": "bmad-method",
"version": "6.0.0-alpha.14",
"version": "6.0.0-alpha.15",
"description": "Breakthrough Method of Agile AI-driven Development",
"keywords": [
"agile",

View File

@ -6,7 +6,7 @@ const chalk = require('chalk');
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from install-config.yaml
* @param {Object} options.config - Module configuration from module.yaml
* @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

View File

@ -63,6 +63,8 @@
<tag>invoke-workflow xml tag → Execute another workflow with given inputs and the workflow.xml runner</tag>
<tag>invoke-task xml tag → Execute specified task</tag>
<tag>invoke-protocol name="protocol_name" xml tag → Execute reusable protocol from protocols section</tag>
<tag>invoke-bash cmd="command" → Execute shell command, capture stdout/stderr, set {{bash_exit_code}}, {{bash_stdout}}, {{bash_stderr}}</tag>
<tag>set-var name="varname" value="..." → Set runtime variable {{varname}} to specified value (supports expressions)</tag>
<tag>goto step="x" → Jump to specified step</tag>
</execute-tags>
</substep>
@ -126,6 +128,8 @@
<tag>invoke-workflow - Call another workflow</tag>
<tag>invoke-task - Call a task</tag>
<tag>invoke-protocol - Execute a reusable protocol (e.g., discover_inputs)</tag>
<tag>invoke-bash cmd="..." - Execute shell command, results in {{bash_exit_code}}, {{bash_stdout}}, {{bash_stderr}}</tag>
<tag>set-var name="..." value="..." - Set runtime variable dynamically</tag>
</execution>
<output>
<tag>template-output - Save content checkpoint</tag>

View File

@ -28,7 +28,7 @@ This uses **micro-file architecture** for disciplined execution:
### Configuration Loading
Load config from `{project-root}/{bmad_folder}/bmm/config.yaml` and resolve:
Load config from `{project-root}/{bmad_folder}/core/config.yaml` and resolve:
- `project_name`, `output_folder`, `user_name`
- `communication_language`, `document_output_language`, `user_skill_level`

View File

@ -27,7 +27,7 @@ This uses **micro-file architecture** with **sequential conversation orchestrati
### Configuration Loading
Load config from `{project-root}/{bmad_folder}/bmm/config.yaml` and resolve:
Load config from `{project-root}/{bmad_folder}/core/config.yaml` and resolve:
- `project_name`, `output_folder`, `user_name`
- `communication_language`, `document_output_language`, `user_skill_level`

View File

@ -8,7 +8,7 @@ const chalk = require('chalk');
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from install-config.yaml
* @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

View File

@ -113,10 +113,10 @@ For a [module type] module, we'll create this structure:"
│ └── [template-files]
├── data/ # Module data files
│ └── [data-files]
├── module.yaml # Required
├── _module-installer/ # Installation configuration
│ ├── install-config.yaml # Required
│ ├── installer.js # Optional
│ └── assets/ # Optional install assets
│ ├── installer.js # Optional
│ └── assets/ # Optional install assets
└── README.md # Module documentation
```

View File

@ -184,7 +184,7 @@ Update module-plan.md with configuration section:
### Result Configuration Structure
The install-config.yaml will generate:
The module.yaml will generate:
- Module configuration at: {bmad_folder}/{module_code}/config.yaml
- User settings stored as: [describe structure]
````

View File

@ -37,7 +37,7 @@ partyModeWorkflow: '{project-root}/{bmad_folder}/core/workflows/party-mode/workf
## EXECUTION PROTOCOLS:
- 🎯 Use configuration plan from step 5
- 💾 Create install-config.yaml with all fields
- 💾 Create module.yaml with all fields
- 📖 Add "step-08-installer" to stepsCompleted array` before loading next step
- 🚫 FORBIDDEN to load next step until user selects 'C'
@ -50,7 +50,7 @@ partyModeWorkflow: '{project-root}/{bmad_folder}/core/workflows/party-mode/workf
## STEP GOAL:
To create the module installer configuration (install-config.yaml) that defines how users will install and configure the module.
To create the module installer configuration (module.yaml) that defines how users will install and configure the module.
## INSTALLER SETUP PROCESS:
@ -74,11 +74,11 @@ From step 5, we planned these configuration fields:
Ensure \_module-installer directory exists
Directory: {custom_module_location}/{module_name}/\_module-installer/
### 3. Create install-config.yaml
### 3. Create module.yaml
"I'll create the install-config.yaml file based on your configuration plan. This is the core installer configuration file."
"I'll create the module.yaml file based on your configuration plan. This is the core installer configuration file."
Create file: {custom_module_location}/{module_name}/\_module-installer/install-config.yaml from template {installConfigTemplate}
Create file: {custom_module_location}/{module_name}/module.yaml from template {installConfigTemplate}
### 4. Handle Custom Installation Logic
@ -117,7 +117,7 @@ Update module-plan.md with installer section:
### Install Configuration
- File: \_module-installer/install-config.yaml
- File: module.yaml
- Module code: {module_name}
- Default selected: false
- Configuration fields: [count]
@ -166,7 +166,7 @@ Display: **Select an Option:** [A] Advanced Elicitation [P] Party Mode [C] Conti
### ✅ SUCCESS:
- install-config.yaml created with all planned fields
- module.yaml created with all planned fields
- YAML syntax valid
- Custom installation logic prepared (if needed)
- Installer follows BMAD standards
@ -174,7 +174,7 @@ Display: **Select an Option:** [A] Advanced Elicitation [P] Party Mode [C] Conti
### ❌ SYSTEM FAILURE:
- Not creating install-config.yaml
- Not creating module.yaml
- Invalid YAML syntax
- Missing required fields
- Not using proper path templates

View File

@ -133,7 +133,8 @@ bmad install {module_name}
├── tasks/ # Task files
├── templates/ # Shared templates
├── data/ # Module data
├── _module-installer/ # Installation config
├── _module-installer/ # Installation optional js file with custom install routine
├── module.yaml # yaml config and install questions
└── README.md # This file
```

View File

@ -207,9 +207,10 @@ workflow {workflow_name}
├── workflows/ # ✅ Structure created, plans written
├── tasks/ # ✅ Created
├── templates/ # ✅ Created
├── data/ # ✅ Created
├── data/ # ✅ Created
├── _module-installer/ # ✅ Configured
└── README.md # ✅ Complete
└── README.md # ✅ Complete
└── module.yaml # ✅ Complete
```
## Completion Criteria

View File

@ -73,8 +73,8 @@ Expected Structure:
├── templates/ [✅/❌]
├── data/ [✅/❌]
├── _module-installer/ [✅/❌]
├── install-config.yaml [✅/❌]
│ └── installer.js [✅/N/A]
└── installer.js [✅/N/A]
├── module.yaml [✅/❌]
└── README.md [✅/❌]
```
@ -87,7 +87,7 @@ Expected Structure:
"**2. Configuration Files Check**"
**Install Configuration:**
Validate install-config.yaml
Validate module.yaml
- [ ] YAML syntax valid
- [ ] Module code matches folder name

View File

@ -6,7 +6,7 @@
/**
* @param {Object} options - Installation options
* @param {string} options.projectRoot - Project root directory
* @param {Object} options.config - Module configuration from install-config.yaml
* @param {Object} options.config - Module configuration from module.yaml
* @param {Array} options.installedIDEs - List of IDE codes being configured
* @param {Object} options.logger - Logger instance (log, warn, error methods)
* @returns {boolean} - true if successful, false to abort installation

View File

@ -13,15 +13,15 @@ This document provides the validation criteria used in step-11-validate.md to en
- [ ] data/ - Module data
- [ ] \_module-installer/ - Installation config
- [ ] README.md - Module documentation
- [ ] module.yaml - module config file
### Required Files in \_module-installer/
### Optional File in \_module-installer/
- [ ] install-config.yaml - Installation configuration
- [ ] installer.js - Custom logic (if needed)
## Configuration Validation
### install-config.yaml
### module.yaml
- [ ] Valid YAML syntax
- [ ] Module code matches folder name

View File

@ -98,7 +98,7 @@ After getting the workflow name:
Based on the module selection, confirm the target location:
- For bmb module: `{custom_workflow_location}` (defaults to `{bmad_folder}/custom/src/workflows`)
- For other modules: Check their install-config.yaml for custom workflow locations
- For other modules: Check their module.yaml for custom workflow locations
- Confirm the exact folder path where the workflow will be created
- Store the confirmed path as `{targetWorkflowPath}`

View File

@ -109,7 +109,7 @@ Create the workflow folder structure in the target location:
```
For bmb module, this will be: `{bmad_folder}/custom/src/workflows/{workflow_name}/`
For other modules, check their install-config.yaml for custom_workflow_location
For other modules, check their module.yaml for custom_workflow_location
### 3. Generate workflow.md

View File

@ -129,8 +129,9 @@ bmgd/
│ (Uses BMM workflows via cross-module references)
├── templates/
├── data/
├── module.yaml
└── _module-installer/
└── install-config.yaml
└── installer.js (optional)
```
## Configuration

View File

@ -9,7 +9,7 @@ const platformCodes = require(path.join(__dirname, '../../../../tools/cli/lib/pl
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from install-config.yaml
* @param {Object} options.config - Module configuration from module.yaml
* @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

View File

@ -5,7 +5,7 @@ const chalk = require('chalk');
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from install-config.yaml
* @param {Object} options.config - Module configuration from module.yaml
* @param {Object} options.logger - Logger instance for output
* @param {Object} options.platformInfo - Platform metadata from global config
* @returns {Promise<boolean>} - Success status

View File

@ -5,7 +5,7 @@ const chalk = require('chalk');
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from install-config.yaml
* @param {Object} options.config - Module configuration from module.yaml
* @param {Object} options.logger - Logger instance for output
* @returns {Promise<boolean>} - Success status
*/

View File

@ -139,9 +139,6 @@ Comprehensive documentation for all BMM workflows organized by phase:
- Complete story lifecycle
- One-story-at-a-time discipline
<<<<<<< Updated upstream
<<<<<<< Updated upstream
- **[Testing & QA Workflows](./test-architecture.md)** - Comprehensive quality assurance (1,420 lines)
- Test strategy, automation, quality gates
- TEA agent and test healing
@ -149,14 +146,6 @@ Comprehensive documentation for all BMM workflows organized by phase:
**Total: 34 workflows documented across all phases**
=======
> > > > > > > Stashed changes
=======
> > > > > > > Stashed changes
### Advanced Workflow References
For detailed technical documentation on specific complex workflows:
@ -181,23 +170,9 @@ Quality assurance guidance:
<!-- Test Architect documentation to be added -->
<<<<<<< Updated upstream
<<<<<<< Updated upstream
- Test design workflows
- Quality gates
- Risk assessment
- # NFR validation
=======
> > > > > > > Stashed changes
- Test design workflows
- Quality gates
- Risk assessment
- NFR validation
> > > > > > > Stashed changes
---
## 🏗️ Module Structure

View File

@ -133,7 +133,6 @@ The `sprint-status.yaml` file is the single source of truth for all implementati
### (BMad Method / Enterprise)
```
<<<<<<< Updated upstream
PRD (PM) → Architecture (Architect)
→ create-epics-and-stories (PM) ← V6: After architecture!
→ implementation-readiness (Architect)
@ -142,7 +141,6 @@ PRD (PM) → Architecture (Architect)
→ story loop (SM/DEV)
→ retrospective (SM)
→ [Next Epic]
=======
Current Phase: 4 (Implementation)
Current Epic: Epic 1 (Authentication)
Current Sprint: Sprint 1
@ -190,108 +188,12 @@ See: [workflow-status instructions](../workflows/workflow-status/instructions.md
See: [document-project reference](./workflow-document-project-reference.md)
---
## Story Lifecycle Visualization
```
┌─────────────────────────────────────────────────────────────┐
│ PHASE 4: IMPLEMENTATION (Iterative Story Lifecycle) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────┐
│ Sprint Planning │ → Creates sprint-status.yaml
└────────┬────────┘ Defines story queue
├──────────────────────────────────────────┐
│ │
▼ │
┌─────────────────────┐ │
│ Epic Tech Context │ → Optional per epic │
│ (Once per epic) │ Provides technical │
└─────────────────────┘ guidance │
│ │
▼ │
┌─────────────────────────────────────────────────┤
│ FOR EACH STORY IN QUEUE: │
├─────────────────────────────────────────────────┤
│ │
▼ │
┌─────────────────┐ │
│ Create Story │ → Generates story file │
│ (TODO → IN PROGRESS) │
└────────┬────────┘ │
│ │
▼ │
┌─────────────────┐ │
│ Story Context │ → Assembles focused context │
└────────┬────────┘ │
│ │
▼ │
┌─────────────────┐ │
│ Dev Story │ → Implements + tests │
│ (IN PROGRESS) │ │
└────────┬────────┘ │
│ │
▼ │
┌─────────────────┐ │
│ Code Review │ → Senior dev review │
│ (IN PROGRESS → │ │
│ READY FOR REVIEW) │
└────────┬────────┘ │
│ │
┌────┴────┐ │
│ Result? │ │
└────┬────┘ │
│ │
┌────┼────────────────────┐ │
│ │ │ │
▼ ▼ ▼ │
APPROVED APPROVED REQUEST │
WITH COMMENTS CHANGES │
│ │ │ │
└─────────┴───────────────────┘ │
│ │
▼ │
┌─────────────────┐ │
│ Story Done │ → READY FOR REVIEW → DONE│
└────────┬────────┘ │
│ │
├─────────────────────────────────────┘
│ More stories?
┌────────────────┐
│ Epic Complete? │
└────────┬───────┘
┌────┼────┐
│ │
Yes No
│ └──> Continue to next story
┌─────────────────┐
│ Retrospective │ → Review epic, lessons learned
└─────────────────┘
All epics done?
Yes → PROJECT COMPLETE
>>>>>>> Stashed changes
```
---
## Related Documentation
- [Phase 1: Analysis Workflows](./workflows-analysis.md)
- [Phase 2: Planning Workflows](./workflows-planning.md)
- [Phase 3: Solutioning Workflows](./workflows-solutioning.md)
---
## Troubleshooting
**Q: Which workflow should I run next?**
@ -306,6 +208,4 @@ A: Not recommended. Complete one story's full lifecycle before starting the next
**Q: What if code review finds issues?**
A: DEV runs `dev-story` to make fixes, re-runs tests, then runs `code-review` again until it passes.
---
_Phase 4 Implementation - One story at a time, done right._

View File

@ -2,7 +2,7 @@
code: bmm
name: "BMM: BMad Method Agile-AI Driven-Development"
default_selected: false # This module will be selected by default for new installations
default_selected: true # This module will be selected by default for new installations
header: "BMad Method™: Breakthrough Method of Agile-Ai Driven-Dev"
subheader: "Agent and Workflow Configuration for this module"
@ -52,3 +52,23 @@ tea_use_playwright_utils:
- "You must install packages yourself, or use test architect's *framework command."
default: false
result: "{value}"
# External Code Review Agent Selection
# Allows delegating code reviews to an external AI agent CLI for independent, unbiased reviews
# Useful when using a different AI as primary IDE agent (e.g., Codex/Gemini users can use Claude for reviews)
external_review_agent:
prompt:
- "Which external agent should perform code reviews?"
- "External agents provide independent, unbiased reviews separate from your primary IDE agent."
- "The selected CLI must be installed and configured on your system."
default: "none"
result: "{value}"
single-select:
- value: "codex"
label: "Codex (OpenAI) - Code review using OpenAI Codex CLI"
- value: "gemini"
label: "Gemini (Google) - Code review using Google Gemini CLI"
- value: "claude"
label: "Claude Code (Anthropic) - Code review using Claude Code CLI"
- value: "none"
label: "None - Use built-in review (no external agent)"

View File

@ -1,5 +1,7 @@
# Senior Developer Review - Validation Checklist
## Story Setup
- [ ] Story file loaded from `{{story_path}}`
- [ ] Story Status verified as reviewable (review)
- [ ] Epic and Story IDs resolved ({{epic_num}}.{{story_num}})
@ -7,12 +9,33 @@
- [ ] Epic Tech Spec located or warning recorded
- [ ] Architecture/standards docs loaded (as available)
- [ ] Tech stack detected and documented
- [ ] MCP doc search performed (or web fallback) and references captured
## External Agent Detection (Runtime)
- [ ] `invoke-bash cmd="command -v codex"` executed → {{codex_available}}
- [ ] `invoke-bash cmd="command -v gemini"` executed → {{gemini_available}}
- [ ] `invoke-bash cmd="command -v claude"` executed → {{claude_available}}
- [ ] Review method determined: {{use_external_agent}} = true/false
- [ ] If external: {{external_agent_cmd}} = codex OR gemini OR claude
- [ ] Config updated with detection results and timestamp
## Code Review Execution
- [ ] Git vs Story discrepancies identified ({{git_findings}})
- [ ] If external agent available: Prompt written to /tmp/code-review-prompt.txt
- [ ] If external agent available: CLI invoked via `invoke-bash` (MANDATORY - NO EXCEPTIONS)
- [ ] External agent output captured in {{bash_stdout}}
- [ ] If external agent CLI failed (non-zero exit): Fallback to built-in review
- [ ] ⚠️ VIOLATION CHECK: Did you skip external agent with a rationalization? If yes, RE-RUN with external agent.
- [ ] Acceptance Criteria cross-checked against implementation
- [ ] File List reviewed and validated for completeness
- [ ] Tests identified and mapped to ACs; gaps noted
- [ ] Code quality review performed on changed files
- [ ] Security review performed on changed files and dependencies
- [ ] Code quality review performed (security, performance, maintainability)
- [ ] Minimum 3 issues found (adversarial review requirement)
## Finalization
- [ ] Findings categorized: HIGH/MEDIUM/LOW severity
- [ ] Outcome decided (Approve/Changes Requested/Blocked)
- [ ] Review notes appended under "Senior Developer Review (AI)"
- [ ] Change Log updated with review entry
@ -21,3 +44,4 @@
- [ ] Story saved successfully
_Reviewer: {{user_name}} on {{date}}_
_External Agent: {{external_agent_cmd}} (codex:{{codex_available}} / gemini:{{gemini_available}} / claude:{{claude_available}})_

View File

@ -0,0 +1,35 @@
You are an ADVERSARIAL code reviewer. Your job is to find problems, not approve code.
VERY IMPORTANT!
- This is a READ ONLY operation. You are not to change anything in this code.
- You are FORBIDDEN to write to any files.
- You are FORBIDDEN to change any files.
- You are FORBIDDEN to delete any files.
REQUIREMENTS:
- Find 3-10 specific issues minimum - no lazy looks good reviews
- Categorize as HIGH (must fix), MEDIUM (should fix), LOW (nice to fix)
- For each issue: specify file:line, describe problem, suggest fix
- Check: Security vulnerabilities, performance issues, error handling, test quality
- Verify: Tasks marked [x] are actually done, ACs are actually implemented
STORY CONTEXT: {{story_path}}
FILES TO REVIEW: {{comprehensive_file_list}}
ACCEPTANCE CRITERIA: {{acceptance_criteria_list}}
TASKS: {{task_list}}
OUTPUT FORMAT:
## HIGH SEVERITY
- [file:line] Issue description | Suggested fix
## MEDIUM SEVERITY
- [file:line] Issue description | Suggested fix
## LOW SEVERITY
- [file:line] Issue description | Suggested fix

View File

@ -4,16 +4,35 @@
<critical>Communicate all responses in {communication_language} and language MUST be tailored to {user_skill_level}</critical>
<critical>Generate all documents in {document_output_language}</critical>
<critical>🔥 YOU ARE AN ADVERSARIAL CODE REVIEWER - Find what's wrong or missing! 🔥</critical>
<!-- ================================================================ -->
<!-- EXTERNAL AGENT MANDATE - THIS IS THE MOST IMPORTANT RULE -->
<!-- ================================================================ -->
<critical>🚨 MANDATORY EXTERNAL AGENT RULE - NO EXCEPTIONS 🚨</critical>
<critical>If an external agent CLI (codex, gemini, or claude) is detected as available, you MUST delegate the code review to that agent.</critical>
<critical>You are FORBIDDEN from performing your own code review analysis if an external agent is available.</critical>
<critical>The external agent provides an independent, unbiased review. Your job is to INVOKE it, not replace it.</critical>
<critical>Only perform built-in review if ALL external agents fail detection OR the CLI invocation actually fails with a non-zero exit code.</critical>
<critical>DO NOT SKIP the invoke-bash commands for detection and invocation - they are MANDATORY.</critical>
<!-- PROHIBITED EXCUSES - DO NOT USE THESE TO SKIP EXTERNAL AGENT -->
<critical>🚫 PROHIBITED RATIONALIZATIONS - You may NOT skip the external agent for ANY of these reasons:</critical>
<critical>❌ "The prompt is too long" - Long prompts are expected and supported. Invoke anyway.</critical>
<critical>❌ "CLI is meant for simple operations" - FALSE. The CLI handles complex prompts. Invoke anyway.</critical>
<critical>❌ "This is a re-review" - Re-reviews MUST use external agent. No exception.</critical>
<critical>❌ "I can do this myself" - You are FORBIDDEN from self-review when external agent is available.</critical>
<critical>❌ "It would be faster/better if I do it" - Irrelevant. External agent is MANDATORY.</critical>
<critical>❌ "The context is too complex" - The external agent handles complexity. Invoke anyway.</critical>
<critical>If you find yourself rationalizing why to skip the external agent, STOP and invoke it anyway.</critical>
<critical>🔥 ADVERSARIAL CODE REVIEW REQUIREMENTS 🔥</critical>
<critical>Your purpose: Validate story file claims against actual implementation</critical>
<critical>Challenge everything: Are tasks marked [x] actually done? Are ACs really implemented?</critical>
<critical>Find 3-10 specific issues in every review minimum - no lazy "looks good" reviews - YOU are so much better than the dev agent
that wrote this slop</critical>
<critical>Find 3-10 specific issues in every review minimum - no lazy "looks good" reviews</critical>
<critical>Read EVERY file in the File List - verify implementation against story requirements</critical>
<critical>Tasks marked complete but not done = CRITICAL finding</critical>
<critical>Acceptance Criteria not implemented = HIGH severity finding</critical>
<step n="1" goal="Load story and discover changes">
<step n="1" goal="Load story and detect external agents">
<action>Use provided {{story_path}} or ask user which story file to review</action>
<action>Read COMPLETE story file</action>
<action>Set {{story_key}} = extracted key from filename (e.g., "1-2-user-authentication.md" → "1-2-user-authentication") or story metadata</action>
@ -38,6 +57,86 @@
<invoke-protocol name="discover_inputs" />
<action>Load {project_context} for coding standards (if exists)</action>
<!-- ============================================================== -->
<!-- EXTERNAL AGENT DETECTION - CHECK CONFIG FIRST, THEN DETECT -->
<!-- ============================================================== -->
<set-var name="use_external_agent" value="false" />
<set-var name="external_agent_cmd" value="" />
<set-var name="codex_available" value="false" />
<set-var name="gemini_available" value="false" />
<set-var name="claude_available" value="false" />
<set-var name="external_agent_failed" value="false" />
<set-var name="preferred_agent" value="{external_review_agent}" />
<!-- Check if user has disabled external agents -->
<check if="{{preferred_agent}} == 'none'">
<output>📋 External agent disabled in config - will use built-in adversarial review</output>
</check>
<!-- Only detect and use external agents if not set to "none" -->
<check if="{{preferred_agent}} != 'none'">
<output>🔍 Detecting external agent availability...</output>
<!-- Detect Codex CLI availability -->
<invoke-bash cmd="command -v codex &amp;&amp; codex --version 2>/dev/null || echo 'NOT_FOUND'" />
<check if="{{bash_exit_code}} == 0 AND {{bash_stdout}} does not contain 'NOT_FOUND'">
<set-var name="codex_available" value="true" />
<output>✓ Codex CLI detected</output>
</check>
<!-- Detect Gemini CLI availability -->
<invoke-bash cmd="command -v gemini &amp;&amp; gemini --version 2>/dev/null || echo 'NOT_FOUND'" />
<check if="{{bash_exit_code}} == 0 AND {{bash_stdout}} does not contain 'NOT_FOUND'">
<set-var name="gemini_available" value="true" />
<output>✓ Gemini CLI detected</output>
</check>
<!-- Detect Claude CLI availability -->
<invoke-bash cmd="command -v claude &amp;&amp; claude --version 2>/dev/null || echo 'NOT_FOUND'" />
<check if="{{bash_exit_code}} == 0 AND {{bash_stdout}} does not contain 'NOT_FOUND'">
<set-var name="claude_available" value="true" />
<output>✓ Claude CLI detected</output>
</check>
<!-- Select which external agent to use based on availability and preference -->
<check if="{{preferred_agent}} == 'codex' AND {{codex_available}} == true">
<set-var name="use_external_agent" value="true" />
<set-var name="external_agent_cmd" value="codex" />
</check>
<check if="{{preferred_agent}} == 'gemini' AND {{gemini_available}} == true">
<set-var name="use_external_agent" value="true" />
<set-var name="external_agent_cmd" value="gemini" />
</check>
<check if="{{preferred_agent}} == 'claude' AND {{claude_available}} == true">
<set-var name="use_external_agent" value="true" />
<set-var name="external_agent_cmd" value="claude" />
</check>
<!-- Fallback selection if preferred agent not available -->
<check if="{{use_external_agent}} == false AND {{codex_available}} == true">
<set-var name="use_external_agent" value="true" />
<set-var name="external_agent_cmd" value="codex" />
<output>⚠️ Preferred agent ({{preferred_agent}}) not available, falling back to Codex</output>
</check>
<check if="{{use_external_agent}} == false AND {{gemini_available}} == true">
<set-var name="use_external_agent" value="true" />
<set-var name="external_agent_cmd" value="gemini" />
<output>⚠️ Preferred agent ({{preferred_agent}}) not available, falling back to Gemini</output>
</check>
<check if="{{use_external_agent}} == false AND {{claude_available}} == true">
<set-var name="use_external_agent" value="true" />
<set-var name="external_agent_cmd" value="claude" />
<output>⚠️ Preferred agent ({{preferred_agent}}) not available, falling back to Claude</output>
</check>
<check if="{{use_external_agent}} == true">
<output>🤖 External agent selected: {{external_agent_cmd}} - will delegate code review</output>
</check>
<check if="{{use_external_agent}} == false">
<output>📋 No external agent available - will use built-in adversarial review</output>
</check>
</check>
</step>
<step n="2" goal="Build review attack plan">
@ -56,41 +155,105 @@
<step n="3" goal="Execute adversarial review">
<critical>VALIDATE EVERY CLAIM - Check git reality vs story claims</critical>
<!-- Git vs Story Discrepancies -->
<!-- Git vs Story Discrepancies - ALWAYS runs -->
<action>Review git vs story File List discrepancies:
1. **Files changed but not in story File List** → MEDIUM finding (incomplete documentation)
2. **Story lists files but no git changes** → HIGH finding (false claims)
3. **Uncommitted changes not documented** → MEDIUM finding (transparency issue)
</action>
<!-- Use combined file list: story File List + git discovered files -->
<action>Create comprehensive review file list from story File List and git changes</action>
<action>Store git discrepancy findings in {{git_findings}}</action>
<!-- AC Validation -->
<action>For EACH Acceptance Criterion:
1. Read the AC requirement
2. Search implementation files for evidence
3. Determine: IMPLEMENTED, PARTIAL, or MISSING
4. If MISSING/PARTIAL → HIGH SEVERITY finding
</action>
<!-- ============================================================== -->
<!-- MANDATORY: INVOKE EXTERNAL AGENT IF AVAILABLE -->
<!-- ============================================================== -->
<critical>If {{use_external_agent}} == true, you MUST invoke the external agent via CLI.</critical>
<critical>DO NOT perform your own code review - delegate to the external agent.</critical>
<!-- Task Completion Audit -->
<action>For EACH task marked [x]:
1. Read the task description
2. Search files for evidence it was actually done
3. **CRITICAL**: If marked [x] but NOT DONE → CRITICAL finding
4. Record specific proof (file:line)
</action>
<check if="{{use_external_agent}} == true">
<output>🔄 Invoking {{external_agent_cmd}} CLI for adversarial code review...</output>
<!-- Code Quality Deep Dive -->
<action>For EACH file in comprehensive review list:
1. **Security**: Look for injection risks, missing validation, auth issues
2. **Performance**: N+1 queries, inefficient loops, missing caching
3. **Error Handling**: Missing try/catch, poor error messages
4. **Code Quality**: Complex functions, magic numbers, poor naming
5. **Test Quality**: Are tests real assertions or placeholders?
</action>
<!-- ============================================================== -->
<!-- INVOKE EXTERNAL AGENT - USE EXACT COMMANDS AS WRITTEN -->
<!-- ============================================================== -->
<critical>🚨 USE EXACT COMMAND SYNTAX - DO NOT MODIFY OR SIMPLIFY 🚨</critical>
<critical>Copy the invoke-bash cmd attribute EXACTLY as written below.</critical>
<critical>DO NOT remove flags, reorder arguments, or "improve" the command.</critical>
<!-- External agent prompt is loaded from external-agent-prompt.md -->
<set-var name="external_prompt_file" value="{installed_path}/external-agent-prompt.md" />
<action>Load {{external_prompt_file}} content into {{external_prompt}}</action>
<check if="{{external_agent_cmd}} == 'codex'">
<critical>CODEX: Use codex exec with read-only sandbox and full-auto</critical>
<invoke-bash cmd="codex exec --sandbox read-only --full-auto &quot;$(cat '{{external_prompt_file}}')&quot;" timeout="300000" />
</check>
<check if="{{external_agent_cmd}} == 'gemini'">
<critical>GEMINI: Use gemini -p with prompt from file and --yolo</critical>
<invoke-bash cmd="gemini -p &quot;$(cat '{{external_prompt_file}}')&quot; --yolo" timeout="300000" />
</check>
<check if="{{external_agent_cmd}} == 'claude'">
<critical>CLAUDE: Use claude -p with prompt from file</critical>
<invoke-bash cmd="claude -p &quot;$(cat '{{external_prompt_file}}')&quot; --dangerously-skip-permissions" timeout="300000" />
</check>
<check if="{{bash_exit_code}} != 0 OR {{bash_stdout}} is empty">
<output>⚠️ External agent CLI failed (exit code: {{bash_exit_code}}), falling back to built-in review</output>
<output>Error: {{bash_stderr}}</output>
<set-var name="use_external_agent" value="false" />
<set-var name="external_agent_failed" value="true" />
</check>
<check if="{{bash_exit_code}} == 0 AND {{bash_stdout}} is not empty">
<set-var name="external_findings" value="{{bash_stdout}}" />
<action>Parse {{external_findings}} into structured HIGH/MEDIUM/LOW lists</action>
<action>Merge {{git_findings}} with {{external_findings}} into {{all_findings}}</action>
<output>✅ External review complete - {{external_agent_cmd}} CLI findings received</output>
</check>
</check>
<!-- Fallback to built-in if external agent failed -->
<check if="{{external_agent_failed}} == true">
<set-var name="use_external_agent" value="false" />
</check>
<check if="{{use_external_agent}} == false">
<!-- ============================================================== -->
<!-- FALLBACK ONLY: Built-in Review (when NO external agent works) -->
<!-- ============================================================== -->
<critical>This section should ONLY execute if ALL external agents failed detection or invocation.</critical>
<critical>If you are here but an external agent was available, you have violated the workflow rules.</critical>
<output>⚠️ No external agent available - performing built-in adversarial review</output>
<!-- AC Validation -->
<action>For EACH Acceptance Criterion:
1. Read the AC requirement
2. Search implementation files for evidence
3. Determine: IMPLEMENTED, PARTIAL, or MISSING
4. If MISSING/PARTIAL → HIGH SEVERITY finding
</action>
<!-- Task Completion Audit -->
<action>For EACH task marked [x]:
1. Read the task description
2. Search files for evidence it was actually done
3. **CRITICAL**: If marked [x] but NOT DONE → CRITICAL finding
4. Record specific proof (file:line)
</action>
<!-- Code Quality Deep Dive -->
<action>For EACH file in comprehensive review list:
1. **Security**: Look for injection risks, missing validation, auth issues
2. **Performance**: N+1 queries, inefficient loops, missing caching
3. **Error Handling**: Missing try/catch, poor error messages
4. **Code Quality**: Complex functions, magic numbers, poor naming
5. **Test Quality**: Are tests real assertions or placeholders?
</action>
<action>Merge {{git_findings}} with built-in findings into {{all_findings}}</action>
</check>
<!-- Minimum issue check - applies to both paths -->
<check if="total_issues_found lt 3">
<critical>NOT LOOKING HARD ENOUGH - Find more problems!</critical>
<action>Re-examine code for:
@ -113,6 +276,7 @@
<output>**🔥 CODE REVIEW FINDINGS, {user_name}!**
**Story:** {{story_file}}
**Review Method:** {{external_agent_cmd}} OR built-in
**Git vs Story Discrepancies:** {{git_discrepancy_count}} found
**Issues Found:** {{high_count}} High, {{medium_count}} Medium, {{low_count}} Low
@ -185,7 +349,7 @@
<action>Set {{current_sprint_status}} = "no-sprint-tracking"</action>
</check>
<!-- Sync sprint-status.yaml when story status changes (only if sprint tracking enabled) -->
<!-- Sync sprint-status.yaml when story status changes -->
<check if="{{current_sprint_status}} != 'no-sprint-tracking'">
<action>Load the FULL file: {sprint_status}</action>
<action>Find development_status key matching {{story_key}}</action>

View File

@ -18,6 +18,7 @@ sprint_status: "{sprint_artifacts}/sprint-status.yaml || {output_folder}/sprint-
installed_path: "{project-root}/{bmad_folder}/bmm/workflows/4-implementation/code-review"
instructions: "{installed_path}/instructions.xml"
validation: "{installed_path}/checklist.md"
external_agent_prompt: "{installed_path}/external-agent-prompt.md"
template: false
variables:
@ -25,6 +26,11 @@ variables:
project_context: "**/project-context.md"
story_dir: "{sprint_artifacts}"
# External code review agent configuration
# User selects preferred agent during install; detection verifies availability at runtime
# Supported values: codex, gemini, claude, none
external_review_agent: "{config_source}:external_review_agent || 'none'"
# Smart input file references - handles both whole docs and sharded docs
# Priority: Whole document first, then sharded version
# Strategy: SELECTIVE LOAD - only load the specific epic needed for this story review
@ -51,4 +57,3 @@ input_file_patterns:
load_strategy: "INDEX_GUIDED"
standalone: true
web_bundle: false

View File

@ -8,7 +8,7 @@ const chalk = require('chalk');
*
* @param {Object} options - Installation options
* @param {string} options.projectRoot - The root directory of the target project
* @param {Object} options.config - Module configuration from install-config.yaml
* @param {Object} options.config - Module configuration from module.yaml
* @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

View File

@ -98,7 +98,7 @@ The installer is a multi-stage system that handles agent compilation, IDE integr
```
1. Collect User Input
- Target directory, modules, IDEs
- Custom module configuration (via install-config.yaml)
- Custom module configuration (via module.yaml)
2. Pre-Installation
- Validate target, check conflicts, backup existing installations
@ -183,12 +183,12 @@ The installer supports **15 IDE environments** through a base-derived architectu
### Custom Module Configuration
Modules define interactive configuration menus via `install-config.yaml` files in their `_module-installer/` directories.
Modules define interactive configuration menus via `module.yaml` files in their `_module-installer/` directories.
**Config File Location**:
- Core: `src/core/_module-installer/install-config.yaml`
- Modules: `src/modules/{module}/_module-installer/install-config.yaml`
- Core: `src/core/module.yaml`
- Modules: `src/modules/{module}/module.yaml`
**Configuration Types**:

View File

@ -132,8 +132,12 @@ class ConfigCollector {
* Collect configuration for all modules
* @param {Array} modules - List of modules to configure (including 'core')
* @param {string} projectDir - Target project directory
* @param {Object} options - Additional options
* @param {Map} options.customModulePaths - Map of module ID to source path for custom modules
*/
async collectAllConfigurations(modules, projectDir) {
async collectAllConfigurations(modules, projectDir, options = {}) {
// Store custom module paths for use in collectModuleConfig
this.customModulePaths = options.customModulePaths || new Map();
await this.loadExistingConfig(projectDir);
// Check if core was already collected (e.g., in early collection phase)
@ -183,24 +187,28 @@ class ConfigCollector {
// Load module's install config schema
// First, try the standard src/modules location
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml');
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
let moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
// If not found in src/modules, we need to find it by searching the project
if (!(await fs.pathExists(installerConfigPath))) {
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source
const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'install-config.yaml');
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
}
}
let configPath = null;
let isCustomModule = false;
if (await fs.pathExists(installerConfigPath)) {
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else {
// Check if this is a custom module with custom.yaml
@ -447,23 +455,37 @@ class ConfigCollector {
this.allAnswers = {};
}
// Load module's config
// First, try the standard src/modules location
let installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'install-config.yaml');
// First, check if we have a custom module path for this module
let installerConfigPath = null;
let moduleConfigPath = null;
// If not found in src/modules, we need to find it by searching the project
if (!(await fs.pathExists(installerConfigPath))) {
if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
const customPath = this.customModulePaths.get(moduleName);
installerConfigPath = path.join(customPath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(customPath, 'module.yaml');
} else {
// Try the standard src/modules location
installerConfigPath = path.join(getModulePath(moduleName), '_module-installer', 'module.yaml');
moduleConfigPath = path.join(getModulePath(moduleName), 'module.yaml');
}
// If not found in src/modules or custom paths, search the project
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(moduleConfigPath))) {
// Use the module manager to find the module source
const { ModuleManager } = require('../modules/manager');
const moduleManager = new ModuleManager();
const moduleSourcePath = await moduleManager.findModuleSource(moduleName);
if (moduleSourcePath) {
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'install-config.yaml');
installerConfigPath = path.join(moduleSourcePath, '_module-installer', 'module.yaml');
moduleConfigPath = path.join(moduleSourcePath, 'module.yaml');
}
}
let configPath = null;
if (await fs.pathExists(installerConfigPath)) {
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else {
// No config for this module

View File

@ -0,0 +1,239 @@
/**
* Custom Module Source Cache
* Caches custom module sources under _cfg/custom/ to ensure they're never lost
* and can be checked into source control
*/
const fs = require('fs-extra');
const path = require('node:path');
const crypto = require('node:crypto');
class CustomModuleCache {
constructor(bmadDir) {
this.bmadDir = bmadDir;
this.customCacheDir = path.join(bmadDir, '_cfg', 'custom');
this.manifestPath = path.join(this.customCacheDir, 'cache-manifest.yaml');
}
/**
* Ensure the custom cache directory exists
*/
async ensureCacheDir() {
await fs.ensureDir(this.customCacheDir);
}
/**
* Get cache manifest
*/
async getCacheManifest() {
if (!(await fs.pathExists(this.manifestPath))) {
return {};
}
const content = await fs.readFile(this.manifestPath, 'utf8');
const yaml = require('js-yaml');
return yaml.load(content) || {};
}
/**
* Update cache manifest
*/
async updateCacheManifest(manifest) {
const yaml = require('js-yaml');
const content = yaml.dump(manifest, {
indent: 2,
lineWidth: -1,
noRefs: true,
sortKeys: false,
});
await fs.writeFile(this.manifestPath, content);
}
/**
* Calculate hash of a file or directory
*/
async calculateHash(sourcePath) {
const hash = crypto.createHash('sha256');
const isDir = (await fs.stat(sourcePath)).isDirectory();
if (isDir) {
// For directories, hash all files
const files = [];
async function collectFiles(dir) {
const entries = await fs.readdir(dir, { withFileTypes: true });
for (const entry of entries) {
if (entry.isFile()) {
files.push(path.join(dir, entry.name));
} else if (entry.isDirectory() && !entry.name.startsWith('.')) {
await collectFiles(path.join(dir, entry.name));
}
}
}
await collectFiles(sourcePath);
files.sort(); // Ensure consistent order
for (const file of files) {
const content = await fs.readFile(file);
const relativePath = path.relative(sourcePath, file);
hash.update(relativePath + '|' + content.toString('base64'));
}
} else {
// For single files
const content = await fs.readFile(sourcePath);
hash.update(content);
}
return hash.digest('hex');
}
/**
* Cache a custom module source
* @param {string} moduleId - Module ID
* @param {string} sourcePath - Original source path
* @param {Object} metadata - Additional metadata to store
* @returns {Object} Cached module info
*/
async cacheModule(moduleId, sourcePath, metadata = {}) {
await this.ensureCacheDir();
const cacheDir = path.join(this.customCacheDir, moduleId);
const cacheManifest = await this.getCacheManifest();
// Check if already cached and unchanged
if (cacheManifest[moduleId]) {
const cached = cacheManifest[moduleId];
if (cached.originalHash && cached.originalHash === (await this.calculateHash(sourcePath))) {
// Source unchanged, return existing cache info
return {
moduleId,
cachePath: cacheDir,
...cached,
};
}
}
// Remove existing cache if it exists
if (await fs.pathExists(cacheDir)) {
await fs.remove(cacheDir);
}
// Copy module to cache
await fs.copy(sourcePath, cacheDir, {
filter: (src) => {
const relative = path.relative(sourcePath, src);
// Skip node_modules, .git, and other common ignore patterns
return !relative.includes('node_modules') && !relative.startsWith('.git') && !relative.startsWith('.DS_Store');
},
});
// Calculate hash of the source
const sourceHash = await this.calculateHash(sourcePath);
const cacheHash = await this.calculateHash(cacheDir);
// Update manifest - don't store originalPath for source control friendliness
cacheManifest[moduleId] = {
originalHash: sourceHash,
cacheHash: cacheHash,
cachedAt: new Date().toISOString(),
...metadata,
};
await this.updateCacheManifest(cacheManifest);
return {
moduleId,
cachePath: cacheDir,
...cacheManifest[moduleId],
};
}
/**
* Get cached module info
* @param {string} moduleId - Module ID
* @returns {Object|null} Cached module info or null
*/
async getCachedModule(moduleId) {
const cacheManifest = await this.getCacheManifest();
const cached = cacheManifest[moduleId];
if (!cached) {
return null;
}
const cacheDir = path.join(this.customCacheDir, moduleId);
if (!(await fs.pathExists(cacheDir))) {
// Cache dir missing, remove from manifest
delete cacheManifest[moduleId];
await this.updateCacheManifest(cacheManifest);
return null;
}
// Verify cache integrity
const currentCacheHash = await this.calculateHash(cacheDir);
if (currentCacheHash !== cached.cacheHash) {
console.warn(`Warning: Cache integrity check failed for ${moduleId}`);
}
return {
moduleId,
cachePath: cacheDir,
...cached,
};
}
/**
* Get all cached modules
* @returns {Array} Array of cached module info
*/
async getAllCachedModules() {
const cacheManifest = await this.getCacheManifest();
const cached = [];
for (const [moduleId, info] of Object.entries(cacheManifest)) {
const cachedModule = await this.getCachedModule(moduleId);
if (cachedModule) {
cached.push(cachedModule);
}
}
return cached;
}
/**
* Remove a cached module
* @param {string} moduleId - Module ID to remove
*/
async removeCachedModule(moduleId) {
const cacheManifest = await this.getCacheManifest();
const cacheDir = path.join(this.customCacheDir, moduleId);
// Remove cache directory
if (await fs.pathExists(cacheDir)) {
await fs.remove(cacheDir);
}
// Remove from manifest
delete cacheManifest[moduleId];
await this.updateCacheManifest(cacheManifest);
}
/**
* Sync cached modules with a list of module IDs
* @param {Array<string>} moduleIds - Module IDs to keep
*/
async syncCache(moduleIds) {
const cached = await this.getAllCachedModules();
for (const cachedModule of cached) {
if (!moduleIds.includes(cachedModule.moduleId)) {
await this.removeCachedModule(cachedModule.moduleId);
}
}
}
}
module.exports = { CustomModuleCache };

View File

@ -17,6 +17,7 @@ class Detector {
hasCore: false,
modules: [],
ides: [],
customModules: [],
manifest: null,
};
@ -32,6 +33,10 @@ class Detector {
result.manifest = manifestData;
result.version = manifestData.version;
result.installed = true;
// Copy custom modules if they exist
if (manifestData.customModules) {
result.customModules = manifestData.customModules;
}
}
// Check for core
@ -275,10 +280,9 @@ class Detector {
hasV6Installation = true;
// Don't break - continue scanning to be thorough
} else {
// Not V6+, check if folder name contains "bmad" (case insensitive)
const nameLower = name.toLowerCase();
if (nameLower.includes('bmad')) {
// Potential V4 legacy folder
// Not V6+, check if this is the exact V4 folder name "bmad-method"
if (name === 'bmad-method') {
// This is the V4 default folder - flag it as legacy
potentialV4Folders.push(fullPath);
}
}

View File

@ -22,6 +22,7 @@ const path = require('node:path');
const fs = require('fs-extra');
const chalk = require('chalk');
const ora = require('ora');
const inquirer = require('inquirer');
const { Detector } = require('./detector');
const { Manifest } = require('./manifest');
const { ModuleManager } = require('../modules/manager');
@ -129,7 +130,7 @@ class Installer {
*/
async copyFileWithPlaceholderReplacement(sourcePath, targetPath, bmadFolderName) {
// List of text file extensions that should have placeholder replacement
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv'];
const textExtensions = ['.md', '.yaml', '.yml', '.txt', '.json', '.js', '.ts', '.html', '.css', '.sh', '.bat', '.csv', '.xml'];
const ext = path.extname(sourcePath).toLowerCase();
// Check if this is a text file that might contain placeholders
@ -434,8 +435,53 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// Quick update already collected all configs, use them directly
moduleConfigs = this.configCollector.collectedConfig;
} 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) {
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
for (const customFile of config.customContent.selectedFiles) {
const customInfo = await customHandler.getCustomInfo(customFile, path.resolve(config.directory));
if (customInfo && customInfo.id) {
customModulePaths.set(customInfo.id, customInfo.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) {
// Get selected cached module IDs (if available)
const selectedCachedIds = config.customContent.selectedCachedModules || [];
// If no selection info, include all cached modules (for backward compatibility)
const shouldIncludeAll = selectedCachedIds.length === 0 && config.customContent.selected;
for (const cachedModule of config.customContent.cachedModules) {
// For cached modules, the path is the cachePath which contains the module.yaml
if (
cachedModule.id &&
cachedModule.cachePath && // Include if selected or if we should include all
(shouldIncludeAll || selectedCachedIds.includes(cachedModule.id))
) {
customModulePaths.set(cachedModule.id, cachedModule.cachePath);
}
}
}
// Get list of all modules including custom modules
const allModulesForConfig = [...(config.modules || [])];
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(config.modules || [], path.resolve(config.directory));
moduleConfigs = await this.configCollector.collectAllConfigurations(allModulesForConfig, path.resolve(config.directory), {
customModulePaths,
});
}
// Get bmad_folder from config (default to 'bmad' for backwards compatibility)
@ -750,13 +796,81 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
spinner.text = 'Creating directory structure...';
await this.createDirectoryStructure(bmadDir);
// Resolve dependencies for selected modules
spinner.text = 'Resolving dependencies...';
// Get project root
const projectRoot = getProjectRoot();
const modulesToInstall = config.installCore ? ['core', ...config.modules] : config.modules;
// Step 1: Install core module first (if requested)
if (config.installCore) {
spinner.start('Installing BMAD core...');
await this.installCoreWithDependencies(bmadDir, { core: {} });
spinner.succeed('Core installed');
// Generate core config file
await this.generateModuleConfigs(bmadDir, { core: config.coreConfig || {} });
}
// Custom content is already handled in UI before module selection
let finalCustomContent = config.customContent;
// Step 3: Prepare modules list including cached custom modules
let allModules = [...(config.modules || [])];
// During quick update, we might have custom module sources from the manifest
if (config._customModuleSources) {
// Add custom modules from stored sources
for (const [moduleId, customInfo] of config._customModuleSources) {
if (!allModules.includes(moduleId) && (await fs.pathExists(customInfo.sourcePath))) {
allModules.push(moduleId);
}
}
}
// Add cached custom modules
if (finalCustomContent && finalCustomContent.cachedModules) {
for (const cachedModule of finalCustomContent.cachedModules) {
if (!allModules.includes(cachedModule.id)) {
allModules.push(cachedModule.id);
}
}
}
// Regular custom content from user input (non-cached)
if (finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
// Add custom modules to the installation list
for (const customFile of finalCustomContent.selectedFiles) {
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
if (customInfo && customInfo.id) {
allModules.push(customInfo.id);
}
}
}
// Don't include core again if already installed
if (config.installCore) {
allModules = allModules.filter((m) => m !== 'core');
}
const modulesToInstall = allModules;
// For dependency resolution, we need to pass the project root
const resolution = await this.dependencyResolver.resolve(projectRoot, config.modules || [], { verbose: config.verbose });
// 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
});
// 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, {
verbose: config.verbose,
moduleManager: tempModuleManager,
});
if (config.verbose) {
spinner.succeed('Dependencies resolved');
@ -764,24 +878,164 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
spinner.succeed('Dependencies resolved');
}
// Install core if requested or if dependencies require it
if (config.installCore || resolution.byModule.core) {
spinner.start('Installing BMAD core...');
await this.installCoreWithDependencies(bmadDir, resolution.byModule.core);
spinner.succeed('Core installed');
}
// Core is already installed above, skip if included in resolution
// Install modules with their dependencies
if (config.modules && config.modules.length > 0) {
for (const moduleName of config.modules) {
if (allModules && allModules.length > 0) {
const installedModuleNames = new Set();
for (const moduleName of allModules) {
// Skip if already installed
if (installedModuleNames.has(moduleName)) {
continue;
}
installedModuleNames.add(moduleName);
spinner.start(`Installing module: ${moduleName}...`);
await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
// Check if this is a custom module
let isCustomModule = false;
let customInfo = null;
let useCache = false;
// First check if we have a cached version
if (finalCustomContent && finalCustomContent.cachedModules) {
const cachedModule = finalCustomContent.cachedModules.find((m) => m.id === moduleName);
if (cachedModule) {
isCustomModule = true;
customInfo = {
id: moduleName,
path: cachedModule.cachePath,
config: {},
};
useCache = true;
}
}
// Then check if we have custom module sources from the manifest (for quick update)
if (!isCustomModule && config._customModuleSources && config._customModuleSources.has(moduleName)) {
customInfo = config._customModuleSources.get(moduleName);
isCustomModule = true;
// Check if this is a cached module (source path starts with _cfg)
if (customInfo.sourcePath && (customInfo.sourcePath.startsWith('_cfg') || customInfo.sourcePath.includes('_cfg/custom'))) {
useCache = true;
// Make sure we have the right path structure
if (!customInfo.path) {
customInfo.path = customInfo.sourcePath;
}
}
}
// Finally check regular custom content
if (!isCustomModule && finalCustomContent && finalCustomContent.selected && finalCustomContent.selectedFiles) {
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
for (const customFile of finalCustomContent.selectedFiles) {
const info = await customHandler.getCustomInfo(customFile, projectDir);
if (info && info.id === moduleName) {
isCustomModule = true;
customInfo = info;
break;
}
}
}
if (isCustomModule && customInfo) {
// Install custom module using CustomHandler but as a proper module
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
// Install to module directory instead of custom directory
const moduleTargetPath = path.join(bmadDir, moduleName);
await fs.ensureDir(moduleTargetPath);
// 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 },
(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);
},
);
// 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);
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);
}
}
await fs.remove(tempCustomPath);
}
// 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 _cfg
sourcePath =
customInfo.sourcePath && customInfo.sourcePath.startsWith('_cfg')
? 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
if (moduleName === 'core') {
await this.installCoreWithDependencies(bmadDir, resolution.byModule[moduleName]);
} else {
await this.installModuleWithDependencies(moduleName, bmadDir, resolution.byModule[moduleName]);
}
}
spinner.succeed(`Module installed: ${moduleName}`);
}
// Install partial modules (only dependencies)
for (const [module, files] of Object.entries(resolution.byModule)) {
if (!config.modules.includes(module) && module !== 'core') {
if (!allModules.includes(module) && module !== 'core') {
const totalFiles =
files.agents.length +
files.tasks.length +
@ -799,6 +1053,11 @@ 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 &&
@ -806,12 +1065,26 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
config.customContent.selected &&
config.customContent.selectedFiles
) {
spinner.start('Installing custom content...');
// Filter out custom modules that were already installed
for (const customFile of config.customContent.selectedFiles) {
const { CustomHandler } = require('../custom/handler');
const customHandler = new CustomHandler();
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 } = require('../custom/handler');
const customHandler = new CustomHandler();
// Use the selected files instead of finding all files
const customFiles = config.customContent.selectedFiles;
// Use the remaining files
const customFiles = remainingCustomContent;
if (customFiles.length > 0) {
console.log(chalk.cyan(`\n Found ${customFiles.length} custom content file(s):`));
@ -867,14 +1140,37 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
spinner.start('Generating workflow and agent manifests...');
const manifestGen = new ManifestGenerator();
// Include preserved modules (from quick update) in the manifest
const allModulesToList = config._preserveModules ? [...(config.modules || []), ...config._preserveModules] : config.modules || [];
// For quick update, we need ALL installed modules in the manifest
// Not just the ones being updated
const allModulesForManifest = config._quickUpdate
? config._existingModules || allModules || []
: config._preserveModules
? [...allModules, ...config._preserveModules]
: allModules || [];
const manifestStats = await manifestGen.generateManifests(bmadDir, config.modules || [], this.installedFiles, {
// For regular installs (including when called from quick update), use what we have
let modulesForCsvPreserve;
if (config._quickUpdate) {
// Quick update - use existing modules or fall back to modules being updated
modulesForCsvPreserve = config._existingModules || allModules || [];
} else {
// Regular install - use the modules we're installing plus any preserved ones
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
}
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, this.installedFiles, {
ides: config.ides || [],
preservedModules: config._preserveModules || [], // Scan these from installed bmad/ dir
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);
}
}
spinner.succeed(
`Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`,
);
@ -1137,6 +1433,30 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const currentVersion = existingInstall.version;
const newVersion = require(path.join(getProjectRoot(), 'package.json')).version;
// Check for custom modules with missing sources before update
const customModuleSources = new Map();
if (existingInstall.customModules) {
for (const customModule of existingInstall.customModules) {
customModuleSources.set(customModule.id, customModule);
}
}
if (customModuleSources.size > 0) {
spinner.stop();
console.log(chalk.yellow('\nChecking custom module sources before update...'));
const projectRoot = getProjectRoot();
await this.handleMissingCustomSources(
customModuleSources,
bmadDir,
projectRoot,
'update',
existingInstall.modules.map((m) => m.id),
);
spinner.start('Preparing update...');
}
if (config.dryRun) {
spinner.stop();
console.log(chalk.cyan('\n🔍 Update Preview (Dry Run)\n'));
@ -1594,6 +1914,9 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
// DO NOT replace {project-root} - LLMs understand this placeholder at runtime
// const processedContent = xmlContent.replaceAll('{project-root}', projectDir);
// Replace {bmad_folder} with actual folder name
xmlContent = xmlContent.replaceAll('{bmad_folder}', this.bmadFolderName || 'bmad');
// Replace {agent_sidecar_folder} if configured
const coreConfig = this.configCollector.collectedConfig.core || {};
if (coreConfig.agent_sidecar_folder && xmlContent.includes('{agent_sidecar_folder}')) {
@ -1905,6 +2228,24 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
throw new Error(`BMAD not installed at ${bmadDir}`);
}
// Check for custom modules with missing sources
const manifest = await this.manifest.read(bmadDir);
if (manifest && manifest.customModules && manifest.customModules.length > 0) {
spinner.stop();
console.log(chalk.yellow('\nChecking custom module sources before compilation...'));
const customModuleSources = new Map();
for (const customModule of manifest.customModules) {
customModuleSources.set(customModule.id, customModule);
}
const projectRoot = getProjectRoot();
const installedModules = manifest.modules || [];
await this.handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, 'compile-agents', installedModules);
spinner.start('Rebuilding agent files...');
}
let agentCount = 0;
let taskCount = 0;
@ -2049,17 +2390,245 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
const existingInstall = await this.detector.detect(bmadDir);
const installedModules = existingInstall.modules.map((m) => m.id);
const configuredIdes = existingInstall.ides || [];
const projectRoot = path.dirname(bmadDir);
// Get custom module sources from manifest
const customModuleSources = new Map();
if (existingInstall.customModules) {
for (const customModule of existingInstall.customModules) {
// Ensure we have an absolute sourcePath
let absoluteSourcePath = customModule.sourcePath;
// Check if sourcePath is a cache-relative path (starts with _cfg/)
if (absoluteSourcePath && absoluteSourcePath.startsWith('_cfg')) {
// 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);
}
// Update the custom module object with the absolute path
const updatedModule = {
...customModule,
sourcePath: absoluteSourcePath,
};
customModuleSources.set(customModule.id, updatedModule);
}
}
// Load saved IDE configurations
const savedIdeConfigs = await this.ideConfigManager.loadAllIdeConfigs(bmadDir);
// Get available modules (what we have source for)
const availableModules = await this.moduleManager.listAvailable();
const availableModuleIds = new Set(availableModules.map((m) => m.id));
const availableModulesData = await this.moduleManager.listAvailable();
const availableModules = [...availableModulesData.modules, ...availableModulesData.customModules];
// Add custom modules from manifest if their sources exist
for (const [moduleId, customModule] of customModuleSources) {
// Use the absolute sourcePath
const sourcePath = customModule.sourcePath;
// Check if source exists at the recorded path
if (
sourcePath &&
(await fs.pathExists(sourcePath)) && // Add to available modules if not already there
!availableModules.some((m) => m.id === moduleId)
) {
availableModules.push({
id: moduleId,
name: customModule.name || moduleId,
path: sourcePath,
isCustom: true,
fromManifest: true,
});
}
}
// 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,
bmadDir,
projectRoot,
'update',
installedModules,
);
// Handle both old return format (array) and new format (object)
let validCustomModules = [];
let keptModulesWithoutSources = [];
if (Array.isArray(customModuleResult)) {
// Old format - just an array
validCustomModules = customModuleResult;
} else if (customModuleResult && typeof customModuleResult === 'object') {
// New format - object with two arrays
validCustomModules = customModuleResult.validCustomModules || [];
keptModulesWithoutSources = customModuleResult.keptModulesWithoutSources || [];
}
const customModulesFromManifest = validCustomModules.map((m) => ({
...m,
isCustom: true,
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));
// Core module is special - never include it in update flow
const nonCoreInstalledModules = installedModules.filter((id) => id !== 'core');
// Only update modules that are BOTH installed AND available (we have source for)
const modulesToUpdate = installedModules.filter((id) => availableModuleIds.has(id));
const skippedModules = installedModules.filter((id) => !availableModuleIds.has(id));
const modulesToUpdate = nonCoreInstalledModules.filter((id) => availableModuleIds.has(id));
const skippedModules = nonCoreInstalledModules.filter((id) => !availableModuleIds.has(id));
// Add custom modules that were kept without sources to the skipped modules
// This ensures their agents are preserved in the manifest
for (const keptModule of keptModulesWithoutSources) {
if (!skippedModules.includes(keptModule)) {
skippedModules.push(keptModule);
}
}
spinner.succeed(`Found ${modulesToUpdate.length} module(s) to update and ${configuredIdes.length} configured tool(s)`);
@ -2124,6 +2693,8 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
_quickUpdate: true, // Flag to skip certain prompts
_preserveModules: skippedModules, // Preserve these in manifest even though we didn't update them
_savedIdeConfigs: savedIdeConfigs, // Pass saved IDE configs to installer
_customModuleSources: customModuleSources, // Pass custom module sources for updates
_existingModules: installedModules, // Pass all installed modules for manifest generation
};
// Call the standard install method
@ -2763,6 +3334,230 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
}
}
}
/**
* Handle missing custom module sources interactively
* @param {Map} customModuleSources - Map of custom module ID to info
* @param {string} bmadDir - BMAD directory
* @param {string} projectRoot - Project root directory
* @param {string} operation - Current operation ('update', 'compile', etc.)
* @param {Array} installedModules - Array of installed module IDs (will be modified)
* @returns {Object} Object with validCustomModules array and keptModulesWithoutSources array
*/
async handleMissingCustomSources(customModuleSources, bmadDir, projectRoot, operation, installedModules) {
const validCustomModules = [];
const keptModulesWithoutSources = []; // Track modules kept without sources
const customModulesWithMissingSources = [];
// Check which sources exist
for (const [moduleId, customInfo] of customModuleSources) {
if (await fs.pathExists(customInfo.sourcePath)) {
validCustomModules.push({
id: moduleId,
name: customInfo.name,
path: customInfo.sourcePath,
info: customInfo,
});
} else {
customModulesWithMissingSources.push({
id: moduleId,
name: customInfo.name,
sourcePath: customInfo.sourcePath,
relativePath: customInfo.relativePath,
info: customInfo,
});
}
}
// If no missing sources, return immediately
if (customModulesWithMissingSources.length === 0) {
return validCustomModules;
}
// Stop any spinner for interactive prompts
const currentSpinner = ora();
if (currentSpinner.isSpinning) {
currentSpinner.stop();
}
console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
const inquirer = require('inquirer');
let keptCount = 0;
let updatedCount = 0;
let removedCount = 0;
for (const missing of customModulesWithMissingSources) {
console.log(chalk.dim(`${missing.name} (${missing.id})`));
console.log(chalk.dim(` Original source: ${missing.relativePath}`));
console.log(chalk.dim(` Full path: ${missing.sourcePath}`));
const choices = [
{
name: 'Keep installed (will not be processed)',
value: 'keep',
short: 'Keep',
},
{
name: 'Specify new source location',
value: 'update',
short: 'Update',
},
];
// Only add remove option if not just compiling agents
if (operation !== 'compile-agents') {
choices.push({
name: '⚠️ REMOVE module completely (destructive!)',
value: 'remove',
short: 'Remove',
});
}
const { action } = await inquirer.prompt([
{
type: 'list',
name: 'action',
message: `How would you like to handle "${missing.name}"?`,
choices,
},
]);
switch (action) {
case 'update': {
const { newSourcePath } = await inquirer.prompt([
{
type: 'input',
name: 'newSourcePath',
message: 'Enter the new path to the custom module:',
default: missing.sourcePath,
validate: async (input) => {
if (!input || input.trim() === '') {
return 'Please enter a path';
}
const expandedPath = path.resolve(input.trim());
if (!(await fs.pathExists(expandedPath))) {
return 'Path does not exist';
}
// Check if it looks like a valid module
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
const agentsPath = path.join(expandedPath, 'agents');
const workflowsPath = path.join(expandedPath, 'workflows');
if (!(await fs.pathExists(moduleYamlPath)) && !(await fs.pathExists(agentsPath)) && !(await fs.pathExists(workflowsPath))) {
return 'Path does not appear to contain a valid custom module';
}
return true;
},
},
]);
// Update the source in manifest
const resolvedPath = path.resolve(newSourcePath.trim());
missing.info.sourcePath = resolvedPath;
// Remove relativePath - we only store absolute sourcePath now
delete missing.info.relativePath;
await this.manifest.addCustomModule(bmadDir, missing.info);
validCustomModules.push({
id: moduleId,
name: missing.name,
path: resolvedPath,
info: missing.info,
});
updatedCount++;
console.log(chalk.green(`✓ Updated source location`));
break;
}
case 'remove': {
// Extra confirmation for destructive remove
console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
console.log(chalk.red(` Module location: ${path.join(bmadDir, moduleId)}`));
const { confirm } = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
default: false,
},
]);
if (confirm) {
const { typedConfirm } = await inquirer.prompt([
{
type: 'input',
name: 'typedConfirm',
message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
validate: (input) => {
if (input !== 'DELETE') {
return chalk.red('You must type "DELETE" exactly to proceed');
}
return true;
},
},
]);
if (typedConfirm === 'DELETE') {
// Remove the module from filesystem and manifest
const modulePath = path.join(bmadDir, moduleId);
if (await fs.pathExists(modulePath)) {
const fsExtra = require('fs-extra');
await fsExtra.remove(modulePath);
console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`));
}
await this.manifest.removeModule(bmadDir, moduleId);
await this.manifest.removeCustomModule(bmadDir, moduleId);
console.log(chalk.yellow(` ✓ Removed from manifest`));
// Also remove from installedModules list
if (installedModules && installedModules.includes(moduleId)) {
const index = installedModules.indexOf(moduleId);
if (index !== -1) {
installedModules.splice(index, 1);
}
}
removedCount++;
console.log(chalk.red.bold(`✓ "${missing.name}" has been permanently removed`));
} else {
console.log(chalk.dim(' Removal cancelled - module will be kept'));
keptCount++;
}
} else {
console.log(chalk.dim(' Removal cancelled - module will be kept'));
keptCount++;
}
break;
}
case 'keep': {
keptCount++;
keptModulesWithoutSources.push(moduleId);
console.log(chalk.dim(` Module will be kept as-is`));
break;
}
// No default
}
}
// Show summary
if (keptCount > 0 || updatedCount > 0 || removedCount > 0) {
console.log(chalk.dim(`\nSummary for custom modules with missing sources:`));
if (keptCount > 0) console.log(chalk.dim(`${keptCount} module(s) kept as-is`));
if (updatedCount > 0) console.log(chalk.dim(`${updatedCount} module(s) updated with new sources`));
if (removedCount > 0) console.log(chalk.red(`${removedCount} module(s) permanently deleted`));
}
return {
validCustomModules,
keptModulesWithoutSources,
};
}
}
module.exports = { Installer };

View File

@ -41,7 +41,11 @@ class ManifestGenerator {
// 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
this.preservedModules = preservedModules; // These stay as-is in CSVs
// 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.bmadDir = bmadDir;
this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '.bmad' or 'bmad')
this.allInstalledFiles = installedFiles;
@ -61,14 +65,14 @@ class ManifestGenerator {
// Collect workflow data
await this.collectWorkflows(selectedModules);
// Collect agent data
await this.collectAgents(selectedModules);
// Collect agent data - use updatedModules which includes all installed modules
await this.collectAgents(this.updatedModules);
// Collect task data
await this.collectTasks(selectedModules);
await this.collectTasks(this.updatedModules);
// Collect tool data
await this.collectTools(selectedModules);
await this.collectTools(this.updatedModules);
// Write manifest files and collect their paths
const manifestFiles = [
@ -450,6 +454,21 @@ 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.load(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,
@ -457,6 +476,7 @@ class ManifestGenerator {
lastUpdated: new Date().toISOString(),
},
modules: this.modules,
customModules: existingCustomModules, // Preserve custom modules
ides: this.selectedIdes,
};
@ -562,12 +582,47 @@ class ManifestGenerator {
async writeWorkflowManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'workflow-manifest.csv');
// Read existing manifest to preserve entries
const existingEntries = new Map();
if (await fs.pathExists(csvPath)) {
const content = await fs.readFile(csvPath, 'utf8');
const lines = content.split('\n').filter((line) => line.trim());
// Skip header
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (line) {
// Parse CSV (simple parsing assuming no commas in quoted fields)
const parts = line.split('","');
if (parts.length >= 4) {
const name = parts[0].replace(/^"/, '');
const module = parts[2];
existingEntries.set(`${module}:${name}`, line);
}
}
}
}
// Create CSV header - removed standalone column as ALL workflows now generate commands
let csv = 'name,description,module,path\n';
// Add all workflows - no standalone property needed anymore
// Combine existing and new workflows
const allWorkflows = new Map();
// Add existing entries
for (const [key, value] of existingEntries) {
allWorkflows.set(key, value);
}
// Add/update new workflows
for (const workflow of this.workflows) {
csv += `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}"\n`;
const key = `${workflow.module}:${workflow.name}`;
allWorkflows.set(key, `"${workflow.name}","${workflow.description}","${workflow.module}","${workflow.path}"`);
}
// Write all workflows
for (const [, value] of allWorkflows) {
csv += value + '\n';
}
await fs.writeFile(csvPath, csv);
@ -581,12 +636,50 @@ class ManifestGenerator {
async writeAgentManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'agent-manifest.csv');
// Read existing manifest to preserve entries
const existingEntries = new Map();
if (await fs.pathExists(csvPath)) {
const content = await fs.readFile(csvPath, 'utf8');
const lines = content.split('\n').filter((line) => line.trim());
// Skip header
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (line) {
// Parse CSV (simple parsing assuming no commas in quoted fields)
const parts = line.split('","');
if (parts.length >= 11) {
const name = parts[0].replace(/^"/, '');
const module = parts[8];
existingEntries.set(`${module}:${name}`, line);
}
}
}
}
// Create CSV header with persona fields
let csv = 'name,displayName,title,icon,role,identity,communicationStyle,principles,module,path\n';
// Add all agents
// Combine existing and new agents, preferring new data for duplicates
const allAgents = new Map();
// Add existing entries
for (const [key, value] of existingEntries) {
allAgents.set(key, value);
}
// Add/update new agents
for (const agent of this.agents) {
csv += `"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"\n`;
const key = `${agent.module}:${agent.name}`;
allAgents.set(
key,
`"${agent.name}","${agent.displayName}","${agent.title}","${agent.icon}","${agent.role}","${agent.identity}","${agent.communicationStyle}","${agent.principles}","${agent.module}","${agent.path}"`,
);
}
// Write all agents
for (const [, value] of allAgents) {
csv += value + '\n';
}
await fs.writeFile(csvPath, csv);
@ -600,12 +693,47 @@ class ManifestGenerator {
async writeTaskManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'task-manifest.csv');
// Read existing manifest to preserve entries
const existingEntries = new Map();
if (await fs.pathExists(csvPath)) {
const content = await fs.readFile(csvPath, 'utf8');
const lines = content.split('\n').filter((line) => line.trim());
// Skip header
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (line) {
// Parse CSV (simple parsing assuming no commas in quoted fields)
const parts = line.split('","');
if (parts.length >= 6) {
const name = parts[0].replace(/^"/, '');
const module = parts[3];
existingEntries.set(`${module}:${name}`, line);
}
}
}
}
// Create CSV header with standalone column
let csv = 'name,displayName,description,module,path,standalone\n';
// Add all tasks
// Combine existing and new tasks
const allTasks = new Map();
// Add existing entries
for (const [key, value] of existingEntries) {
allTasks.set(key, value);
}
// Add/update new tasks
for (const task of this.tasks) {
csv += `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"\n`;
const key = `${task.module}:${task.name}`;
allTasks.set(key, `"${task.name}","${task.displayName}","${task.description}","${task.module}","${task.path}","${task.standalone}"`);
}
// Write all tasks
for (const [, value] of allTasks) {
csv += value + '\n';
}
await fs.writeFile(csvPath, csv);
@ -619,12 +747,47 @@ class ManifestGenerator {
async writeToolManifest(cfgDir) {
const csvPath = path.join(cfgDir, 'tool-manifest.csv');
// Read existing manifest to preserve entries
const existingEntries = new Map();
if (await fs.pathExists(csvPath)) {
const content = await fs.readFile(csvPath, 'utf8');
const lines = content.split('\n').filter((line) => line.trim());
// Skip header
for (let i = 1; i < lines.length; i++) {
const line = lines[i];
if (line) {
// Parse CSV (simple parsing assuming no commas in quoted fields)
const parts = line.split('","');
if (parts.length >= 6) {
const name = parts[0].replace(/^"/, '');
const module = parts[3];
existingEntries.set(`${module}:${name}`, line);
}
}
}
}
// Create CSV header with standalone column
let csv = 'name,displayName,description,module,path,standalone\n';
// Add all tools
// Combine existing and new tools
const allTools = new Map();
// Add existing entries
for (const [key, value] of existingEntries) {
allTools.set(key, value);
}
// Add/update new tools
for (const tool of this.tools) {
csv += `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"\n`;
const key = `${tool.module}:${tool.name}`;
allTools.set(key, `"${tool.name}","${tool.displayName}","${tool.description}","${tool.module}","${tool.path}","${tool.standalone}"`);
}
// Write all tools
for (const [, value] of allTools) {
csv += value + '\n';
}
await fs.writeFile(csvPath, csv);

View File

@ -61,6 +61,7 @@ class Manifest {
installDate: manifestData.installation?.installDate,
lastUpdated: manifestData.installation?.lastUpdated,
modules: manifestData.modules || [],
customModules: manifestData.customModules || [],
ides: manifestData.ides || [],
};
} catch (error) {
@ -93,6 +94,7 @@ class Manifest {
lastUpdated: manifest.lastUpdated,
},
modules: manifest.modules || [],
customModules: manifest.customModules || [],
ides: manifest.ides || [],
};
@ -535,6 +537,51 @@ class Manifest {
return configs;
}
/**
* Add a custom module to the manifest with its source path
* @param {string} bmadDir - Path to bmad directory
* @param {Object} customModule - Custom module info
*/
async addCustomModule(bmadDir, customModule) {
const manifest = await this.read(bmadDir);
if (!manifest) {
throw new Error('No manifest found');
}
if (!manifest.customModules) {
manifest.customModules = [];
}
// Check if custom module already exists
const existingIndex = manifest.customModules.findIndex((m) => m.id === customModule.id);
if (existingIndex === -1) {
// Add new entry
manifest.customModules.push(customModule);
} else {
// Update existing entry
manifest.customModules[existingIndex] = customModule;
}
await this.update(bmadDir, { customModules: manifest.customModules });
}
/**
* Remove a custom module from the manifest
* @param {string} bmadDir - Path to bmad directory
* @param {string} moduleId - Module ID to remove
*/
async removeCustomModule(bmadDir, moduleId) {
const manifest = await this.read(bmadDir);
if (!manifest || !manifest.customModules) {
return;
}
const index = manifest.customModules.findIndex((m) => m.id === moduleId);
if (index !== -1) {
manifest.customModules.splice(index, 1);
await this.update(bmadDir, { customModules: manifest.customModules });
}
}
}
module.exports = { Manifest };

View File

@ -3,6 +3,7 @@ const fs = require('fs-extra');
const chalk = require('chalk');
const yaml = require('js-yaml');
const { FileOps } = require('../../../lib/file-ops');
const { XmlHandler } = require('../../../lib/xml-handler');
/**
* Handler for custom content (custom.yaml)
@ -11,6 +12,7 @@ const { FileOps } = require('../../../lib/file-ops');
class CustomHandler {
constructor() {
this.fileOps = new FileOps();
this.xmlHandler = new XmlHandler();
}
/**
@ -52,6 +54,12 @@ class CustomHandler {
} else if (entry.name === 'custom.yaml') {
// Found a custom.yaml file
customPaths.push(fullPath);
} else if (
entry.name === 'module.yaml' && // Check if this is a custom module (either in _module-installer or in root directory)
// Skip if it's in src/modules (those are standard modules)
!fullPath.includes(path.join('src', 'modules'))
) {
customPaths.push(fullPath);
}
}
} catch {
@ -66,40 +74,44 @@ class CustomHandler {
}
/**
* Get custom content info from a custom.yaml file
* @param {string} customYamlPath - Path to custom.yaml file
* Get custom content info from a custom.yaml or module.yaml file
* @param {string} configPath - Path to config file
* @param {string} projectRoot - Project root directory for calculating relative paths
* @returns {Object|null} Custom content info
*/
async getCustomInfo(customYamlPath, projectRoot = null) {
async getCustomInfo(configPath, projectRoot = null) {
try {
const configContent = await fs.readFile(customYamlPath, 'utf8');
const configContent = await fs.readFile(configPath, 'utf8');
// Try to parse YAML with error handling
let config;
try {
config = yaml.load(configContent);
} catch (parseError) {
console.warn(chalk.yellow(`Warning: YAML parse error in ${customYamlPath}:`, parseError.message));
console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message));
return null;
}
const customDir = path.dirname(customYamlPath);
// Check if this is an module.yaml (module) or custom.yaml (custom content)
const isInstallConfig = configPath.endsWith('module.yaml');
const configDir = path.dirname(configPath);
// Use provided projectRoot or fall back to process.cwd()
const basePath = projectRoot || process.cwd();
const relativePath = path.relative(basePath, customDir);
const relativePath = path.relative(basePath, configDir);
return {
id: config.code || path.basename(customDir),
name: config.name || `Custom: ${path.basename(customDir)}`,
description: config.description || 'Custom agents and workflows',
path: customDir,
id: config.code || 'unknown-code',
name: config.name,
description: config.description || '',
path: configDir,
relativePath: relativePath,
defaultSelected: config.default_selected === true,
config: config,
isInstallConfig: isInstallConfig, // Track which type this is
};
} catch (error) {
console.warn(chalk.yellow(`Warning: Failed to read ${customYamlPath}:`, error.message));
console.warn(chalk.yellow(`Warning: Failed to read ${configPath}:`, error.message));
return null;
}
}
@ -131,10 +143,10 @@ class CustomHandler {
await fs.ensureDir(bmadAgentsDir);
await fs.ensureDir(bmadWorkflowsDir);
// Process agents - copy entire agents directory structure
// Process agents - compile and copy agents
const agentsDir = path.join(customPath, 'agents');
if (await fs.pathExists(agentsDir)) {
await this.copyDirectory(agentsDir, bmadAgentsDir, results, fileTrackingCallback, config);
await this.compileAndCopyAgents(agentsDir, bmadAgentsDir, bmadDir, config, fileTrackingCallback, results);
// Count agent files
const agentFiles = await this.findFilesRecursively(agentsDir, ['.agent.yaml', '.md']);
@ -271,6 +283,114 @@ class CustomHandler {
}
}
}
/**
* Compile .agent.yaml files to .md format and handle sidecars
* @param {string} sourceAgentsPath - Source agents directory
* @param {string} targetAgentsPath - Target agents directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} config - Configuration for placeholder replacement
* @param {Function} fileTrackingCallback - Optional callback to track installed files
* @param {Object} results - Results object to update
*/
async compileAndCopyAgents(sourceAgentsPath, targetAgentsPath, bmadDir, config, fileTrackingCallback, results) {
// Get all .agent.yaml files recursively
const agentFiles = await this.findFilesRecursively(sourceAgentsPath, ['.agent.yaml']);
for (const agentFile of agentFiles) {
const relativePath = path.relative(sourceAgentsPath, agentFile);
const targetDir = path.join(targetAgentsPath, path.dirname(relativePath));
await fs.ensureDir(targetDir);
const agentName = path.basename(agentFile, '.agent.yaml');
const targetMdPath = path.join(targetDir, `${agentName}.md`);
// Use the actual bmadDir if available (for when installing to temp dir)
const actualBmadDir = config._bmadDir || bmadDir;
const customizePath = path.join(actualBmadDir, '_cfg', 'agents', `custom-${agentName}.customize.yaml`);
// Read and compile the YAML
try {
const yamlContent = await fs.readFile(agentFile, 'utf8');
const { compileAgent } = require('../../../lib/agent/compiler');
// Create customize template if it doesn't exist
if (!(await fs.pathExists(customizePath))) {
const { getSourcePath } = require('../../../lib/project-root');
const genericTemplatePath = getSourcePath('utility', 'templates', 'agent.customize.template.yaml');
if (await fs.pathExists(genericTemplatePath)) {
// Copy with placeholder replacement
let templateContent = await fs.readFile(genericTemplatePath, 'utf8');
templateContent = templateContent.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
await fs.writeFile(customizePath, templateContent, 'utf8');
console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`));
}
}
// Compile the agent
const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config });
// Replace placeholders in the compiled content
let processedXml = xml;
processedXml = processedXml.replaceAll('{bmad_folder}', config.bmad_folder || 'bmad');
processedXml = processedXml.replaceAll('{user_name}', config.user_name || 'User');
processedXml = processedXml.replaceAll('{communication_language}', config.communication_language || 'English');
processedXml = processedXml.replaceAll('{output_folder}', config.output_folder || 'docs');
// Write the compiled MD file
await fs.writeFile(targetMdPath, processedXml, 'utf8');
// Check if agent has sidecar
let hasSidecar = false;
try {
const yamlLib = require('yaml');
const agentYaml = yamlLib.parse(yamlContent);
hasSidecar = agentYaml?.agent?.metadata?.hasSidecar === true;
} catch {
// Continue without sidecar processing
}
// Copy sidecar files if agent has hasSidecar flag
if (hasSidecar && config.agent_sidecar_folder) {
const { copyAgentSidecarFiles } = require('../../../lib/agent/installer');
// Resolve agent sidecar folder path
const projectDir = path.dirname(bmadDir);
const resolvedSidecarFolder = config.agent_sidecar_folder
.replaceAll('{project-root}', projectDir)
.replaceAll('{bmad_folder}', path.basename(bmadDir));
// Create sidecar directory for this agent
const agentSidecarDir = path.join(resolvedSidecarFolder, agentName);
await fs.ensureDir(agentSidecarDir);
// Copy sidecar files
const sidecarResult = copyAgentSidecarFiles(path.dirname(agentFile), agentSidecarDir, agentFile);
if (sidecarResult.copied.length > 0) {
console.log(chalk.dim(` Copied ${sidecarResult.copied.length} sidecar file(s) to: ${agentSidecarDir}`));
}
if (sidecarResult.preserved.length > 0) {
console.log(chalk.dim(` Preserved ${sidecarResult.preserved.length} existing sidecar file(s)`));
}
}
// Track the file
if (fileTrackingCallback) {
fileTrackingCallback(targetMdPath);
}
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}`);
}
}
}
}
module.exports = { CustomHandler };

View File

@ -22,11 +22,12 @@ const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/p
* await manager.install('core-module', '/path/to/bmad');
*/
class ModuleManager {
constructor() {
constructor(options = {}) {
// Path to source modules directory
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
}
/**
@ -106,7 +107,7 @@ class ModuleManager {
}
/**
* Find all modules in the project by searching for install-config.yaml files
* Find all modules in the project by searching for module.yaml files
* @returns {Array} List of module paths
*/
async findModulesInProject() {
@ -143,12 +144,14 @@ class ModuleManager {
continue;
}
// Check if this directory contains a module (install-config.yaml OR custom.yaml)
const installerConfigPath = path.join(fullPath, '_module-installer', 'install-config.yaml');
// 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))
@ -175,10 +178,11 @@ class ModuleManager {
/**
* List all available modules (excluding core which is always installed)
* @returns {Array} List of available modules with metadata
* @returns {Object} Object with modules array and customModules array
*/
async listAvailable() {
const modules = [];
const customModules = [];
// First, scan src/modules (the standard location)
if (await fs.pathExists(this.modulesSourcePath)) {
@ -187,12 +191,17 @@ class ModuleManager {
for (const entry of entries) {
if (entry.isDirectory()) {
const modulePath = path.join(this.modulesSourcePath, entry.name);
// Check for module structure (install-config.yaml OR custom.yaml)
const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml');
// Check for module structure (module.yaml OR custom.yaml)
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');
// Skip if this doesn't look like a module
if (!(await fs.pathExists(installerConfigPath)) && !(await fs.pathExists(customConfigPath))) {
if (
!(await fs.pathExists(moduleConfigPath)) &&
!(await fs.pathExists(installerConfigPath)) &&
!(await fs.pathExists(customConfigPath))
) {
continue;
}
@ -209,25 +218,50 @@ class ModuleManager {
}
}
// Then, find all other modules in the project
const otherModulePaths = await this.findModulesInProject();
for (const modulePath of otherModulePaths) {
const moduleName = path.basename(modulePath);
const relativePath = path.relative(getProjectRoot(), modulePath);
// 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;
// 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);
}
}
}
const moduleInfo = await this.getModuleInfo(modulePath, moduleName, relativePath);
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id)) {
// Avoid duplicates - skip if we already have this module ID
modules.push(moduleInfo);
// Also check for cached custom modules in _cfg/custom/
if (this.bmadDir) {
const customCacheDir = path.join(this.bmadDir, '_cfg', '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, '_cfg/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);
}
}
}
}
}
}
return modules;
return { modules, customModules };
}
/**
@ -238,13 +272,16 @@ class ModuleManager {
* @returns {Object|null} Module info or null if not a valid module
*/
async getModuleInfo(modulePath, defaultName, sourceDescription) {
// Check for module structure (install-config.yaml OR custom.yaml)
const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml');
// Check for module structure (module.yaml OR custom.yaml)
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(installerConfigPath)) {
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else if (await fs.pathExists(customConfigPath)) {
configPath = customConfigPath;
@ -305,10 +342,11 @@ class ModuleManager {
// First, check src/modules
const srcModulePath = path.join(this.modulesSourcePath, moduleName);
if (await fs.pathExists(srcModulePath)) {
// Check if this looks like a module (has install-config.yaml)
const installerConfigPath = path.join(srcModulePath, '_module-installer', 'install-config.yaml');
// 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(installerConfigPath)) {
if ((await fs.pathExists(moduleConfigPath)) || (await fs.pathExists(installerConfigPath))) {
return srcModulePath;
}
@ -330,12 +368,15 @@ class ModuleManager {
// Also check by module ID (not just folder name)
// Need to read configs to match by ID
for (const modulePath of allModulePaths) {
const installerConfigPath = path.join(modulePath, '_module-installer', 'install-config.yaml');
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(installerConfigPath)) {
if (await fs.pathExists(moduleConfigPath)) {
configPath = moduleConfigPath;
} else if (await fs.pathExists(installerConfigPath)) {
configPath = installerConfigPath;
} else if (await fs.pathExists(customConfigPath)) {
configPath = customConfigPath;
@ -576,7 +617,7 @@ class ModuleManager {
}
// Skip _module-installer directory - it's only needed at install time
if (file.startsWith('_module-installer/')) {
if (file.startsWith('_module-installer/') || file === 'module.yaml') {
continue;
}
@ -812,8 +853,13 @@ class ModuleManager {
// Compile with customizations if any
const { xml } = compileAgent(yamlContent, {}, agentName, relativePath, { config: this.coreConfig });
// Write the compiled MD file
await fs.writeFile(targetMdPath, xml, 'utf8');
// Replace {bmad_folder} placeholder if needed
if (xml.includes('{bmad_folder}') && this.bmadFolderName) {
const processedXml = xml.replaceAll('{bmad_folder}', this.bmadFolderName);
await fs.writeFile(targetMdPath, processedXml, 'utf8');
} else {
await fs.writeFile(targetMdPath, xml, 'utf8');
}
// Copy sidecar files if agent has hasSidecar flag
if (hasSidecar) {

View File

@ -445,17 +445,9 @@ function compileAgent(yamlContent, answers = {}, agentName = '', targetPath = ''
// Parse YAML
const agentYaml = yaml.parse(yamlContent);
// Inject custom agent name into metadata.name if provided
// This is the user's chosen persona name (e.g., "Fred" instead of "Inkwell Von Comitizen")
if (agentName && agentYaml.agent && agentYaml.agent.metadata) {
// Convert kebab-case to title case for the name field
// e.g., "fred-commit-poet" → "Fred Commit Poet"
const titleCaseName = agentName
.split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
agentYaml.agent.metadata.name = titleCaseName;
}
// Note: agentName parameter is for UI display only, not for modifying the YAML
// The persona name (metadata.name) should always come from the YAML file
// We should NEVER modify metadata.name as it's part of the agent's identity
// Extract install_config
const installConfig = extractInstallConfig(agentYaml);

View File

@ -242,7 +242,8 @@ function installAgent(agentInfo, answers, targetPath, options = {}) {
const { xml, metadata, processedYaml } = compileAgent(fs.readFileSync(agentInfo.yamlFile, 'utf8'), answers);
// Determine target agent folder name
const agentFolderName = metadata.name ? metadata.name.toLowerCase().replaceAll(/\s+/g, '-') : agentInfo.name;
// Use the folder name from agentInfo, NOT the persona name from metadata
const agentFolderName = agentInfo.name;
const agentTargetDir = path.join(targetPath, agentFolderName);

View File

@ -3,6 +3,7 @@ const boxen = require('boxen');
const wrapAnsi = require('wrap-ansi');
const figlet = require('figlet');
const path = require('node:path');
const os = require('node:os');
const CLIUtils = {
/**
@ -84,8 +85,8 @@ const CLIUtils = {
/**
* Display module configuration header
* @param {string} moduleName - Module name (fallback if no custom header)
* @param {string} header - Custom header from install-config.yaml
* @param {string} subheader - Custom subheader from install-config.yaml
* @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml
*/
displayModuleConfigHeader(moduleName, header = null, subheader = null) {
// Simple blue banner with custom header/subheader if provided
@ -100,8 +101,8 @@ const CLIUtils = {
/**
* Display module with no custom configuration
* @param {string} moduleName - Module name (fallback if no custom header)
* @param {string} header - Custom header from install-config.yaml
* @param {string} subheader - Custom subheader from install-config.yaml
* @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml
*/
displayModuleNoConfig(moduleName, header = null, subheader = null) {
// Show full banner with header/subheader, just like modules with config
@ -205,6 +206,22 @@ const CLIUtils = {
// No longer clear screen or show boxes - just a simple completion message
// This is deprecated but kept for backwards compatibility
},
/**
* Expand path with ~ expansion
* @param {string} inputPath - Path to expand
* @returns {string} Expanded path
*/
expandPath(inputPath) {
if (!inputPath) return inputPath;
// Expand ~ to home directory
if (inputPath.startsWith('~')) {
return path.join(os.homedir(), inputPath.slice(1));
}
return inputPath;
},
};
module.exports = { CLIUtils };

View File

@ -52,9 +52,6 @@ class UI {
await installer.handleLegacyV4Migration(confirmedDirectory, legacyV4);
}
// Prompt for custom content location (separate from installation directory)
const customContentConfig = await this.promptCustomContentLocation();
// Check if there's an existing BMAD installation
const fs = require('fs-extra');
const path = require('node:path');
@ -62,6 +59,17 @@ class UI {
const bmadDir = await installer.findBmadDir(confirmedDirectory);
const hasExistingInstall = await fs.pathExists(bmadDir);
// Always ask for custom content, but we'll handle it differently for new installs
let customContentConfig = { hasCustomContent: false };
if (hasExistingInstall) {
// Existing installation - prompt to add/update custom content
customContentConfig = await this.promptCustomContentForExisting();
} else {
// New installation - we'll prompt after creating the directory structure
// For now, set a flag to indicate we should ask later
customContentConfig._shouldAsk = true;
}
// Track action type (only set if there's an existing installation)
let actionType;
@ -88,12 +96,11 @@ class UI {
// Handle quick update separately
if (actionType === 'quick-update') {
// Even for quick update, ask about custom content
const customContentConfig = await this.promptCustomContentLocation();
// Quick update doesn't install custom content - just updates existing modules
return {
actionType: 'quick-update',
directory: confirmedDirectory,
customContent: customContentConfig,
customContent: { hasCustomContent: false },
};
}
@ -123,6 +130,64 @@ class UI {
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
const coreConfig = await this.collectCoreConfig(confirmedDirectory);
// For new installations, create the directory structure first so we can cache custom content
if (!hasExistingInstall && customContentConfig._shouldAsk) {
// Create the bmad directory based on core config
const path = require('node:path');
const fs = require('fs-extra');
const bmadFolderName = coreConfig.bmad_folder || 'bmad';
const bmadDir = path.join(confirmedDirectory, bmadFolderName);
await fs.ensureDir(bmadDir);
await fs.ensureDir(path.join(bmadDir, '_cfg'));
await fs.ensureDir(path.join(bmadDir, '_cfg', 'custom'));
// Now prompt for custom content
customContentConfig = await this.promptCustomContentLocation();
// If custom content found, cache it
if (customContentConfig.hasCustomContent) {
const { CustomModuleCache } = require('../installers/lib/core/custom-module-cache');
const cache = new CustomModuleCache(bmadDir);
const { CustomHandler } = require('../installers/lib/custom/handler');
const customHandler = new CustomHandler();
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
for (const customFile of customFiles) {
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo && customInfo.id) {
// Cache the module source
await cache.cacheModule(customInfo.id, customInfo.path, {
name: customInfo.name,
type: 'custom',
});
console.log(chalk.dim(` Cached ${customInfo.name} to _cfg/custom/${customInfo.id}`));
}
}
// Update config to use cached modules
customContentConfig.cachedModules = [];
for (const customFile of customFiles) {
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo && customInfo.id) {
customContentConfig.cachedModules.push({
id: customInfo.id,
cachePath: path.join(bmadDir, '_cfg', 'custom', customInfo.id),
// Store relative path from cache for the manifest
relativePath: path.join('_cfg', 'custom', customInfo.id),
});
}
}
console.log(chalk.green(`✓ Cached ${customFiles.length} custom module(s)`));
}
// Clear the flag
delete customContentConfig._shouldAsk;
}
// Skip module selection during update/reinstall - keep existing modules
let selectedModules;
if (actionType === 'update' || actionType === 'reinstall') {
@ -136,15 +201,46 @@ class UI {
// Check which custom content items were selected
const selectedCustomContent = selectedModules.filter((mod) => mod.startsWith('__CUSTOM_CONTENT__'));
if (selectedCustomContent.length > 0) {
// For cached modules (new installs), check if any cached modules were selected
let selectedCachedModules = [];
if (customContentConfig.cachedModules) {
selectedCachedModules = selectedModules.filter(
(mod) => !mod.startsWith('__CUSTOM_CONTENT__') && customContentConfig.cachedModules.some((cm) => cm.id === mod),
);
}
if (selectedCustomContent.length > 0 || selectedCachedModules.length > 0) {
customContentConfig.selected = true;
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
// Filter out custom content markers since they're not real modules
selectedModules = selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__'));
// Handle directory-based custom content (existing installs)
if (selectedCustomContent.length > 0) {
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
// Convert custom content to module IDs for installation
const customContentModuleIds = [];
const { CustomHandler } = require('../installers/lib/custom/handler');
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];
}
// For cached modules, they're already module IDs, just mark as selected
if (selectedCachedModules.length > 0) {
customContentConfig.selectedCachedModules = selectedCachedModules;
// No need to filter since they're already proper module IDs
}
} else if (customContentConfig.hasCustomContent) {
// User provided custom content but didn't select any
customContentConfig.selected = false;
customContentConfig.selectedFiles = [];
customContentConfig.selectedCachedModules = [];
}
}
@ -511,42 +607,134 @@ class UI {
const moduleChoices = [];
const isNewInstallation = installedModuleIds.size === 0;
// Add custom content items first if found
if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
// Add separator before custom content
moduleChoices.push(new inquirer.Separator('── Custom Content ──'));
const customContentItems = [];
const hasCustomContentItems = false;
// Get the custom content info to display proper names
const { CustomHandler } = require('../installers/lib/custom/handler');
const customHandler = new CustomHandler();
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
// Add custom content items
if (customContentConfig && customContentConfig.hasCustomContent) {
if (customContentConfig.cachedModules) {
// New installation - show cached modules
for (const cachedModule of customContentConfig.cachedModules) {
// Get the module info from cache
const yaml = require('js-yaml');
const fs = require('fs-extra');
for (const customFile of customFiles) {
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo) {
moduleChoices.push({
name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`,
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
checked: true, // Default to selected since user chose to provide custom content
});
// Try multiple possible config file locations
const possibleConfigPaths = [
path.join(cachedModule.cachePath, 'module.yaml'),
path.join(cachedModule.cachePath, 'custom.yaml'),
path.join(cachedModule.cachePath, '_module-installer', 'module.yaml'),
path.join(cachedModule.cachePath, '_module-installer', 'custom.yaml'),
];
let moduleData = null;
let foundPath = null;
for (const configPath of possibleConfigPaths) {
if (await fs.pathExists(configPath)) {
try {
const yamlContent = await fs.readFile(configPath, 'utf8');
moduleData = yaml.load(yamlContent);
foundPath = configPath;
break;
} catch {
// Continue to next path
}
}
}
if (moduleData) {
// Use the name from the custom info if we have it
const moduleName = cachedModule.name || moduleData.name || cachedModule.id;
customContentItems.push({
name: `${chalk.cyan('✓')} ${moduleName} ${chalk.gray('(cached)')}`,
value: cachedModule.id, // Use module ID directly
checked: true, // Default to selected
cached: true,
});
} else {
// Debug: show what paths we tried to check
console.log(chalk.dim(`DEBUG: No module config found for ${cachedModule.id}`));
console.log(
chalk.dim(
`DEBUG: Tried paths:`,
possibleConfigPaths.map((p) => p.replace(cachedModule.cachePath, '.')),
),
);
console.log(chalk.dim(`DEBUG: cachedModule:`, JSON.stringify(cachedModule, null, 2)));
}
}
} else if (customContentConfig.customPath) {
// Existing installation - show from directory
const { CustomHandler } = require('../installers/lib/custom/handler');
const customHandler = new CustomHandler();
const customFiles = await customHandler.findCustomContent(customContentConfig.customPath);
for (const customFile of customFiles) {
const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo) {
customContentItems.push({
name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`,
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
checked: true, // Default to selected since user chose to provide custom content
path: customInfo.path, // Track path to avoid duplicates
});
}
}
}
// Add separator for official content
moduleChoices.push(new inquirer.Separator('── Official Content ──'));
}
// Add official modules
const { ModuleManager } = require('../installers/lib/modules/manager');
const moduleManager = new ModuleManager();
const availableModules = await moduleManager.listAvailable();
// 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();
// First, add all items to appropriate sections
const allCustomModules = [];
// Add custom content items from directory
allCustomModules.push(...customContentItems);
// Add custom modules from project scan (if scanning is enabled)
for (const mod of customModulesFromProject) {
// 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})`)}`,
value: mod.id,
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
});
}
}
// Add separators and modules in correct order
if (allCustomModules.length > 0) {
// Add separator for custom content, all custom modules, and official content separator
moduleChoices.push(
new inquirer.Separator('── Custom Content ──'),
...allCustomModules,
new inquirer.Separator('── Official Content ──'),
);
}
// Add official modules (only non-custom ones)
for (const mod of availableModules) {
moduleChoices.push({
name: mod.name,
value: mod.id,
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
});
if (!mod.isCustom) {
moduleChoices.push({
name: mod.name,
value: mod.id,
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
});
}
}
return moduleChoices;
@ -632,7 +820,7 @@ class UI {
*/
async promptCustomContentLocation() {
try {
CLIUtils.displaySection('Custom Content', 'Optional: Add custom agents and workflows');
CLIUtils.displaySection('Custom Content', 'Optional: Add custom agents, workflows, and modules');
const { hasCustomContent } = await inquirer.prompt([
{
@ -666,7 +854,7 @@ class UI {
{
type: 'input',
name: 'directory',
message: 'Enter the path to your custom content directory:',
message: 'Enter directory to search for custom content (will scan subfolders):',
default: process.cwd(), // Use actual current working directory
validate: async (input) => {
if (!input || input.trim() === '') {
@ -699,7 +887,7 @@ class UI {
const customFiles = await customHandler.findCustomContent(expandedPath);
if (customFiles.length === 0) {
console.log(chalk.yellow(`\nNo custom.yaml files found in ${expandedPath}`));
console.log(chalk.yellow(`\nNo custom content found in ${expandedPath}`));
const { tryAgain } = await inquirer.prompt([
{
@ -718,7 +906,12 @@ class UI {
}
customPath = expandedPath;
console.log(chalk.green(`\n✓ Found ${customFiles.length} custom content file(s)`));
console.log(chalk.green(`\n✓ Found ${customFiles.length} custom content item(s):`));
for (const file of customFiles) {
const relativePath = path.relative(expandedPath, path.dirname(file));
const folderName = path.dirname(file).split(path.sep).pop();
console.log(chalk.dim(`${folderName} ${chalk.gray(`(${relativePath})`)}`));
}
}
return { hasCustomContent: true, customPath };
@ -1016,6 +1209,144 @@ class UI {
return (await fs.pathExists(hookPath)) && (await fs.pathExists(playTtsPath));
}
/**
* Prompt for custom content for existing installations
* @returns {Object} Custom content configuration
*/
async promptCustomContentForExisting() {
try {
CLIUtils.displaySection('Custom Content', 'Add new custom agents, workflows, or modules to your installation');
const { hasCustomContent } = await inquirer.prompt([
{
type: 'list',
name: 'hasCustomContent',
message: 'Do you want to add or update custom content?',
choices: [
{
name: 'No, continue with current installation only',
value: false,
},
{
name: 'Yes, I have custom content to add or update',
value: true,
},
],
default: false,
},
]);
if (!hasCustomContent) {
return { hasCustomContent: false };
}
// Get directory path
const { customPath } = await inquirer.prompt([
{
type: 'input',
name: 'customPath',
message: 'Enter directory to search for custom content (will scan subfolders):',
default: process.cwd(),
validate: async (input) => {
if (!input || input.trim() === '') {
return 'Please enter a directory path';
}
// Normalize and check if path exists
const expandedPath = CLIUtils.expandPath(input.trim());
const pathExists = await fs.pathExists(expandedPath);
if (!pathExists) {
return 'Directory does not exist';
}
// Check if it's actually a directory
const stats = await fs.stat(expandedPath);
if (!stats.isDirectory()) {
return 'Path must be a directory';
}
return true;
},
transformer: (input) => {
return CLIUtils.expandPath(input);
},
},
]);
const resolvedPath = CLIUtils.expandPath(customPath);
// Find custom content
const { CustomHandler } = require('../installers/lib/custom/handler');
const customHandler = new CustomHandler();
const customFiles = await customHandler.findCustomContent(resolvedPath);
if (customFiles.length === 0) {
console.log(chalk.yellow(`\nNo custom content found in ${resolvedPath}`));
const { tryDifferent } = await inquirer.prompt([
{
type: 'confirm',
name: 'tryDifferent',
message: 'Try a different directory?',
default: true,
},
]);
if (tryDifferent) {
return await this.promptCustomContentForExisting();
}
return { hasCustomContent: false };
}
// Display found items
console.log(chalk.cyan(`\nFound ${customFiles.length} custom content file(s):`));
const { CustomHandler: CustomHandler2 } = require('../installers/lib/custom/handler');
const customHandler2 = new CustomHandler2();
const customContentItems = [];
for (const customFile of customFiles) {
const customInfo = await customHandler2.getCustomInfo(customFile);
if (customInfo) {
customContentItems.push({
name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`,
value: `__CUSTOM_CONTENT__${customFile}`,
checked: true,
});
}
}
// Add option to keep existing custom content
console.log(chalk.yellow('\nExisting custom modules will be preserved unless you remove them'));
const { selectedFiles } = await inquirer.prompt([
{
type: 'checkbox',
name: 'selectedFiles',
message: 'Select custom content to add:',
choices: customContentItems,
pageSize: 15,
validate: (answer) => {
if (answer.length === 0) {
return 'You must select at least one item';
}
return true;
},
},
]);
return {
hasCustomContent: true,
customPath: resolvedPath,
selected: true,
selectedFiles: selectedFiles,
};
} catch (error) {
console.error(chalk.red('Error configuring custom content:'), error);
return { hasCustomContent: false };
}
}
}
module.exports = { UI };

View File

@ -0,0 +1,124 @@
/**
* Migration script to convert relative paths to absolute paths in custom module manifests
* This should be run once to update existing installations
*/
const fs = require('fs-extra');
const path = require('node:path');
const yaml = require('yaml');
const chalk = require('chalk');
/**
* Find BMAD directory in project
*/
function findBmadDir(projectDir = process.cwd()) {
const possibleNames = ['bmad', '.bmad'];
for (const name of possibleNames) {
const bmadDir = path.join(projectDir, name);
if (fs.existsSync(bmadDir)) {
return bmadDir;
}
}
return null;
}
/**
* Update manifest to use absolute paths
*/
async function updateManifest(manifestPath, projectRoot) {
console.log(chalk.cyan(`\nUpdating manifest: ${manifestPath}`));
const content = await fs.readFile(manifestPath, 'utf8');
const manifest = yaml.parse(content);
if (!manifest.customModules || manifest.customModules.length === 0) {
console.log(chalk.dim(' No custom modules found'));
return false;
}
let updated = false;
for (const customModule of manifest.customModules) {
if (customModule.relativePath && !customModule.sourcePath) {
// Convert relative path to absolute
const absolutePath = path.resolve(projectRoot, customModule.relativePath);
customModule.sourcePath = absolutePath;
// Remove the old relativePath
delete customModule.relativePath;
console.log(chalk.green(` ✓ Updated ${customModule.id}: ${customModule.relativePath}${absolutePath}`));
updated = true;
} else if (customModule.sourcePath && !path.isAbsolute(customModule.sourcePath)) {
// Source path exists but is not absolute
const absolutePath = path.resolve(customModule.sourcePath);
customModule.sourcePath = absolutePath;
console.log(chalk.green(` ✓ Updated ${customModule.id}: ${customModule.sourcePath}${absolutePath}`));
updated = true;
}
}
if (updated) {
// Write back the updated manifest
const yamlStr = yaml.dump(manifest, {
indent: 2,
lineWidth: -1,
noRefs: true,
sortKeys: false,
});
await fs.writeFile(manifestPath, yamlStr);
console.log(chalk.green(' Manifest updated successfully'));
} else {
console.log(chalk.dim(' All paths already absolute'));
}
return updated;
}
/**
* Main migration function
*/
async function migrate(directory) {
const projectRoot = path.resolve(directory || process.cwd());
const bmadDir = findBmadDir(projectRoot);
if (!bmadDir) {
console.error(chalk.red('✗ No BMAD installation found in directory'));
process.exit(1);
}
console.log(chalk.blue.bold('🔄 BMAD Custom Module Path Migration'));
console.log(chalk.dim(`Project: ${projectRoot}`));
console.log(chalk.dim(`BMAD Directory: ${bmadDir}`));
const manifestPath = path.join(bmadDir, '_cfg', 'manifest.yaml');
if (!fs.existsSync(manifestPath)) {
console.error(chalk.red('✗ No manifest.yaml found'));
process.exit(1);
}
const updated = await updateManifest(manifestPath, projectRoot);
if (updated) {
console.log(chalk.green.bold('\n✨ Migration completed successfully!'));
console.log(chalk.dim('Custom modules now use absolute source paths.'));
} else {
console.log(chalk.yellow('\n⚠ No migration needed - paths already absolute'));
}
}
// Run migration if called directly
if (require.main === module) {
const directory = process.argv[2];
migrate(directory).catch((error) => {
console.error(chalk.red('\n✗ Migration failed:'), error.message);
process.exit(1);
});
}
module.exports = { migrate };