Compare commits
9 Commits
3da3338a4e
...
3cc41490bb
| Author | SHA1 | Date |
|---|---|---|
|
|
3cc41490bb | |
|
|
c24821b6ed | |
|
|
2c4c2d9717 | |
|
|
901b39de9a | |
|
|
4d8d1f84f7 | |
|
|
48795d46de | |
|
|
4fd8b9018f | |
|
|
12e0840c62 | |
|
|
10dc25f43d |
|
|
@ -0,0 +1,383 @@
|
||||||
|
# BMad Method PR #2: Agent Task Pre-Flight Protocol
|
||||||
|
|
||||||
|
**Feature Type**: Safety & quality framework
|
||||||
|
**Status**: Draft for community review
|
||||||
|
**Origin**: tellingCube project learnings (masemIT e.U.)
|
||||||
|
**Author**: Mario Semper (@sempre)
|
||||||
|
**Date**: 2025-11-23
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**Agent Task Pre-Flight Protocol** establishes mandatory safety checks for high-risk agent tasks (marketing, legal, deployment) to prevent factual errors, trademark violations, privacy breaches, and assumption-based mistakes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
### Real-World Failure Case
|
||||||
|
|
||||||
|
**Scenario**: Marketing agent (Sophie) created LinkedIn launch posts for tellingCube without:
|
||||||
|
- Reading existing project documentation
|
||||||
|
- Verifying pricing against actual implementation
|
||||||
|
- Checking trademark compliance rules
|
||||||
|
- Reviewing privacy guidelines
|
||||||
|
|
||||||
|
**Result**: Multiple critical errors:
|
||||||
|
- ❌ Mentioned user's day job title (privacy/legal risk)
|
||||||
|
- ❌ Used family member's name (privacy violation)
|
||||||
|
- ❌ Claimed "60 seconds" generation time (factually wrong)
|
||||||
|
- ❌ Advertised "€9/month" pricing (doesn't exist - actual: €29-€999 ONE-TIME)
|
||||||
|
- ❌ Used "IBCS-compliant" (trademark violation - should be "inspired by IBCS©")
|
||||||
|
|
||||||
|
**Root Cause**: Agent operated independently without pre-task verification protocol.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Current BMad Behavior (Risky)
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
User: "Sophie, create LinkedIn launch posts"
|
||||||
|
|
||||||
|
Sophie:
|
||||||
|
1. Generates content based on general knowledge
|
||||||
|
2. Makes assumptions about features/pricing
|
||||||
|
3. Uses marketing best practices
|
||||||
|
4. Presents to user
|
||||||
|
|
||||||
|
❌ Problem: No verification step before creation
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Solution: Pre-Flight Protocol
|
||||||
|
|
||||||
|
### Mandatory Checks Before High-Risk Tasks
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
Agent Task Pre-Flight Protocol:
|
||||||
|
|
||||||
|
BEFORE executing tasks with external impact:
|
||||||
|
1. Discover Critical Context
|
||||||
|
- Search for CRITICAL-GUIDELINES.md or similar
|
||||||
|
- Read recent related work in project
|
||||||
|
- Check actual implementation (code, configs, not assumptions)
|
||||||
|
|
||||||
|
2. Verify Assumptions
|
||||||
|
- Pricing: Read Stripe config / pricing components
|
||||||
|
- Features: Grep codebase for actual capabilities
|
||||||
|
- Legal/Trademark: Check documented compliance rules
|
||||||
|
- Privacy: Verify no personal info in public content
|
||||||
|
|
||||||
|
3. Cross-Agent Review (for high-risk outputs)
|
||||||
|
- Orchestrator reviews before user sees
|
||||||
|
- Fact-checker agent validates claims
|
||||||
|
- Minimum 2 agents verify before publishing
|
||||||
|
|
||||||
|
4. User Approval Gate
|
||||||
|
- Present content as DRAFT
|
||||||
|
- Highlight assumptions made
|
||||||
|
- Get explicit approval before finalizing
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## High-Risk Task Categories
|
||||||
|
|
||||||
|
### 1. Marketing & Public Content
|
||||||
|
**Examples**: LinkedIn posts, press releases, demo videos, website copy
|
||||||
|
|
||||||
|
**Pre-Flight Required**:
|
||||||
|
- [ ] Read `CRITICAL-GUIDELINES.md` (legal, trademark, privacy rules)
|
||||||
|
- [ ] Verify pricing from actual Stripe/payment config
|
||||||
|
- [ ] Verify features from actual codebase (not roadmap ideas)
|
||||||
|
- [ ] Check trademark compliance (e.g., "IBCS©" usage rules)
|
||||||
|
- [ ] Privacy review (no personal identifiers without consent)
|
||||||
|
- [ ] Cross-agent fact-check before presenting to user
|
||||||
|
|
||||||
|
### 2. Legal & Compliance
|
||||||
|
**Examples**: Terms of service, privacy policy, license agreements
|
||||||
|
|
||||||
|
**Pre-Flight Required**:
|
||||||
|
- [ ] Read existing legal docs (don't start from scratch)
|
||||||
|
- [ ] Check jurisdiction-specific requirements
|
||||||
|
- [ ] Verify against actual product behavior (data handling, cookies, etc.)
|
||||||
|
- [ ] Legal expert review (human or specialized agent)
|
||||||
|
- [ ] User final approval required
|
||||||
|
|
||||||
|
### 3. Deployment & Infrastructure
|
||||||
|
**Examples**: Database migrations, production deployments, DNS changes
|
||||||
|
|
||||||
|
**Pre-Flight Required**:
|
||||||
|
- [ ] Read deployment runbooks/checklists
|
||||||
|
- [ ] Verify current production state
|
||||||
|
- [ ] Check for breaking changes
|
||||||
|
- [ ] Backup strategy confirmed
|
||||||
|
- [ ] Rollback plan documented
|
||||||
|
- [ ] User explicit approval with understanding of risks
|
||||||
|
|
||||||
|
### 4. Financial & Billing
|
||||||
|
**Examples**: Stripe configuration, pricing changes, refund policies
|
||||||
|
|
||||||
|
**Pre-Flight Required**:
|
||||||
|
- [ ] Read current Stripe dashboard state
|
||||||
|
- [ ] Verify tax/legal implications
|
||||||
|
- [ ] Check grandfather clause impacts
|
||||||
|
- [ ] Financial impact assessment
|
||||||
|
- [ ] User approval with revenue projections
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Guidelines
|
||||||
|
|
||||||
|
### For Agent Developers
|
||||||
|
|
||||||
|
**In agent YAML definition**:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
agent:
|
||||||
|
name: Sophie
|
||||||
|
id: marketing
|
||||||
|
high_risk_tasks: true # Triggers pre-flight protocol
|
||||||
|
|
||||||
|
pre_flight:
|
||||||
|
required_reads:
|
||||||
|
- docs/marketing/CRITICAL-GUIDELINES.md
|
||||||
|
- components/landing/PricingSection.tsx
|
||||||
|
- docs/_masemIT/readme.md
|
||||||
|
|
||||||
|
verification_steps:
|
||||||
|
- Grep for actual pricing tiers in codebase
|
||||||
|
- Check trademark compliance rules
|
||||||
|
- Privacy scan (no personal names/details)
|
||||||
|
|
||||||
|
cross_check:
|
||||||
|
agents: [river, mary]
|
||||||
|
approval_required: true
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
create-linkedin-post:
|
||||||
|
pre_flight_mandatory: true
|
||||||
|
approval_gate: user
|
||||||
|
```
|
||||||
|
|
||||||
|
### For Orchestrators (River-like agents)
|
||||||
|
|
||||||
|
**Orchestrator responsibilities**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
def execute_high_risk_task(agent, task, user_request):
|
||||||
|
# Step 1: Pre-flight checks
|
||||||
|
critical_docs = discover_critical_guidelines()
|
||||||
|
agent.read(critical_docs)
|
||||||
|
|
||||||
|
# Step 2: Agent executes with verification
|
||||||
|
draft_output = agent.execute_task(task)
|
||||||
|
|
||||||
|
# Step 3: Cross-agent review
|
||||||
|
fact_check_agent = get_agent("mary")
|
||||||
|
verification = fact_check_agent.verify(draft_output, codebase)
|
||||||
|
|
||||||
|
# Step 4: Present as DRAFT to user
|
||||||
|
if verification.has_issues:
|
||||||
|
present_issues_to_user(verification.issues)
|
||||||
|
|
||||||
|
present_as_draft(draft_output)
|
||||||
|
|
||||||
|
# Step 5: User approval gate
|
||||||
|
approval = get_user_approval()
|
||||||
|
|
||||||
|
if approval:
|
||||||
|
finalize(draft_output)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Example: Correct Marketing Flow
|
||||||
|
|
||||||
|
### Before (Risky)
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Create LinkedIn launch posts"
|
||||||
|
Sophie: [Generates 3 posts with assumptions]
|
||||||
|
Sophie: "Here are your posts!"
|
||||||
|
|
||||||
|
❌ Contains errors user must catch
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (Safe)
|
||||||
|
|
||||||
|
```
|
||||||
|
User: "Create LinkedIn launch posts"
|
||||||
|
|
||||||
|
River: "Sophie, this is a high-risk task. Running pre-flight..."
|
||||||
|
|
||||||
|
Sophie:
|
||||||
|
✅ Read CRITICAL-GUIDELINES.md
|
||||||
|
✅ Read PricingSection.tsx (actual pricing: €29-€999)
|
||||||
|
✅ Checked IBCS© compliance rules (must say "inspired by")
|
||||||
|
✅ Privacy check (no "Product Owner", no "brother")
|
||||||
|
|
||||||
|
Sophie: [Generates 3 posts with verified facts]
|
||||||
|
|
||||||
|
River: "Mary, fact-check Sophie's output..."
|
||||||
|
|
||||||
|
Mary:
|
||||||
|
✅ Pricing correct (€29-€999 lifetime)
|
||||||
|
✅ No trademark violations ("inspired by IBCS©")
|
||||||
|
✅ No privacy issues
|
||||||
|
✅ Generation time accurate ("minutes")
|
||||||
|
|
||||||
|
River: "Sempre, here's the DRAFT (pre-flight verified). Approve?"
|
||||||
|
|
||||||
|
User: [Reviews, approves]
|
||||||
|
|
||||||
|
✅ No errors, factually accurate
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Critical Guidelines Template
|
||||||
|
|
||||||
|
**Every project should have**: `docs/PROJECT-NAME/CRITICAL-GUIDELINES.md`
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# CRITICAL Guidelines for [Project Name]
|
||||||
|
|
||||||
|
## ❌ NEVER MENTION
|
||||||
|
- Confidential info (list specific items)
|
||||||
|
- Personal details (family, private life)
|
||||||
|
- Competitor names (if under NDA)
|
||||||
|
|
||||||
|
## ✅ ALWAYS VERIFY
|
||||||
|
- Pricing: Check [file path]
|
||||||
|
- Features: Grep [codebase location]
|
||||||
|
- Legal: Comply with [trademark/license rules]
|
||||||
|
|
||||||
|
## Trademark Compliance
|
||||||
|
- "IBCS©" → Always say "inspired by IBCS©" (not "compliant")
|
||||||
|
- [Other trademarks...]
|
||||||
|
|
||||||
|
## Privacy Rules
|
||||||
|
- No personal job titles in public content
|
||||||
|
- No family member names
|
||||||
|
- [Other privacy rules...]
|
||||||
|
|
||||||
|
## Approval Requirements
|
||||||
|
- Marketing content: River + Mary review
|
||||||
|
- Legal docs: Legal expert review
|
||||||
|
- Deployment: User explicit approval
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
✅ **Prevents costly mistakes** - Catches errors before they're public
|
||||||
|
✅ **Protects legal compliance** - Trademark, privacy, licensing
|
||||||
|
✅ **Ensures factual accuracy** - Features/pricing match reality
|
||||||
|
✅ **Builds user trust** - Agents don't hallucinate facts
|
||||||
|
✅ **Scalable safety** - Works across all BMad projects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tradeoffs & Considerations
|
||||||
|
|
||||||
|
### Slower Task Execution
|
||||||
|
- **Before**: Agent outputs in 30 seconds
|
||||||
|
- **After**: Pre-flight adds 1-2 minutes
|
||||||
|
- **Worth it?**: YES for high-risk tasks (marketing, legal, deployment)
|
||||||
|
|
||||||
|
### More Agent Coordination
|
||||||
|
- Requires orchestrator (River) to manage pre-flight
|
||||||
|
- Cross-agent reviews add complexity
|
||||||
|
- **Mitigation**: Only for high-risk tasks, not every task
|
||||||
|
|
||||||
|
### User Approval Friction
|
||||||
|
- Adds approval gate before finalization
|
||||||
|
- **Mitigation**: Present as DRAFT with verification status
|
||||||
|
- User can fast-track if comfortable
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rollout Strategy
|
||||||
|
|
||||||
|
### Phase 1: Opt-In (Recommended)
|
||||||
|
- Projects mark agents as `high_risk_tasks: true`
|
||||||
|
- Orchestrators enforce pre-flight for marked agents
|
||||||
|
- Community feedback on friction/benefits
|
||||||
|
|
||||||
|
### Phase 2: Default for Risky Categories
|
||||||
|
- Marketing, legal, deployment agents default to pre-flight
|
||||||
|
- Other agents opt-in if needed
|
||||||
|
|
||||||
|
### Phase 3: Configurable Per-Task
|
||||||
|
- Users set risk level per task
|
||||||
|
- `*create-post --risk high` triggers pre-flight
|
||||||
|
- `*create-post --risk low` skips for drafts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Real-World Validation
|
||||||
|
|
||||||
|
**Origin Project**: tellingCube (masemIT e.U.)
|
||||||
|
|
||||||
|
**Failure Scenario**:
|
||||||
|
- Marketing agent created launch posts without verification
|
||||||
|
- 5 critical errors caught by user (should have been caught earlier)
|
||||||
|
- 30 minutes of rework to fix
|
||||||
|
|
||||||
|
**After Implementing Protocol**:
|
||||||
|
- CRITICAL-GUIDELINES.md created
|
||||||
|
- Pre-flight checklist enforced
|
||||||
|
- Cross-agent review (River → Sophie → Mary → User)
|
||||||
|
- **Result**: Zero errors in final content
|
||||||
|
|
||||||
|
**User Feedback (Mario Semper)**:
|
||||||
|
> "I love BMad, but I don't want to repeat the ChatGPT hallucination nightmare. This protocol gives me confidence that agents verify facts before presenting them."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions for Community
|
||||||
|
|
||||||
|
1. **Scope**: Which task types should default to pre-flight?
|
||||||
|
2. **Performance**: Is 1-2 minute overhead acceptable for high-risk tasks?
|
||||||
|
3. **Configurability**: Per-project, per-agent, or per-task risk settings?
|
||||||
|
4. **Tooling**: Should pre-flight be a separate tool or built into agent execution?
|
||||||
|
5. **Enforcement**: Optional best practice or mandatory for certain agents?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Community feedback** on protocol design
|
||||||
|
2. **Reference implementation** in BMad core
|
||||||
|
3. **Agent template updates** to include pre-flight hooks
|
||||||
|
4. **Documentation** with examples for common scenarios
|
||||||
|
5. **Testing** across different project types
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comparison to Similar Patterns
|
||||||
|
|
||||||
|
| Pattern | Focus | When to Use |
|
||||||
|
|---------|-------|-------------|
|
||||||
|
| **Pre-Flight Protocol** | Safety & accuracy | High-risk external outputs |
|
||||||
|
| **Code Review** | Code quality | Before merging code |
|
||||||
|
| **QA Gates** | Testing | Before production deployment |
|
||||||
|
| **Approval Workflows** | Governance | Multi-stakeholder decisions |
|
||||||
|
|
||||||
|
**Pre-Flight Protocol** = "Code review + QA gate" for **agent outputs**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- **Source Project**: tellingCube (https://github.com/masemIT/telling-cube) [if public]
|
||||||
|
- **Failure Case**: `docs/bmad-contributions/` (this document)
|
||||||
|
- **Implementation**: `docs/marketing/CRITICAL-GUIDELINES.md` (tellingCube)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Contribution ready for review.** This came from painful real-world experience - let's make BMad safer for everyone! 🛡️
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
# BMad Method PR #1: Ring of Fire (ROF) Sessions
|
||||||
|
|
||||||
|
**Feature Type**: Core workflow enhancement
|
||||||
|
**Status**: Draft for community review
|
||||||
|
**Origin**: tellingCube project (masemIT e.U.)
|
||||||
|
**Author**: Mario Semper (@sempre)
|
||||||
|
**Date**: 2025-11-23
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
**Ring of Fire (ROF) Sessions** enable multi-agent collaborative sessions that run in parallel to the user's main workflow, allowing users to delegate complex multi-perspective analysis while continuing other work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Problem Statement
|
||||||
|
|
||||||
|
Current BMad Method requires **sequential agent interaction**. When users need multiple agents to collaborate on a complex topic, they must:
|
||||||
|
- Manually orchestrate each agent conversation
|
||||||
|
- Stay in the loop for every exchange
|
||||||
|
- Wait for sequential responses before proceeding
|
||||||
|
- Context-switch constantly between tasks
|
||||||
|
|
||||||
|
This creates **bottlenecks** and prevents **parallel work streams**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Solution: Ring of Fire Sessions
|
||||||
|
|
||||||
|
A new command pattern that enables **scoped multi-agent collaboration sessions** that run while the user continues other work.
|
||||||
|
|
||||||
|
### Command Syntax
|
||||||
|
|
||||||
|
```bash
|
||||||
|
*rof "<topic>" --agents <agent-list> [--report brief|detailed|live]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
*rof "API Refactoring Strategy" --agents dev,architect,qa --report brief
|
||||||
|
```
|
||||||
|
|
||||||
|
**What happens**:
|
||||||
|
1. Dev, Architect, and QA agents enter a collaborative session
|
||||||
|
2. They analyze the topic together (code review, design discussion, testing concerns)
|
||||||
|
3. When agents need tool access (read files, run commands), they request user approval
|
||||||
|
4. User continues working on other tasks in parallel
|
||||||
|
5. Session ends with consolidated report (brief: just recommendations, detailed: full transcript)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
|
||||||
|
### 1. User-Controlled Scope
|
||||||
|
- **Small**: 2 agents, 5-minute quick discussion
|
||||||
|
- **Large**: 10 agents, 2-hour deep analysis
|
||||||
|
- User decides granularity based on complexity
|
||||||
|
|
||||||
|
### 2. Approval-Gated Tool Access
|
||||||
|
- Agents can **discuss** freely within the session
|
||||||
|
- When agents need **tools** (read files, execute commands, make changes), they:
|
||||||
|
- Pause the session
|
||||||
|
- Request user approval
|
||||||
|
- Resume after user decision
|
||||||
|
|
||||||
|
**Why**: Maintains user control, prevents runaway agent actions
|
||||||
|
|
||||||
|
### 3. Flexible Reporting
|
||||||
|
|
||||||
|
| Mode | Description | Use Case |
|
||||||
|
|------|-------------|----------|
|
||||||
|
| `brief` | Final recommendations only | "Just tell me what to do" |
|
||||||
|
| `detailed` | Full transcript + recommendations | "Show me the reasoning" |
|
||||||
|
| `live` | Real-time updates as agents discuss | "I want to observe" |
|
||||||
|
|
||||||
|
**Default**: `brief` with Q&A available
|
||||||
|
|
||||||
|
### 4. Parallel Workflows
|
||||||
|
- User works on **Task A** while ROF session tackles **Task B**
|
||||||
|
- No context-switching overhead
|
||||||
|
- Efficient use of time
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use Cases
|
||||||
|
|
||||||
|
### 1. Architecture Reviews
|
||||||
|
```bash
|
||||||
|
*rof "Evaluate microservices vs monolith for new feature" --agents architect,dev,qa
|
||||||
|
```
|
||||||
|
**Agents collaborate on**: Design trade-offs, implementation complexity, testing implications
|
||||||
|
|
||||||
|
### 2. Code Refactoring
|
||||||
|
```bash
|
||||||
|
*rof "Refactor authentication module" --agents dev,architect --report detailed
|
||||||
|
```
|
||||||
|
**Agents collaborate on**: Current code analysis, refactoring approach, migration strategy
|
||||||
|
|
||||||
|
### 3. Feature Planning
|
||||||
|
```bash
|
||||||
|
*rof "Plan user notifications feature" --agents pm,ux,dev --report brief
|
||||||
|
```
|
||||||
|
**Agents collaborate on**: Requirements, UX flow, technical feasibility, timeline
|
||||||
|
|
||||||
|
### 4. Quality Gates
|
||||||
|
```bash
|
||||||
|
*rof "Investigate test failures in CI/CD" --agents qa,dev --report live
|
||||||
|
```
|
||||||
|
**Agents collaborate on**: Root cause analysis, fix recommendations, regression prevention
|
||||||
|
|
||||||
|
### 5. Documentation Sprints
|
||||||
|
```bash
|
||||||
|
*rof "Document API endpoints" --agents dev,pm,ux
|
||||||
|
```
|
||||||
|
**Agents collaborate on**: Technical accuracy, user-friendly examples, completeness
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## User Experience Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
User->>River: *rof "Topic" --agents dev,architect
|
||||||
|
River->>Dev: Join ROF session
|
||||||
|
River->>Architect: Join ROF session
|
||||||
|
River->>User: Session started, continue your work
|
||||||
|
|
||||||
|
Dev->>Architect: Discuss approach
|
||||||
|
Architect->>Dev: Suggest alternatives
|
||||||
|
|
||||||
|
Dev->>User: Need to read auth.ts - approve?
|
||||||
|
User->>Dev: Approved
|
||||||
|
Dev->>Architect: After reading file...
|
||||||
|
|
||||||
|
Architect->>Dev: Recommendation
|
||||||
|
Dev->>River: Session complete
|
||||||
|
River->>User: Brief report: [Recommendations]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Considerations
|
||||||
|
|
||||||
|
### Technical Requirements
|
||||||
|
- **Session state management**: Track active ROF sessions, participating agents
|
||||||
|
- **Agent context sharing**: Agents share knowledge within session scope
|
||||||
|
- **User approval workflow**: Clear prompt for tool requests
|
||||||
|
- **Report generation**: Brief/detailed/live output formatting
|
||||||
|
- **Workflow integration**: Link ROF findings to existing workflow plans/todos
|
||||||
|
|
||||||
|
### Open Questions for Community
|
||||||
|
|
||||||
|
1. **Integration**: Core BMad feature or plugin/extension?
|
||||||
|
2. **Concurrency**: How to handle file conflicts if multiple agents want to edit?
|
||||||
|
3. **Cost Model**: Guidance for LLM call budgeting with multiple agents?
|
||||||
|
4. **Session Limits**: Recommended max agents/duration?
|
||||||
|
5. **Agent Communication**: Free-form discussion or structured turn-taking?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Real-World Validation
|
||||||
|
|
||||||
|
**Origin Project**: tellingCube (BI dashboard, masemIT e.U.)
|
||||||
|
|
||||||
|
**Validation Scenario**:
|
||||||
|
- **Topic**: "Next steps for tellingCube after validation test"
|
||||||
|
- **Agents**: River (orchestrator), Mary (analyst), Winston (architect)
|
||||||
|
- **Report Mode**: Brief
|
||||||
|
- **Outcome**: Successfully analyzed post-validation roadmap with 3 scenarios (GO/CHANGE/NO-GO), delivered consolidated recommendations in 5 minutes
|
||||||
|
|
||||||
|
**User Feedback (Mario Semper)**:
|
||||||
|
> "This is exactly what I needed - I wanted multiple perspectives without having to orchestrate every conversation. The brief report gave me actionable next steps immediately."
|
||||||
|
|
||||||
|
**Documentation**: `docs/_masemIT/readme.md` in tellingCube repository
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Proposed Documentation Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
.bmad-core/
|
||||||
|
features/
|
||||||
|
ring-of-fire.md # Feature specification
|
||||||
|
|
||||||
|
docs/
|
||||||
|
guides/
|
||||||
|
using-rof-sessions.md # User guide with examples
|
||||||
|
|
||||||
|
architecture/
|
||||||
|
agent-collaboration.md # Technical design
|
||||||
|
rof-session-management.md # State handling approach
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Benefits
|
||||||
|
|
||||||
|
✅ **Unlocks parallel workflows** - User productivity gains
|
||||||
|
✅ **Reduces context-switching** - Cognitive load reduction
|
||||||
|
✅ **Enables complex analysis** - Multi-perspective insights
|
||||||
|
✅ **Maintains user control** - Approval gates for tools
|
||||||
|
✅ **Scales flexibly** - From quick checks to deep dives
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comparison to Existing Patterns
|
||||||
|
|
||||||
|
| Feature | Standard Agent Use | ROF Session |
|
||||||
|
|---------|-------------------|-------------|
|
||||||
|
| Agent collaboration | Sequential (one at a time) | Parallel (multiple simultaneously) |
|
||||||
|
| User involvement | Required for every exchange | Only for approvals |
|
||||||
|
| Parallel work | No (user waits) | Yes (user continues tasks) |
|
||||||
|
| Output | Chat transcript | Consolidated report |
|
||||||
|
| Use case | Single-perspective tasks | Multi-perspective analysis |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Community feedback** on approach and open questions
|
||||||
|
2. **Technical design** refinement (state management, agent communication)
|
||||||
|
3. **Prototype implementation** in BMad core or as extension
|
||||||
|
4. **Beta testing** with real projects (beyond tellingCube)
|
||||||
|
5. **Documentation** completion with examples
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alternatives Considered
|
||||||
|
|
||||||
|
### Alt 1: "Breakout Session"
|
||||||
|
- **Pros**: Clear meeting metaphor
|
||||||
|
- **Cons**: Less evocative, doesn't convey "continuous collaborative space"
|
||||||
|
|
||||||
|
### Alt 2: "Agent Huddle"
|
||||||
|
- **Pros**: Short, casual
|
||||||
|
- **Cons**: Implies quick/informal only
|
||||||
|
|
||||||
|
### Alt 3: "Lagerfeuer" (original German name)
|
||||||
|
- **Pros**: Warm, campfire metaphor
|
||||||
|
- **Cons**: Poor i18n, hard to pronounce/remember for non-German speakers
|
||||||
|
|
||||||
|
**Chosen**: **Ring of Fire** - evokes continuous collaboration circle, internationally understood, memorable, shortcut "ROF" works well
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- **Source Project**: tellingCube (https://github.com/masemIT/telling-cube) [if public]
|
||||||
|
- **Documentation**: `docs/_masemIT/readme.md`
|
||||||
|
- **Discussion**: [Link to BMad community discussion if applicable]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Contribution ready for review.** Feedback welcome! 🔥
|
||||||
|
|
@ -21,15 +21,6 @@ module.exports = {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle agent compilation separately
|
|
||||||
if (config.actionType === 'compile') {
|
|
||||||
const result = await installer.compileAgents(config);
|
|
||||||
console.log(chalk.green('\n✨ Agent compilation complete!'));
|
|
||||||
console.log(chalk.cyan(`Rebuilt ${result.agentCount} agents and ${result.taskCount} tasks`));
|
|
||||||
process.exit(0);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle quick update separately
|
// Handle quick update separately
|
||||||
if (config.actionType === 'quick-update') {
|
if (config.actionType === 'quick-update') {
|
||||||
const result = await installer.quickUpdate(config);
|
const result = await installer.quickUpdate(config);
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@ module.exports = {
|
||||||
options: [],
|
options: [],
|
||||||
action: async () => {
|
action: async () => {
|
||||||
try {
|
try {
|
||||||
const modules = await installer.getAvailableModules();
|
const result = await installer.getAvailableModules();
|
||||||
|
const { modules, customModules } = result;
|
||||||
|
|
||||||
console.log(chalk.cyan('\n📦 Available BMAD Modules:\n'));
|
console.log(chalk.cyan('\n📦 Available BMAD Modules:\n'));
|
||||||
|
|
||||||
for (const module of modules) {
|
for (const module of modules) {
|
||||||
|
|
@ -19,6 +21,16 @@ module.exports = {
|
||||||
console.log();
|
console.log();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (customModules && customModules.length > 0) {
|
||||||
|
console.log(chalk.cyan('\n🔧 Custom Modules:\n'));
|
||||||
|
for (const module of customModules) {
|
||||||
|
console.log(chalk.bold(` ${module.id}`));
|
||||||
|
console.log(chalk.dim(` ${module.description}`));
|
||||||
|
console.log(chalk.dim(` Version: ${module.version}`));
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(chalk.red('Error:'), error.message);
|
console.error(chalk.red('Error:'), error.message);
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ class Installer {
|
||||||
this.dependencyResolver = new DependencyResolver();
|
this.dependencyResolver = new DependencyResolver();
|
||||||
this.configCollector = new ConfigCollector();
|
this.configCollector = new ConfigCollector();
|
||||||
this.ideConfigManager = new IdeConfigManager();
|
this.ideConfigManager = new IdeConfigManager();
|
||||||
this.installedFiles = []; // Track all installed files
|
this.installedFiles = new Set(); // Track all installed files
|
||||||
this.ttsInjectedFiles = []; // Track files with TTS injection applied
|
this.ttsInjectedFiles = []; // Track files with TTS injection applied
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -431,7 +431,41 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
if (config._quickUpdate) {
|
if (config._quickUpdate) {
|
||||||
// Quick update already collected all configs, use them directly
|
// Quick update already collected all configs, use them directly
|
||||||
moduleConfigs = this.configCollector.collectedConfig;
|
moduleConfigs = this.configCollector.collectedConfig;
|
||||||
|
|
||||||
|
// For quick update, populate customModulePaths from _customModuleSources
|
||||||
|
if (config._customModuleSources) {
|
||||||
|
for (const [moduleId, customInfo] of config._customModuleSources) {
|
||||||
|
customModulePaths.set(moduleId, customInfo.sourcePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// For regular updates (modify flow), check manifest for custom module sources
|
||||||
|
if (config._isUpdate && config._existingInstall && config._existingInstall.customModules) {
|
||||||
|
for (const customModule of config._existingInstall.customModules) {
|
||||||
|
// Ensure we have an absolute sourcePath
|
||||||
|
let absoluteSourcePath = customModule.sourcePath;
|
||||||
|
|
||||||
|
// Check if sourcePath is a cache-relative path (starts with _config)
|
||||||
|
if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) {
|
||||||
|
// Convert cache-relative path to absolute path
|
||||||
|
absoluteSourcePath = path.join(bmadDir, absoluteSourcePath);
|
||||||
|
}
|
||||||
|
// If no sourcePath but we have relativePath, convert it
|
||||||
|
else if (!absoluteSourcePath && customModule.relativePath) {
|
||||||
|
// relativePath is relative to the project root (parent of bmad dir)
|
||||||
|
absoluteSourcePath = path.resolve(projectDir, customModule.relativePath);
|
||||||
|
}
|
||||||
|
// Ensure sourcePath is absolute for anything else
|
||||||
|
else if (absoluteSourcePath && !path.isAbsolute(absoluteSourcePath)) {
|
||||||
|
absoluteSourcePath = path.resolve(absoluteSourcePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (absoluteSourcePath) {
|
||||||
|
customModulePaths.set(customModule.id, absoluteSourcePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Build custom module paths map from customContent
|
// Build custom module paths map from customContent
|
||||||
|
|
||||||
// Handle selectedFiles (from existing install path or manual directory input)
|
// Handle selectedFiles (from existing install path or manual directory input)
|
||||||
|
|
@ -582,20 +616,39 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
|
|
||||||
// Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
|
// Detect custom and modified files BEFORE updating (compare current files vs files-manifest.csv)
|
||||||
const existingFilesManifest = await this.readFilesManifest(bmadDir);
|
const existingFilesManifest = await this.readFilesManifest(bmadDir);
|
||||||
console.log(chalk.dim(`DEBUG: Read ${existingFilesManifest.length} files from manifest`));
|
|
||||||
console.log(chalk.dim(`DEBUG: Manifest has hashes: ${existingFilesManifest.some((f) => f.hash)}`));
|
|
||||||
|
|
||||||
const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
|
const { customFiles, modifiedFiles } = await this.detectCustomFiles(bmadDir, existingFilesManifest);
|
||||||
|
|
||||||
console.log(chalk.dim(`DEBUG: Found ${customFiles.length} custom files, ${modifiedFiles.length} modified files`));
|
|
||||||
if (modifiedFiles.length > 0) {
|
|
||||||
console.log(chalk.yellow('DEBUG: Modified files:'));
|
|
||||||
for (const f of modifiedFiles) console.log(chalk.dim(` - ${f.path}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
config._customFiles = customFiles;
|
config._customFiles = customFiles;
|
||||||
config._modifiedFiles = modifiedFiles;
|
config._modifiedFiles = modifiedFiles;
|
||||||
|
|
||||||
|
// Also check cache directory for custom modules (like quick update does)
|
||||||
|
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||||
|
if (await fs.pathExists(cacheDir)) {
|
||||||
|
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const cachedModule of cachedModules) {
|
||||||
|
if (cachedModule.isDirectory()) {
|
||||||
|
const moduleId = cachedModule.name;
|
||||||
|
|
||||||
|
// Skip if we already have this module from manifest
|
||||||
|
if (customModulePaths.has(moduleId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedPath = path.join(cacheDir, moduleId);
|
||||||
|
|
||||||
|
// Check if this is actually a custom module (has module.yaml)
|
||||||
|
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
||||||
|
if (await fs.pathExists(moduleYamlPath)) {
|
||||||
|
customModulePaths.set(moduleId, cachedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update module manager with the new custom module paths from cache
|
||||||
|
this.moduleManager.setCustomModulePaths(customModulePaths);
|
||||||
|
}
|
||||||
|
|
||||||
// If there are custom files, back them up temporarily
|
// If there are custom files, back them up temporarily
|
||||||
if (customFiles.length > 0) {
|
if (customFiles.length > 0) {
|
||||||
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
|
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
|
||||||
|
|
@ -618,20 +671,16 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
|
const tempModifiedBackupDir = path.join(projectDir, '_bmad-modified-backup-temp');
|
||||||
await fs.ensureDir(tempModifiedBackupDir);
|
await fs.ensureDir(tempModifiedBackupDir);
|
||||||
|
|
||||||
console.log(chalk.yellow(`\nDEBUG: Backing up ${modifiedFiles.length} modified files to temp location`));
|
|
||||||
spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
|
spinner.start(`Backing up ${modifiedFiles.length} modified files...`);
|
||||||
for (const modifiedFile of modifiedFiles) {
|
for (const modifiedFile of modifiedFiles) {
|
||||||
const relativePath = path.relative(bmadDir, modifiedFile.path);
|
const relativePath = path.relative(bmadDir, modifiedFile.path);
|
||||||
const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
|
const tempBackupPath = path.join(tempModifiedBackupDir, relativePath);
|
||||||
console.log(chalk.dim(`DEBUG: Backing up ${relativePath} to temp`));
|
|
||||||
await fs.ensureDir(path.dirname(tempBackupPath));
|
await fs.ensureDir(path.dirname(tempBackupPath));
|
||||||
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
|
await fs.copy(modifiedFile.path, tempBackupPath, { overwrite: true });
|
||||||
}
|
}
|
||||||
spinner.succeed(`Backed up ${modifiedFiles.length} modified files`);
|
spinner.succeed(`Backed up ${modifiedFiles.length} modified files`);
|
||||||
|
|
||||||
config._tempModifiedBackupDir = tempModifiedBackupDir;
|
config._tempModifiedBackupDir = tempModifiedBackupDir;
|
||||||
} else {
|
|
||||||
console.log(chalk.dim('DEBUG: No modified files detected'));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (existingInstall.installed && config._quickUpdate) {
|
} else if (existingInstall.installed && config._quickUpdate) {
|
||||||
|
|
@ -647,6 +696,34 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
config._customFiles = customFiles;
|
config._customFiles = customFiles;
|
||||||
config._modifiedFiles = modifiedFiles;
|
config._modifiedFiles = modifiedFiles;
|
||||||
|
|
||||||
|
// Also check cache directory for custom modules (like quick update does)
|
||||||
|
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||||
|
if (await fs.pathExists(cacheDir)) {
|
||||||
|
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const cachedModule of cachedModules) {
|
||||||
|
if (cachedModule.isDirectory()) {
|
||||||
|
const moduleId = cachedModule.name;
|
||||||
|
|
||||||
|
// Skip if we already have this module from manifest
|
||||||
|
if (customModulePaths.has(moduleId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedPath = path.join(cacheDir, moduleId);
|
||||||
|
|
||||||
|
// Check if this is actually a custom module (has module.yaml)
|
||||||
|
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
||||||
|
if (await fs.pathExists(moduleYamlPath)) {
|
||||||
|
customModulePaths.set(moduleId, cachedPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update module manager with the new custom module paths from cache
|
||||||
|
this.moduleManager.setCustomModulePaths(customModulePaths);
|
||||||
|
}
|
||||||
|
|
||||||
// Back up custom files
|
// Back up custom files
|
||||||
if (customFiles.length > 0) {
|
if (customFiles.length > 0) {
|
||||||
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
|
const tempBackupDir = path.join(projectDir, '_bmad-custom-backup-temp');
|
||||||
|
|
@ -825,7 +902,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
// For dependency resolution, we need to pass the project root
|
// For dependency resolution, we need to pass the project root
|
||||||
// Create a temporary module manager that knows about custom content locations
|
// Create a temporary module manager that knows about custom content locations
|
||||||
const tempModuleManager = new ModuleManager({
|
const tempModuleManager = new ModuleManager({
|
||||||
scanProjectForModules: true,
|
|
||||||
bmadDir: bmadDir, // Pass bmadDir so we can check cache
|
bmadDir: bmadDir, // Pass bmadDir so we can check cache
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -847,7 +923,9 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
}
|
}
|
||||||
installedModuleNames.add(moduleName);
|
installedModuleNames.add(moduleName);
|
||||||
|
|
||||||
spinner.start(`Installing module: ${moduleName}...`);
|
// Show appropriate message based on whether this is a quick update
|
||||||
|
const isQuickUpdate = config._quickUpdate || false;
|
||||||
|
spinner.start(`${isQuickUpdate ? 'Updating' : 'Installing'} module: ${moduleName}...`);
|
||||||
|
|
||||||
// Check if this is a custom module
|
// Check if this is a custom module
|
||||||
let isCustomModule = false;
|
let isCustomModule = false;
|
||||||
|
|
@ -900,103 +978,36 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCustomModule && customInfo) {
|
if (isCustomModule && customInfo) {
|
||||||
// Install custom module using CustomHandler but as a proper module
|
// Custom modules are now installed via ModuleManager just like standard modules
|
||||||
const customHandler = new CustomHandler();
|
// The custom module path should already be in customModulePaths from earlier setup
|
||||||
|
if (!customModulePaths.has(moduleName) && customInfo.path) {
|
||||||
// Install to module directory instead of custom directory
|
customModulePaths.set(moduleName, customInfo.path);
|
||||||
const moduleTargetPath = path.join(bmadDir, moduleName);
|
this.moduleManager.setCustomModulePaths(customModulePaths);
|
||||||
await fs.ensureDir(moduleTargetPath);
|
}
|
||||||
|
|
||||||
// Get collected config for this custom module (from module.yaml prompts)
|
// Get collected config for this custom module (from module.yaml prompts)
|
||||||
const collectedModuleConfig = moduleConfigs[moduleName] || {};
|
const collectedModuleConfig = moduleConfigs[moduleName] || {};
|
||||||
|
|
||||||
const result = await customHandler.install(
|
// Use ModuleManager to install the custom module
|
||||||
customInfo.path,
|
await this.moduleManager.install(
|
||||||
path.join(bmadDir, 'temp-custom'),
|
moduleName,
|
||||||
{ ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig, _bmadDir: bmadDir },
|
bmadDir,
|
||||||
(filePath) => {
|
(filePath) => {
|
||||||
// Track installed files with correct path
|
this.installedFiles.add(filePath);
|
||||||
const relativePath = path.relative(path.join(bmadDir, 'temp-custom'), filePath);
|
},
|
||||||
const finalPath = path.join(moduleTargetPath, relativePath);
|
{
|
||||||
this.installedFiles.push(finalPath);
|
isCustom: true,
|
||||||
|
moduleConfig: collectedModuleConfig,
|
||||||
|
isQuickUpdate: config._quickUpdate || false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Move from temp-custom to actual module directory
|
// ModuleManager installs directly to the target directory, no need to move files
|
||||||
const tempCustomPath = path.join(bmadDir, 'temp-custom');
|
|
||||||
if (await fs.pathExists(tempCustomPath)) {
|
|
||||||
const customDir = path.join(tempCustomPath, 'custom');
|
|
||||||
if (await fs.pathExists(customDir)) {
|
|
||||||
// Move contents to module directory
|
|
||||||
const items = await fs.readdir(customDir);
|
|
||||||
const movedItems = [];
|
|
||||||
try {
|
|
||||||
for (const item of items) {
|
|
||||||
const srcPath = path.join(customDir, item);
|
|
||||||
const destPath = path.join(moduleTargetPath, item);
|
|
||||||
|
|
||||||
// If destination exists, remove it first (or we could merge)
|
|
||||||
if (await fs.pathExists(destPath)) {
|
|
||||||
await fs.remove(destPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.move(srcPath, destPath);
|
|
||||||
movedItems.push({ src: srcPath, dest: destPath });
|
|
||||||
}
|
|
||||||
} catch (moveError) {
|
|
||||||
// Rollback: restore any successfully moved items
|
|
||||||
for (const moved of movedItems) {
|
|
||||||
try {
|
|
||||||
await fs.move(moved.dest, moved.src);
|
|
||||||
} catch {
|
|
||||||
// Best-effort rollback - log if it fails
|
|
||||||
console.error(`Failed to rollback ${moved.dest} during cleanup`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to move custom module files: ${moveError.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await fs.remove(tempCustomPath);
|
|
||||||
} catch (cleanupError) {
|
|
||||||
// Non-fatal: temp directory cleanup failed but files were moved successfully
|
|
||||||
console.warn(`Warning: Could not clean up temp directory: ${cleanupError.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create module config (include collected config from module.yaml prompts)
|
// Create module config (include collected config from module.yaml prompts)
|
||||||
await this.generateModuleConfigs(bmadDir, {
|
await this.generateModuleConfigs(bmadDir, {
|
||||||
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
[moduleName]: { ...config.coreConfig, ...customInfo.config, ...collectedModuleConfig },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Store custom module info for later manifest update
|
|
||||||
if (!config._customModulesToTrack) {
|
|
||||||
config._customModulesToTrack = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// For cached modules, use appropriate path handling
|
|
||||||
let sourcePath;
|
|
||||||
if (useCache) {
|
|
||||||
// Check if we have cached modules info (from initial install)
|
|
||||||
if (finalCustomContent && finalCustomContent.cachedModules) {
|
|
||||||
sourcePath = finalCustomContent.cachedModules.find((m) => m.id === moduleName)?.relativePath;
|
|
||||||
} else {
|
|
||||||
// During update, the sourcePath is already cache-relative if it starts with _config
|
|
||||||
sourcePath =
|
|
||||||
customInfo.sourcePath && customInfo.sourcePath.startsWith('_config')
|
|
||||||
? customInfo.sourcePath
|
|
||||||
: path.relative(bmadDir, customInfo.path || customInfo.sourcePath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
sourcePath = path.resolve(customInfo.path || customInfo.sourcePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
config._customModulesToTrack.push({
|
|
||||||
id: customInfo.id,
|
|
||||||
name: customInfo.name,
|
|
||||||
sourcePath: useCache ? `_config/custom/${customInfo.id}` : sourcePath,
|
|
||||||
installDate: new Date().toISOString(),
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// Regular module installation
|
// Regular module installation
|
||||||
// Special case for core module
|
// Special case for core module
|
||||||
|
|
@ -1007,7 +1018,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
spinner.succeed(`Module installed: ${moduleName}`);
|
spinner.succeed(`Module ${isQuickUpdate ? 'updated' : 'installed'}: ${moduleName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install partial modules (only dependencies)
|
// Install partial modules (only dependencies)
|
||||||
|
|
@ -1029,69 +1040,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Install custom content if provided AND selected
|
// All content is now installed as modules - no separate custom content handling needed
|
||||||
// Process custom content that wasn't installed as modules
|
|
||||||
// This is now handled in the module installation loop above
|
|
||||||
// This section is kept for backward compatibility with any custom content
|
|
||||||
// that doesn't have a module structure
|
|
||||||
const remainingCustomContent = [];
|
|
||||||
if (
|
|
||||||
config.customContent &&
|
|
||||||
config.customContent.hasCustomContent &&
|
|
||||||
config.customContent.customPath &&
|
|
||||||
config.customContent.selected &&
|
|
||||||
config.customContent.selectedFiles
|
|
||||||
) {
|
|
||||||
// Filter out custom modules that were already installed
|
|
||||||
const customHandler = new CustomHandler();
|
|
||||||
for (const customFile of config.customContent.selectedFiles) {
|
|
||||||
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
|
|
||||||
|
|
||||||
// Skip if this was installed as a module
|
|
||||||
if (!customInfo || !customInfo.id || !allModules.includes(customInfo.id)) {
|
|
||||||
remainingCustomContent.push(customFile);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remainingCustomContent.length > 0) {
|
|
||||||
spinner.start('Installing remaining custom content...');
|
|
||||||
const customHandler = new CustomHandler();
|
|
||||||
|
|
||||||
// Use the remaining files
|
|
||||||
const customFiles = remainingCustomContent;
|
|
||||||
|
|
||||||
if (customFiles.length > 0) {
|
|
||||||
console.log(chalk.cyan(`\n Found ${customFiles.length} custom content file(s):`));
|
|
||||||
for (const customFile of customFiles) {
|
|
||||||
const customInfo = await customHandler.getCustomInfo(customFile, projectDir);
|
|
||||||
if (customInfo) {
|
|
||||||
console.log(chalk.dim(` • ${customInfo.name} (${customInfo.relativePath})`));
|
|
||||||
|
|
||||||
// Install the custom content
|
|
||||||
const result = await customHandler.install(
|
|
||||||
customInfo.path,
|
|
||||||
bmadDir,
|
|
||||||
{ ...config.coreConfig, ...customInfo.config },
|
|
||||||
(filePath) => {
|
|
||||||
// Track installed files
|
|
||||||
this.installedFiles.push(filePath);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.errors.length > 0) {
|
|
||||||
console.log(chalk.yellow(` ⚠️ ${result.errors.length} error(s) occurred`));
|
|
||||||
for (const error of result.errors) {
|
|
||||||
console.log(chalk.dim(` - ${error}`));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(chalk.green(` ✓ Installed ${result.agentsInstalled} agents, ${result.workflowsInstalled} workflows`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
spinner.succeed('Custom content installed');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate clean config.yaml files for each installed module
|
// Generate clean config.yaml files for each installed module
|
||||||
spinner.start('Generating module configurations...');
|
spinner.start('Generating module configurations...');
|
||||||
|
|
@ -1104,12 +1053,10 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
|
|
||||||
// Pre-register manifest files that will be created (except files-manifest.csv to avoid recursion)
|
// Pre-register manifest files that will be created (except files-manifest.csv to avoid recursion)
|
||||||
const cfgDir = path.join(bmadDir, '_config');
|
const cfgDir = path.join(bmadDir, '_config');
|
||||||
this.installedFiles.push(
|
this.installedFiles.add(path.join(cfgDir, 'manifest.yaml'));
|
||||||
path.join(cfgDir, 'manifest.yaml'),
|
this.installedFiles.add(path.join(cfgDir, 'workflow-manifest.csv'));
|
||||||
path.join(cfgDir, 'workflow-manifest.csv'),
|
this.installedFiles.add(path.join(cfgDir, 'agent-manifest.csv'));
|
||||||
path.join(cfgDir, 'agent-manifest.csv'),
|
this.installedFiles.add(path.join(cfgDir, 'task-manifest.csv'));
|
||||||
path.join(cfgDir, 'task-manifest.csv'),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup
|
// Generate CSV manifests for workflows, agents, tasks AND ALL FILES with hashes BEFORE IDE setup
|
||||||
spinner.start('Generating workflow and agent manifests...');
|
spinner.start('Generating workflow and agent manifests...');
|
||||||
|
|
@ -1133,19 +1080,12 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
|
modulesForCsvPreserve = config._preserveModules ? [...allModules, ...config._preserveModules] : allModules;
|
||||||
}
|
}
|
||||||
|
|
||||||
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, this.installedFiles, {
|
const manifestStats = await manifestGen.generateManifests(bmadDir, allModulesForManifest, [...this.installedFiles], {
|
||||||
ides: config.ides || [],
|
ides: config.ides || [],
|
||||||
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
|
preservedModules: modulesForCsvPreserve, // Scan these from installed bmad/ dir
|
||||||
customModules: config._customModulesToTrack || [], // Custom modules to exclude from regular modules list
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add custom modules to manifest (now that it exists)
|
// Custom modules are now included in the main modules list - no separate tracking needed
|
||||||
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(
|
spinner.succeed(
|
||||||
`Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`,
|
`Manifests generated: ${manifestStats.workflows} workflows, ${manifestStats.agents} agents, ${manifestStats.tasks} tasks, ${manifestStats.tools} tools, ${manifestStats.files} files`,
|
||||||
|
|
@ -1186,7 +1126,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
|
|
||||||
// Pass pre-collected configuration to avoid re-prompting
|
// Pass pre-collected configuration to avoid re-prompting
|
||||||
await this.ideManager.setup(ide, projectDir, bmadDir, {
|
await this.ideManager.setup(ide, projectDir, bmadDir, {
|
||||||
selectedModules: config.modules || [],
|
selectedModules: allModules || [],
|
||||||
preCollectedConfig: ideConfigurations[ide] || null,
|
preCollectedConfig: ideConfigurations[ide] || null,
|
||||||
verbose: config.verbose,
|
verbose: config.verbose,
|
||||||
});
|
});
|
||||||
|
|
@ -1313,11 +1253,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
// Report custom and modified files if any were found
|
// Report custom and modified files if any were found
|
||||||
if (customFiles.length > 0) {
|
if (customFiles.length > 0) {
|
||||||
console.log(chalk.cyan(`\n📁 Custom files preserved: ${customFiles.length}`));
|
console.log(chalk.cyan(`\n📁 Custom files preserved: ${customFiles.length}`));
|
||||||
console.log(chalk.dim('The following custom files were found and restored:\n'));
|
|
||||||
for (const customFile of customFiles) {
|
|
||||||
const relativePath = path.relative(projectDir, customFile);
|
|
||||||
console.log(chalk.dim(` • ${relativePath}`));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modifiedFiles.length > 0) {
|
if (modifiedFiles.length > 0) {
|
||||||
|
|
@ -1380,12 +1315,44 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
|
|
||||||
// Check for custom modules with missing sources before update
|
// Check for custom modules with missing sources before update
|
||||||
const customModuleSources = new Map();
|
const customModuleSources = new Map();
|
||||||
|
|
||||||
|
// Check manifest for backward compatibility
|
||||||
if (existingInstall.customModules) {
|
if (existingInstall.customModules) {
|
||||||
for (const customModule of existingInstall.customModules) {
|
for (const customModule of existingInstall.customModules) {
|
||||||
customModuleSources.set(customModule.id, customModule);
|
customModuleSources.set(customModule.id, customModule);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also check cache directory
|
||||||
|
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||||
|
if (await fs.pathExists(cacheDir)) {
|
||||||
|
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const cachedModule of cachedModules) {
|
||||||
|
if (cachedModule.isDirectory()) {
|
||||||
|
const moduleId = cachedModule.name;
|
||||||
|
|
||||||
|
// Skip if we already have this module
|
||||||
|
if (customModuleSources.has(moduleId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedPath = path.join(cacheDir, moduleId);
|
||||||
|
|
||||||
|
// Check if this is actually a custom module (has module.yaml)
|
||||||
|
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
||||||
|
if (await fs.pathExists(moduleYamlPath)) {
|
||||||
|
customModuleSources.set(moduleId, {
|
||||||
|
id: moduleId,
|
||||||
|
name: moduleId,
|
||||||
|
sourcePath: path.join('_config', 'custom', moduleId), // Relative path
|
||||||
|
cached: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (customModuleSources.size > 0) {
|
if (customModuleSources.size > 0) {
|
||||||
spinner.stop();
|
spinner.stop();
|
||||||
console.log(chalk.yellow('\nChecking custom module sources before update...'));
|
console.log(chalk.yellow('\nChecking custom module sources before update...'));
|
||||||
|
|
@ -1578,7 +1545,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
await fs.writeFile(configPath, content.endsWith('\n') ? content : content + '\n', 'utf8');
|
await fs.writeFile(configPath, content.endsWith('\n') ? content : content + '\n', 'utf8');
|
||||||
|
|
||||||
// Track the config file in installedFiles
|
// Track the config file in installedFiles
|
||||||
this.installedFiles.push(configPath);
|
this.installedFiles.add(configPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1617,7 +1584,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
moduleName,
|
moduleName,
|
||||||
bmadDir,
|
bmadDir,
|
||||||
(filePath) => {
|
(filePath) => {
|
||||||
this.installedFiles.push(filePath);
|
this.installedFiles.add(filePath);
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
skipModuleInstaller: true, // We'll run it later after IDE setup
|
skipModuleInstaller: true, // We'll run it later after IDE setup
|
||||||
|
|
@ -1654,7 +1621,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
|
|
||||||
if (await fs.pathExists(sourcePath)) {
|
if (await fs.pathExists(sourcePath)) {
|
||||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
||||||
this.installedFiles.push(targetPath);
|
this.installedFiles.add(targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1670,7 +1637,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
|
|
||||||
if (await fs.pathExists(sourcePath)) {
|
if (await fs.pathExists(sourcePath)) {
|
||||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
||||||
this.installedFiles.push(targetPath);
|
this.installedFiles.add(targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1686,7 +1653,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
|
|
||||||
if (await fs.pathExists(sourcePath)) {
|
if (await fs.pathExists(sourcePath)) {
|
||||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
||||||
this.installedFiles.push(targetPath);
|
this.installedFiles.add(targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1702,7 +1669,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
|
|
||||||
if (await fs.pathExists(sourcePath)) {
|
if (await fs.pathExists(sourcePath)) {
|
||||||
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
await this.copyFileWithPlaceholderReplacement(sourcePath, targetPath, this.bmadFolderName || 'bmad');
|
||||||
this.installedFiles.push(targetPath);
|
this.installedFiles.add(targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1717,7 +1684,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
|
|
||||||
if (await fs.pathExists(dataPath)) {
|
if (await fs.pathExists(dataPath)) {
|
||||||
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad');
|
await this.copyFileWithPlaceholderReplacement(dataPath, targetPath, this.bmadFolderName || 'bmad');
|
||||||
this.installedFiles.push(targetPath);
|
this.installedFiles.add(targetPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1816,7 +1783,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Track the installed file
|
// Track the installed file
|
||||||
this.installedFiles.push(targetFile);
|
this.installedFiles.add(targetFile);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2281,35 +2248,35 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
const configuredIdes = existingInstall.ides || [];
|
const configuredIdes = existingInstall.ides || [];
|
||||||
const projectRoot = path.dirname(bmadDir);
|
const projectRoot = path.dirname(bmadDir);
|
||||||
|
|
||||||
// Get custom module sources from manifest
|
// Get custom module sources from cache
|
||||||
const customModuleSources = new Map();
|
const customModuleSources = new Map();
|
||||||
if (existingInstall.customModules) {
|
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||||
for (const customModule of existingInstall.customModules) {
|
if (await fs.pathExists(cacheDir)) {
|
||||||
// Ensure we have an absolute sourcePath
|
const cachedModules = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||||
let absoluteSourcePath = customModule.sourcePath;
|
|
||||||
|
|
||||||
// Check if sourcePath is a cache-relative path (starts with _config/)
|
for (const cachedModule of cachedModules) {
|
||||||
if (absoluteSourcePath && absoluteSourcePath.startsWith('_config')) {
|
if (cachedModule.isDirectory()) {
|
||||||
// Convert cache-relative path to absolute path
|
const moduleId = cachedModule.name;
|
||||||
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
|
// Skip if we already have this module from manifest
|
||||||
const updatedModule = {
|
if (customModuleSources.has(moduleId)) {
|
||||||
...customModule,
|
continue;
|
||||||
sourcePath: absoluteSourcePath,
|
}
|
||||||
};
|
|
||||||
|
|
||||||
customModuleSources.set(customModule.id, updatedModule);
|
const cachedPath = path.join(cacheDir, moduleId);
|
||||||
|
|
||||||
|
// Check if this is actually a custom module (has module.yaml)
|
||||||
|
const moduleYamlPath = path.join(cachedPath, 'module.yaml');
|
||||||
|
if (await fs.pathExists(moduleYamlPath)) {
|
||||||
|
// For quick update, we always rebuild from cache
|
||||||
|
customModuleSources.set(moduleId, {
|
||||||
|
id: moduleId,
|
||||||
|
name: moduleId, // We'll read the actual name if needed
|
||||||
|
sourcePath: cachedPath,
|
||||||
|
cached: true, // Flag to indicate this is from cache
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2341,126 +2308,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for untracked custom modules (installed but not in manifest)
|
|
||||||
const untrackedCustomModules = [];
|
|
||||||
for (const installedModule of installedModules) {
|
|
||||||
// Skip standard modules and core
|
|
||||||
const standardModuleIds = ['bmb', 'bmgd', 'bmm', 'cis', 'core'];
|
|
||||||
if (standardModuleIds.includes(installedModule)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this installed module is not tracked in customModules
|
|
||||||
if (!customModuleSources.has(installedModule)) {
|
|
||||||
const modulePath = path.join(bmadDir, installedModule);
|
|
||||||
if (await fs.pathExists(modulePath)) {
|
|
||||||
untrackedCustomModules.push({
|
|
||||||
id: installedModule,
|
|
||||||
name: installedModule, // We don't have the original name
|
|
||||||
path: modulePath,
|
|
||||||
untracked: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we found untracked custom modules, offer to track them
|
|
||||||
if (untrackedCustomModules.length > 0) {
|
|
||||||
spinner.stop();
|
|
||||||
console.log(chalk.yellow(`\n⚠️ Found ${untrackedCustomModules.length} custom module(s) not tracked in manifest:`));
|
|
||||||
|
|
||||||
for (const untracked of untrackedCustomModules) {
|
|
||||||
console.log(chalk.dim(` • ${untracked.id} (installed at ${path.relative(projectRoot, untracked.path)})`));
|
|
||||||
}
|
|
||||||
|
|
||||||
const { trackModules } = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'trackModules',
|
|
||||||
message: chalk.cyan('Would you like to scan for their source locations?'),
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (trackModules) {
|
|
||||||
const { scanDirectory } = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
name: 'scanDirectory',
|
|
||||||
message: 'Enter directory to scan for custom module sources (or leave blank to skip):',
|
|
||||||
default: projectRoot,
|
|
||||||
validate: async (input) => {
|
|
||||||
if (input && input.trim() !== '') {
|
|
||||||
const expandedPath = path.resolve(input.trim());
|
|
||||||
if (!(await fs.pathExists(expandedPath))) {
|
|
||||||
return 'Directory does not exist';
|
|
||||||
}
|
|
||||||
const stats = await fs.stat(expandedPath);
|
|
||||||
if (!stats.isDirectory()) {
|
|
||||||
return 'Path must be a directory';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (scanDirectory && scanDirectory.trim() !== '') {
|
|
||||||
console.log(chalk.dim('\nScanning for custom module sources...'));
|
|
||||||
|
|
||||||
// Scan for all module.yaml files
|
|
||||||
const allModulePaths = await this.moduleManager.findModulesInProject(scanDirectory);
|
|
||||||
const { ModuleManager } = require('../modules/manager');
|
|
||||||
const mm = new ModuleManager({ scanProjectForModules: true });
|
|
||||||
|
|
||||||
for (const untracked of untrackedCustomModules) {
|
|
||||||
let foundSource = null;
|
|
||||||
|
|
||||||
// Try to find by module ID
|
|
||||||
for (const modulePath of allModulePaths) {
|
|
||||||
try {
|
|
||||||
const moduleInfo = await mm.getModuleInfo(modulePath);
|
|
||||||
if (moduleInfo && moduleInfo.id === untracked.id) {
|
|
||||||
foundSource = {
|
|
||||||
path: modulePath,
|
|
||||||
info: moduleInfo,
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Continue searching
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (foundSource) {
|
|
||||||
console.log(chalk.green(` ✓ Found source for ${untracked.id}: ${path.relative(projectRoot, foundSource.path)}`));
|
|
||||||
|
|
||||||
// Add to manifest
|
|
||||||
await this.manifest.addCustomModule(bmadDir, {
|
|
||||||
id: untracked.id,
|
|
||||||
name: foundSource.info.name || untracked.name,
|
|
||||||
sourcePath: path.resolve(foundSource.path),
|
|
||||||
installDate: new Date().toISOString(),
|
|
||||||
tracked: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add to customModuleSources for processing
|
|
||||||
customModuleSources.set(untracked.id, {
|
|
||||||
id: untracked.id,
|
|
||||||
name: foundSource.info.name || untracked.name,
|
|
||||||
sourcePath: path.resolve(foundSource.path),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.log(chalk.yellow(` ⚠ Could not find source for ${untracked.id}`));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(chalk.dim('\nUntracked custom modules will remain installed but cannot be updated without their source.'));
|
|
||||||
spinner.start('Preparing update...');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle missing custom module sources using shared method
|
// Handle missing custom module sources using shared method
|
||||||
const customModuleResult = await this.handleMissingCustomSources(
|
const customModuleResult = await this.handleMissingCustomSources(
|
||||||
customModuleSources,
|
customModuleSources,
|
||||||
|
|
@ -2478,18 +2325,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
hasUpdate: 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 allAvailableModules = [...availableModules, ...customModulesFromManifest];
|
||||||
const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
|
const availableModuleIds = new Set(allAvailableModules.map((m) => m.id));
|
||||||
|
|
||||||
|
|
@ -2762,14 +2597,10 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
const installedFilesMap = new Map();
|
const installedFilesMap = new Map();
|
||||||
for (const fileEntry of existingFilesManifest) {
|
for (const fileEntry of existingFilesManifest) {
|
||||||
if (fileEntry.path) {
|
if (fileEntry.path) {
|
||||||
// Paths are relative to bmadDir. Legacy manifests incorrectly prefixed 'bmad/' -
|
const absolutePath = path.join(bmadDir, fileEntry.path);
|
||||||
// strip it if present. This is safe because no real path inside bmadDir would
|
|
||||||
// start with 'bmad/' (you'd never have _bmad/bmad/... as an actual structure).
|
|
||||||
const relativePath = fileEntry.path.startsWith('bmad/') ? fileEntry.path.slice(5) : fileEntry.path;
|
|
||||||
const absolutePath = path.join(bmadDir, relativePath);
|
|
||||||
installedFilesMap.set(path.normalize(absolutePath), {
|
installedFilesMap.set(path.normalize(absolutePath), {
|
||||||
hash: fileEntry.hash,
|
hash: fileEntry.hash,
|
||||||
relativePath: relativePath,
|
relativePath: fileEntry.path,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2821,7 +2652,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip config.yaml files - these are regenerated on each install/update
|
// Skip config.yaml files - these are regenerated on each install/update
|
||||||
// Users should use _config/agents/ override files instead
|
|
||||||
if (fileName === 'config.yaml') {
|
if (fileName === 'config.yaml') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -2844,8 +2674,6 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If manifest doesn't have hashes, we can't detect modifications
|
|
||||||
// so we just skip files that are in the manifest
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
|
@ -2975,7 +2803,7 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(configPath, configContent, 'utf8');
|
await fs.writeFile(configPath, configContent, 'utf8');
|
||||||
this.installedFiles.push(configPath); // Track agent config files
|
this.installedFiles.add(configPath); // Track agent config files
|
||||||
createdCount++;
|
createdCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3065,13 +2893,23 @@ If AgentVibes party mode is enabled, immediately trigger TTS with agent's voice:
|
||||||
info: customInfo,
|
info: customInfo,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
customModulesWithMissingSources.push({
|
// For cached modules that are missing, we just skip them without prompting
|
||||||
id: moduleId,
|
if (customInfo.cached) {
|
||||||
name: customInfo.name,
|
// Skip cached modules without prompting
|
||||||
sourcePath: customInfo.sourcePath,
|
keptModulesWithoutSources.push({
|
||||||
relativePath: customInfo.relativePath,
|
id: moduleId,
|
||||||
info: customInfo,
|
name: customInfo.name,
|
||||||
});
|
cached: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
customModulesWithMissingSources.push({
|
||||||
|
id: moduleId,
|
||||||
|
name: customInfo.name,
|
||||||
|
sourcePath: customInfo.sourcePath,
|
||||||
|
relativePath: customInfo.relativePath,
|
||||||
|
info: customInfo,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,24 +34,21 @@ class ManifestGenerator {
|
||||||
|
|
||||||
// Store modules list (all modules including preserved ones)
|
// Store modules list (all modules including preserved ones)
|
||||||
const preservedModules = options.preservedModules || [];
|
const preservedModules = options.preservedModules || [];
|
||||||
const customModules = options.customModules || [];
|
|
||||||
|
|
||||||
// Scan the bmad directory to find all actually installed modules
|
// Scan the bmad directory to find all actually installed modules
|
||||||
const installedModules = await this.scanInstalledModules(bmadDir);
|
const installedModules = await this.scanInstalledModules(bmadDir);
|
||||||
|
|
||||||
// Filter out custom modules from the regular modules list
|
// Since custom modules are now installed the same way as regular modules,
|
||||||
const customModuleIds = new Set(customModules.map((cm) => cm.id));
|
// we don't need to exclude them from manifest generation
|
||||||
const regularModules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])].filter(
|
const allModules = [...new Set(['core', ...selectedModules, ...preservedModules, ...installedModules])];
|
||||||
(module) => !customModuleIds.has(module),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.modules = regularModules;
|
this.modules = allModules;
|
||||||
this.updatedModules = [...new Set(['core', ...selectedModules, ...installedModules])].filter((module) => !customModuleIds.has(module)); // Also exclude custom modules from rescanning
|
this.updatedModules = allModules; // Include ALL modules (including custom) for scanning
|
||||||
|
|
||||||
// For CSV manifests, we need to include ALL modules that are installed
|
// 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)
|
// preservedModules controls which modules stay as-is in the CSV (don't get rescanned)
|
||||||
// But all modules should be included in the final manifest
|
// But all modules should be included in the final manifest
|
||||||
this.preservedModules = [...new Set([...preservedModules, ...selectedModules, ...installedModules])]; // Include all installed modules
|
this.preservedModules = allModules; // Include ALL modules (including custom)
|
||||||
this.bmadDir = bmadDir;
|
this.bmadDir = bmadDir;
|
||||||
this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '_bmad' or 'bmad')
|
this.bmadFolderName = path.basename(bmadDir); // Get the actual folder name (e.g., '_bmad' or 'bmad')
|
||||||
this.allInstalledFiles = installedFiles;
|
this.allInstalledFiles = installedFiles;
|
||||||
|
|
@ -460,29 +457,13 @@ class ManifestGenerator {
|
||||||
async writeMainManifest(cfgDir) {
|
async writeMainManifest(cfgDir) {
|
||||||
const manifestPath = path.join(cfgDir, 'manifest.yaml');
|
const manifestPath = path.join(cfgDir, 'manifest.yaml');
|
||||||
|
|
||||||
// Read existing manifest to preserve custom modules
|
|
||||||
let existingCustomModules = [];
|
|
||||||
if (await fs.pathExists(manifestPath)) {
|
|
||||||
try {
|
|
||||||
const existingContent = await fs.readFile(manifestPath, 'utf8');
|
|
||||||
const existingManifest = yaml.parse(existingContent);
|
|
||||||
if (existingManifest && existingManifest.customModules) {
|
|
||||||
existingCustomModules = existingManifest.customModules;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// If we can't read the existing manifest, continue without preserving custom modules
|
|
||||||
console.warn('Warning: Could not read existing manifest to preserve custom modules');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
installation: {
|
installation: {
|
||||||
version: packageJson.version,
|
version: packageJson.version,
|
||||||
installDate: new Date().toISOString(),
|
installDate: new Date().toISOString(),
|
||||||
lastUpdated: new Date().toISOString(),
|
lastUpdated: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
modules: this.modules,
|
modules: this.modules, // Include ALL modules (standard and custom)
|
||||||
customModules: existingCustomModules, // Preserve custom modules
|
|
||||||
ides: this.selectedIdes,
|
ides: this.selectedIdes,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -62,8 +62,8 @@ class Manifest {
|
||||||
version: manifestData.installation?.version,
|
version: manifestData.installation?.version,
|
||||||
installDate: manifestData.installation?.installDate,
|
installDate: manifestData.installation?.installDate,
|
||||||
lastUpdated: manifestData.installation?.lastUpdated,
|
lastUpdated: manifestData.installation?.lastUpdated,
|
||||||
modules: manifestData.modules || [],
|
modules: manifestData.modules || [], // All modules (standard and custom)
|
||||||
customModules: manifestData.customModules || [],
|
customModules: manifestData.customModules || [], // Keep for backward compatibility
|
||||||
ides: manifestData.ides || [],
|
ides: manifestData.ides || [],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -95,8 +95,7 @@ class Manifest {
|
||||||
installDate: manifest.installDate,
|
installDate: manifest.installDate,
|
||||||
lastUpdated: manifest.lastUpdated,
|
lastUpdated: manifest.lastUpdated,
|
||||||
},
|
},
|
||||||
modules: manifest.modules || [],
|
modules: manifest.modules || [], // All modules (standard and custom)
|
||||||
customModules: manifest.customModules || [],
|
|
||||||
ides: manifest.ides || [],
|
ides: manifest.ides || [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,6 @@ class ModuleManager {
|
||||||
this.modulesSourcePath = getSourcePath('modules');
|
this.modulesSourcePath = getSourcePath('modules');
|
||||||
this.xmlHandler = new XmlHandler();
|
this.xmlHandler = new XmlHandler();
|
||||||
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
this.bmadFolderName = 'bmad'; // Default, can be overridden
|
||||||
this.scanProjectForModules = options.scanProjectForModules !== false; // Default to true for backward compatibility
|
|
||||||
this.customModulePaths = new Map(); // Initialize custom module paths
|
this.customModulePaths = new Map(); // Initialize custom module paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,76 +115,6 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Find all modules in the project by searching for module.yaml files
|
|
||||||
* @returns {Array} List of module paths
|
|
||||||
*/
|
|
||||||
async findModulesInProject() {
|
|
||||||
const projectRoot = getProjectRoot();
|
|
||||||
const modulePaths = new Set();
|
|
||||||
|
|
||||||
// Helper function to recursively scan directories
|
|
||||||
async function scanDirectory(dir, excludePaths = []) {
|
|
||||||
try {
|
|
||||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const fullPath = path.join(dir, entry.name);
|
|
||||||
|
|
||||||
// Skip hidden directories, node_modules, and literal placeholder directories
|
|
||||||
if (
|
|
||||||
entry.name.startsWith('.') ||
|
|
||||||
entry.name === 'node_modules' ||
|
|
||||||
entry.name === 'dist' ||
|
|
||||||
entry.name === 'build' ||
|
|
||||||
entry.name === '{project-root}'
|
|
||||||
) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip excluded paths
|
|
||||||
if (excludePaths.some((exclude) => fullPath.startsWith(exclude))) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
// Skip core module - it's always installed first and not selectable
|
|
||||||
if (entry.name === 'core') {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this directory contains a module (module.yaml OR custom.yaml)
|
|
||||||
const moduleConfigPath = path.join(fullPath, 'module.yaml');
|
|
||||||
const installerConfigPath = path.join(fullPath, '_module-installer', 'module.yaml');
|
|
||||||
const customConfigPath = path.join(fullPath, '_module-installer', 'custom.yaml');
|
|
||||||
const rootCustomConfigPath = path.join(fullPath, 'custom.yaml');
|
|
||||||
|
|
||||||
if (
|
|
||||||
(await fs.pathExists(moduleConfigPath)) ||
|
|
||||||
(await fs.pathExists(installerConfigPath)) ||
|
|
||||||
(await fs.pathExists(customConfigPath)) ||
|
|
||||||
(await fs.pathExists(rootCustomConfigPath))
|
|
||||||
) {
|
|
||||||
modulePaths.add(fullPath);
|
|
||||||
// Don't scan inside modules - they might have their own nested structures
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively scan subdirectories
|
|
||||||
await scanDirectory(fullPath, excludePaths);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore errors (e.g., permission denied)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scan the entire project, but exclude src/modules since we handle it separately
|
|
||||||
await scanDirectory(projectRoot, [this.modulesSourcePath]);
|
|
||||||
|
|
||||||
return [...modulePaths];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List all available modules (excluding core which is always installed)
|
* List all available modules (excluding core which is always installed)
|
||||||
* @returns {Object} Object with modules array and customModules array
|
* @returns {Object} Object with modules array and customModules array
|
||||||
|
|
@ -228,43 +157,19 @@ class ModuleManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Then, find all other modules in the project (only if scanning is enabled)
|
// Check for cached custom modules in _config/custom/
|
||||||
if (this.scanProjectForModules) {
|
if (this.bmadDir) {
|
||||||
const otherModulePaths = await this.findModulesInProject();
|
const customCacheDir = path.join(this.bmadDir, '_config', 'custom');
|
||||||
for (const modulePath of otherModulePaths) {
|
if (await fs.pathExists(customCacheDir)) {
|
||||||
const moduleName = path.basename(modulePath);
|
const cacheEntries = await fs.readdir(customCacheDir, { withFileTypes: true });
|
||||||
const relativePath = path.relative(getProjectRoot(), modulePath);
|
for (const entry of cacheEntries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
// Skip core module - it's always installed first and not selectable
|
const cachePath = path.join(customCacheDir, entry.name);
|
||||||
if (moduleName === 'core') {
|
const moduleInfo = await this.getModuleInfo(cachePath, entry.name, '_config/custom');
|
||||||
continue;
|
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id) && !customModules.some((m) => m.id === moduleInfo.id)) {
|
||||||
}
|
moduleInfo.isCustom = true;
|
||||||
|
moduleInfo.fromCache = true;
|
||||||
const moduleInfo = await this.getModuleInfo(modulePath, moduleName, relativePath);
|
customModules.push(moduleInfo);
|
||||||
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id) && !customModules.some((m) => m.id === moduleInfo.id)) {
|
|
||||||
// Avoid duplicates - skip if we already have this module ID
|
|
||||||
if (moduleInfo.isCustom) {
|
|
||||||
customModules.push(moduleInfo);
|
|
||||||
} else {
|
|
||||||
modules.push(moduleInfo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check for cached custom modules in _config/custom/
|
|
||||||
if (this.bmadDir) {
|
|
||||||
const customCacheDir = path.join(this.bmadDir, '_config', 'custom');
|
|
||||||
if (await fs.pathExists(customCacheDir)) {
|
|
||||||
const cacheEntries = await fs.readdir(customCacheDir, { withFileTypes: true });
|
|
||||||
for (const entry of cacheEntries) {
|
|
||||||
if (entry.isDirectory()) {
|
|
||||||
const cachePath = path.join(customCacheDir, entry.name);
|
|
||||||
const moduleInfo = await this.getModuleInfo(cachePath, entry.name, '_config/custom');
|
|
||||||
if (moduleInfo && !modules.some((m) => m.id === moduleInfo.id) && !customModules.some((m) => m.id === moduleInfo.id)) {
|
|
||||||
moduleInfo.isCustom = true;
|
|
||||||
moduleInfo.fromCache = true;
|
|
||||||
customModules.push(moduleInfo);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -343,71 +248,50 @@ class ModuleManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the source path for a module by searching all possible locations
|
* Find the source path for a module by searching all possible locations
|
||||||
* @param {string} moduleName - Name of the module to find
|
* @param {string} moduleCode - Code of the module to find (from module.yaml)
|
||||||
* @returns {string|null} Path to the module source or null if not found
|
* @returns {string|null} Path to the module source or null if not found
|
||||||
*/
|
*/
|
||||||
async findModuleSource(moduleName) {
|
async findModuleSource(moduleCode) {
|
||||||
const projectRoot = getProjectRoot();
|
const projectRoot = getProjectRoot();
|
||||||
|
|
||||||
// First check custom module paths if they exist
|
// First check custom module paths if they exist
|
||||||
if (this.customModulePaths && this.customModulePaths.has(moduleName)) {
|
if (this.customModulePaths && this.customModulePaths.has(moduleCode)) {
|
||||||
return this.customModulePaths.get(moduleName);
|
return this.customModulePaths.get(moduleCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, check src/modules
|
// Search in src/modules by READING module.yaml files to match by code
|
||||||
const srcModulePath = path.join(this.modulesSourcePath, moduleName);
|
if (await fs.pathExists(this.modulesSourcePath)) {
|
||||||
if (await fs.pathExists(srcModulePath)) {
|
const entries = await fs.readdir(this.modulesSourcePath, { withFileTypes: true });
|
||||||
// Check if this looks like a module (has module.yaml)
|
for (const entry of entries) {
|
||||||
const moduleConfigPath = path.join(srcModulePath, 'module.yaml');
|
if (entry.isDirectory()) {
|
||||||
const installerConfigPath = path.join(srcModulePath, '_module-installer', 'module.yaml');
|
const modulePath = path.join(this.modulesSourcePath, entry.name);
|
||||||
|
|
||||||
if ((await fs.pathExists(moduleConfigPath)) || (await fs.pathExists(installerConfigPath))) {
|
// Read module.yaml to get the code
|
||||||
return srcModulePath;
|
const moduleConfigPath = path.join(modulePath, 'module.yaml');
|
||||||
}
|
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
|
||||||
|
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
|
||||||
|
|
||||||
// Also check for custom.yaml in src/modules/_module-installer
|
let configPath = null;
|
||||||
const customConfigPath = path.join(srcModulePath, '_module-installer', 'custom.yaml');
|
if (await fs.pathExists(moduleConfigPath)) {
|
||||||
if (await fs.pathExists(customConfigPath)) {
|
configPath = moduleConfigPath;
|
||||||
return srcModulePath;
|
} else if (await fs.pathExists(installerConfigPath)) {
|
||||||
}
|
configPath = installerConfigPath;
|
||||||
}
|
} else if (await fs.pathExists(customConfigPath)) {
|
||||||
|
configPath = customConfigPath;
|
||||||
// If not found in src/modules, search the entire project
|
}
|
||||||
const allModulePaths = await this.findModulesInProject();
|
|
||||||
for (const modulePath of allModulePaths) {
|
if (configPath) {
|
||||||
if (path.basename(modulePath) === moduleName) {
|
try {
|
||||||
return modulePath;
|
const configContent = await fs.readFile(configPath, 'utf8');
|
||||||
}
|
const config = yaml.parse(configContent);
|
||||||
}
|
if (config.code === moduleCode) {
|
||||||
|
return modulePath;
|
||||||
// Also check by module ID (not just folder name)
|
}
|
||||||
// Need to read configs to match by ID
|
} catch (error) {
|
||||||
for (const modulePath of allModulePaths) {
|
// Continue to next module if parse fails
|
||||||
const moduleConfigPath = path.join(modulePath, 'module.yaml');
|
console.warn(`Warning: Failed to parse module config at ${configPath}: ${error.message}`);
|
||||||
const installerConfigPath = path.join(modulePath, '_module-installer', 'module.yaml');
|
}
|
||||||
const customConfigPath = path.join(modulePath, '_module-installer', 'custom.yaml');
|
|
||||||
const rootCustomConfigPath = path.join(modulePath, 'custom.yaml');
|
|
||||||
|
|
||||||
let configPath = null;
|
|
||||||
if (await fs.pathExists(moduleConfigPath)) {
|
|
||||||
configPath = moduleConfigPath;
|
|
||||||
} else if (await fs.pathExists(installerConfigPath)) {
|
|
||||||
configPath = installerConfigPath;
|
|
||||||
} else if (await fs.pathExists(customConfigPath)) {
|
|
||||||
configPath = customConfigPath;
|
|
||||||
} else if (await fs.pathExists(rootCustomConfigPath)) {
|
|
||||||
configPath = rootCustomConfigPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (configPath) {
|
|
||||||
try {
|
|
||||||
const configContent = await fs.readFile(configPath, 'utf8');
|
|
||||||
const config = yaml.parse(configContent);
|
|
||||||
if (config.code === moduleName) {
|
|
||||||
return modulePath;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to parse module.yaml at ${configPath}: ${error.message}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -417,7 +301,7 @@ class ModuleManager {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Install a module
|
* Install a module
|
||||||
* @param {string} moduleName - Name of the module to install
|
* @param {string} moduleName - Code of the module to install (from module.yaml)
|
||||||
* @param {string} bmadDir - Target bmad directory
|
* @param {string} bmadDir - Target bmad directory
|
||||||
* @param {Function} fileTrackingCallback - Optional callback to track installed files
|
* @param {Function} fileTrackingCallback - Optional callback to track installed files
|
||||||
* @param {Object} options - Additional installation options
|
* @param {Object} options - Additional installation options
|
||||||
|
|
@ -431,7 +315,10 @@ class ModuleManager {
|
||||||
|
|
||||||
// Check if source module exists
|
// Check if source module exists
|
||||||
if (!sourcePath) {
|
if (!sourcePath) {
|
||||||
throw new Error(`Module '${moduleName}' not found in any source location`);
|
// Provide a more user-friendly error message
|
||||||
|
throw new Error(
|
||||||
|
`Source for module '${moduleName}' is not available. It will be retained but cannot be updated without its source files.`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a custom module and read its custom.yaml values
|
// Check if this is a custom module and read its custom.yaml values
|
||||||
|
|
@ -465,7 +352,6 @@ class ModuleManager {
|
||||||
|
|
||||||
// Check if already installed
|
// Check if already installed
|
||||||
if (await fs.pathExists(targetPath)) {
|
if (await fs.pathExists(targetPath)) {
|
||||||
console.log(chalk.yellow(`Module '${moduleName}' already installed, updating...`));
|
|
||||||
await fs.remove(targetPath);
|
await fs.remove(targetPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,11 +195,7 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common actions
|
// Common actions
|
||||||
choices.push(
|
choices.push({ name: 'Modify BMAD Installation', value: 'update' });
|
||||||
{ name: 'Modify BMAD Installation', value: 'update' },
|
|
||||||
{ name: 'Add / Update Custom Content', value: 'add-custom' },
|
|
||||||
{ name: 'Rebuild Agents', value: 'compile' },
|
|
||||||
);
|
|
||||||
|
|
||||||
const promptResult = await inquirer.prompt([
|
const promptResult = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
|
|
@ -224,64 +220,6 @@ class UI {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle add custom content separately
|
|
||||||
if (actionType === 'add-custom') {
|
|
||||||
customContentConfig = await this.promptCustomContentSource();
|
|
||||||
// After adding custom content, continue to select additional modules
|
|
||||||
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
|
||||||
|
|
||||||
// Ask if user wants to add additional modules
|
|
||||||
const { wantsMoreModules } = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'wantsMoreModules',
|
|
||||||
message: 'Do you want to add any additional modules?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
let selectedModules = [];
|
|
||||||
if (wantsMoreModules) {
|
|
||||||
const moduleChoices = await this.getModuleChoices(installedModuleIds, customContentConfig);
|
|
||||||
selectedModules = await this.selectModules(moduleChoices);
|
|
||||||
|
|
||||||
// Process custom content selection
|
|
||||||
const selectedCustomContent = selectedModules.filter((mod) => mod.startsWith('__CUSTOM_CONTENT__'));
|
|
||||||
|
|
||||||
if (selectedCustomContent.length > 0) {
|
|
||||||
customContentConfig.selected = true;
|
|
||||||
customContentConfig.selectedFiles = selectedCustomContent.map((mod) => mod.replace('__CUSTOM_CONTENT__', ''));
|
|
||||||
|
|
||||||
// Convert to module IDs
|
|
||||||
const customContentModuleIds = [];
|
|
||||||
const customHandler = new CustomHandler();
|
|
||||||
for (const customFile of customContentConfig.selectedFiles) {
|
|
||||||
const customInfo = await customHandler.getCustomInfo(customFile);
|
|
||||||
if (customInfo) {
|
|
||||||
customContentModuleIds.push(customInfo.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectedModules = [...selectedModules.filter((mod) => !mod.startsWith('__CUSTOM_CONTENT__')), ...customContentModuleIds];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
actionType: 'update',
|
|
||||||
directory: confirmedDirectory,
|
|
||||||
installCore: false, // Don't reinstall core
|
|
||||||
modules: selectedModules,
|
|
||||||
customContent: customContentConfig,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle agent compilation separately
|
|
||||||
if (actionType === 'compile') {
|
|
||||||
return {
|
|
||||||
actionType: 'compile',
|
|
||||||
directory: confirmedDirectory,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// If actionType === 'update', handle it with the new flow
|
// If actionType === 'update', handle it with the new flow
|
||||||
// Return early with modify configuration
|
// Return early with modify configuration
|
||||||
if (actionType === 'update') {
|
if (actionType === 'update') {
|
||||||
|
|
@ -293,7 +231,7 @@ class UI {
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'changeModuleSelection',
|
name: 'changeModuleSelection',
|
||||||
message: 'Change which modules are installed?',
|
message: 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
@ -307,6 +245,14 @@ class UI {
|
||||||
selectedModules = [...installedModuleIds];
|
selectedModules = [...installedModuleIds];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// After module selection, ask about custom modules
|
||||||
|
const customModuleResult = await this.handleCustomModulesInModifyFlow(confirmedDirectory, selectedModules);
|
||||||
|
|
||||||
|
// Merge any selected custom modules
|
||||||
|
if (customModuleResult.selectedCustomModules.length > 0) {
|
||||||
|
selectedModules.push(...customModuleResult.selectedCustomModules);
|
||||||
|
}
|
||||||
|
|
||||||
// Get tool selection
|
// Get tool selection
|
||||||
const toolSelection = await this.promptToolSelection(confirmedDirectory, selectedModules);
|
const toolSelection = await this.promptToolSelection(confirmedDirectory, selectedModules);
|
||||||
|
|
||||||
|
|
@ -337,7 +283,7 @@ class UI {
|
||||||
ides: toolSelection.ides,
|
ides: toolSelection.ides,
|
||||||
skipIde: toolSelection.skipIde,
|
skipIde: toolSelection.skipIde,
|
||||||
coreConfig: coreConfig,
|
coreConfig: coreConfig,
|
||||||
customContent: { hasCustomContent: false },
|
customContent: customModuleResult.customContentConfig,
|
||||||
enableAgentVibes: enableTts,
|
enableAgentVibes: enableTts,
|
||||||
agentVibesInstalled: false,
|
agentVibesInstalled: false,
|
||||||
};
|
};
|
||||||
|
|
@ -352,7 +298,7 @@ class UI {
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'wantsOfficialModules',
|
name: 'wantsOfficialModules',
|
||||||
message: 'Will you be installing any official modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
message: 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
@ -368,7 +314,7 @@ class UI {
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'wantsCustomContent',
|
name: 'wantsCustomContent',
|
||||||
message: 'Will you be installing any locally stored custom content?',
|
message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
@ -734,14 +680,8 @@ class UI {
|
||||||
|
|
||||||
// Add official modules
|
// Add official modules
|
||||||
const { ModuleManager } = require('../installers/lib/modules/manager');
|
const { ModuleManager } = require('../installers/lib/modules/manager');
|
||||||
// For new installations, don't scan project yet (will do after custom content is discovered)
|
const moduleManager = new ModuleManager();
|
||||||
// For existing installations, scan if user selected custom content
|
const { modules: availableModules, customModules: customModulesFromCache } = await moduleManager.listAvailable();
|
||||||
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
|
// First, add all items to appropriate sections
|
||||||
const allCustomModules = [];
|
const allCustomModules = [];
|
||||||
|
|
@ -749,14 +689,14 @@ class UI {
|
||||||
// Add custom content items from directory
|
// Add custom content items from directory
|
||||||
allCustomModules.push(...customContentItems);
|
allCustomModules.push(...customContentItems);
|
||||||
|
|
||||||
// Add custom modules from project scan (if scanning is enabled)
|
// Add custom modules from cache
|
||||||
for (const mod of customModulesFromProject) {
|
for (const mod of customModulesFromCache) {
|
||||||
// Skip if this module is already in customContentItems (by path)
|
// 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));
|
const isDuplicate = allCustomModules.some((item) => item.path && mod.path && path.resolve(item.path) === path.resolve(mod.path));
|
||||||
|
|
||||||
if (!isDuplicate) {
|
if (!isDuplicate) {
|
||||||
allCustomModules.push({
|
allCustomModules.push({
|
||||||
name: `${chalk.cyan('✓')} ${mod.name} ${chalk.gray(`(${mod.source})`)}`,
|
name: `${chalk.cyan('✓')} ${mod.name} ${chalk.gray(`(cached)`)}`,
|
||||||
value: mod.id,
|
value: mod.id,
|
||||||
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
|
checked: isNewInstallation ? mod.defaultSelected || false : installedModuleIds.has(mod.id),
|
||||||
});
|
});
|
||||||
|
|
@ -803,7 +743,9 @@ class UI {
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return moduleAnswer.modules || [];
|
const selected = moduleAnswer.modules || [];
|
||||||
|
|
||||||
|
return selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -1472,6 +1414,136 @@ class UI {
|
||||||
|
|
||||||
return customContentConfig;
|
return customContentConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle custom modules in the modify flow
|
||||||
|
* @param {string} directory - Installation directory
|
||||||
|
* @param {Array} selectedModules - Currently selected modules
|
||||||
|
* @returns {Object} Result with selected custom modules and custom content config
|
||||||
|
*/
|
||||||
|
async handleCustomModulesInModifyFlow(directory, selectedModules) {
|
||||||
|
// Get existing installation to find custom modules
|
||||||
|
const { existingInstall } = await this.getExistingInstallation(directory);
|
||||||
|
|
||||||
|
// Check if there are any custom modules in cache
|
||||||
|
const { Installer } = require('../installers/lib/core/installer');
|
||||||
|
const installer = new Installer();
|
||||||
|
const { bmadDir } = await installer.findBmadDir(directory);
|
||||||
|
|
||||||
|
const cacheDir = path.join(bmadDir, '_config', 'custom');
|
||||||
|
const cachedCustomModules = [];
|
||||||
|
|
||||||
|
if (await fs.pathExists(cacheDir)) {
|
||||||
|
const entries = await fs.readdir(cacheDir, { withFileTypes: true });
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
const moduleYamlPath = path.join(cacheDir, entry.name, 'module.yaml');
|
||||||
|
if (await fs.pathExists(moduleYamlPath)) {
|
||||||
|
const yaml = require('yaml');
|
||||||
|
const content = await fs.readFile(moduleYamlPath, 'utf8');
|
||||||
|
const moduleData = yaml.parse(content);
|
||||||
|
|
||||||
|
cachedCustomModules.push({
|
||||||
|
id: entry.name,
|
||||||
|
name: moduleData.name || entry.name,
|
||||||
|
description: moduleData.description || 'Custom module from cache',
|
||||||
|
checked: selectedModules.includes(entry.name),
|
||||||
|
fromCache: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
selectedCustomModules: [],
|
||||||
|
customContentConfig: { hasCustomContent: false },
|
||||||
|
};
|
||||||
|
|
||||||
|
if (cachedCustomModules.length === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ask user about custom modules
|
||||||
|
console.log(chalk.cyan('\n⚙️ Custom Modules'));
|
||||||
|
console.log(chalk.dim('Found custom modules in your installation:'));
|
||||||
|
|
||||||
|
const { customAction } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'list',
|
||||||
|
name: 'customAction',
|
||||||
|
message: 'What would you like to do with custom modules?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Keep all existing custom modules', value: 'keep' },
|
||||||
|
{ name: 'Select which custom modules to keep', value: 'select' },
|
||||||
|
{ name: 'Add new custom modules', value: 'add' },
|
||||||
|
{ name: 'Remove all custom modules', value: 'remove' },
|
||||||
|
],
|
||||||
|
default: 'keep',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
switch (customAction) {
|
||||||
|
case 'keep': {
|
||||||
|
// Keep all existing custom modules
|
||||||
|
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
|
||||||
|
console.log(chalk.dim(`Keeping ${result.selectedCustomModules.length} custom module(s)`));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'select': {
|
||||||
|
// Let user choose which to keep
|
||||||
|
const choices = cachedCustomModules.map((m) => ({
|
||||||
|
name: `${m.name} ${chalk.gray(`(${m.id})`)}`,
|
||||||
|
value: m.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { keepModules } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'checkbox',
|
||||||
|
name: 'keepModules',
|
||||||
|
message: 'Select custom modules to keep:',
|
||||||
|
choices: choices,
|
||||||
|
default: cachedCustomModules.filter((m) => m.checked).map((m) => m.id),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
result.selectedCustomModules = keepModules;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'add': {
|
||||||
|
// First ask to keep existing ones
|
||||||
|
const { keepExisting } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'keepExisting',
|
||||||
|
message: 'Keep existing custom modules?',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (keepExisting) {
|
||||||
|
result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then prompt for new ones (reuse existing method)
|
||||||
|
const newCustomContent = await this.promptCustomContentSource();
|
||||||
|
if (newCustomContent.hasCustomContent && newCustomContent.selected) {
|
||||||
|
result.selectedCustomModules.push(...newCustomContent.selectedModuleIds);
|
||||||
|
result.customContentConfig = newCustomContent;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'remove': {
|
||||||
|
// Remove all custom modules
|
||||||
|
console.log(chalk.yellow('All custom modules will be removed from the installation'));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { UI };
|
module.exports = { UI };
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue