Compare commits

..

No commits in common. "dde139a560d94fd7e588306a72bad5a64e66c894" and "00a23349697123c59daed00167040d65bb7dd83a" have entirely different histories.

104 changed files with 3331 additions and 4134 deletions

View File

@ -6,6 +6,7 @@ on:
- main - main
paths: paths:
- "docs/**" - "docs/**"
- "src/modules/*/docs/**"
- "website/**" - "website/**"
- "tools/build-docs.mjs" - "tools/build-docs.mjs"
- ".github/workflows/docs.yaml" - ".github/workflows/docs.yaml"
@ -18,7 +19,6 @@ permissions:
concurrency: concurrency:
group: "pages" group: "pages"
# No big win in setting this to true — risk of cancelling a deploy mid-flight.
cancel-in-progress: false cancel-in-progress: false
jobs: jobs:
@ -28,13 +28,12 @@ jobs:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
# Full history needed for Starlight's lastUpdated timestamps (git log)
fetch-depth: 0 fetch-depth: 0
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version-file: ".nvmrc" node-version: "20"
cache: "npm" cache: "npm"
- name: Install dependencies - name: Install dependencies

View File

@ -84,8 +84,10 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Validate documentation links
run: npm run docs:validate-links
- name: Build documentation - name: Build documentation
# Note: build-docs.mjs runs link validation internally before building
run: npm run docs:build run: npm run docs:build
validate: validate:

View File

@ -147,15 +147,6 @@ Keep messages under 72 characters. Each commit = one logical change.
- Everything is natural language (markdown) — no code in core framework - Everything is natural language (markdown) — no code in core framework
- Use BMad modules for domain-specific features - Use BMad modules for domain-specific features
- Validate YAML schemas: `npm run validate:schemas` - Validate YAML schemas: `npm run validate:schemas`
- Validate file references: `npm run validate:refs`
### File-Pattern-to-Validator Mapping
| File Pattern | Validator | Extraction Function |
| ------------ | --------- | ------------------- |
| `*.yaml`, `*.yml` | `validate-file-refs.js` | `extractYamlRefs` |
| `*.md`, `*.xml` | `validate-file-refs.js` | `extractMarkdownRefs` |
| `*.csv` | `validate-file-refs.js` | `extractCsvRefs` |
--- ---

View File

@ -36,7 +36,7 @@ Follow the installer prompts, then open your AI IDE (Claude Code, Cursor, Windsu
npx bmad-method install --directory /path/to/project --modules bmm --tools claude-code --yes npx bmad-method install --directory /path/to/project --modules bmm --tools claude-code --yes
``` ```
See [Non-Interactive Installation Guide](http://docs.bmad-method.org/how-to/non-interactive-installation/) for all available options. See [Non-Interactive Installation Guide](docs/non-interactive-installation.md) for all available options.
> **Not sure what to do?** Run `/bmad-help` — it tells you exactly what's next and what's optional. You can also ask it questions like: > **Not sure what to do?** Run `/bmad-help` — it tells you exactly what's next and what's optional. You can also ask it questions like:

View File

@ -6,4 +6,4 @@ template: splash
The page you're looking for doesn't exist or has been moved. The page you're looking for doesn't exist or has been moved.
[Return to Home](./index.md) [Return to Home](/docs/index.md)

BIN
docs/bmgd/bmgd-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

376
docs/bmgd/game-types.md Normal file
View File

@ -0,0 +1,376 @@
---
title: "Game Types Reference"
description: 24 game type templates with genre-specific GDD sections for BMGD
draft: true
---
BMGD supports 24 game type templates. Each adds genre-specific sections to your GDD.
## Game Types
### Action & Combat
#### Action Platformer
Side-scrolling or 3D platforming with combat mechanics.
**Examples:** Hollow Knight, Mega Man, Celeste
**GDD sections:**
- Movement systems (jumps, dashes, wall mechanics)
- Combat mechanics (melee/ranged, combos)
- Level design patterns
- Boss design
#### Shooter
Projectile combat with aiming mechanics.
**Examples:** Doom, Call of Duty, Splatoon
**GDD sections:**
- Weapon systems
- Aiming and accuracy
- Enemy AI patterns
- Level/arena design
- Multiplayer considerations
#### Fighting
1v1 combat with combos and frame data.
**Examples:** Street Fighter, Tekken, Super Smash Bros.
**GDD sections:**
- Frame data systems
- Combo mechanics
- Character movesets
- Competitive balance
- Netcode requirements
### Strategy & Tactics
#### Strategy
Resource management with tactical decisions.
**Examples:** StarCraft, Civilization, Europa Universalis
**GDD sections:**
- Resource systems
- Unit/building design
- AI opponent behavior
- Map/scenario design
- Victory conditions
#### Turn-Based Tactics
Grid-based movement with turn order.
**Examples:** XCOM, Fire Emblem, Into the Breach
**GDD sections:**
- Grid and movement systems
- Turn order mechanics
- Cover and positioning
- Unit progression
- Procedural mission generation
#### Tower Defense
Wave-based defense with tower placement.
**Examples:** Bloons TD, Kingdom Rush, Plants vs. Zombies
**GDD sections:**
- Tower types and upgrades
- Wave design and pacing
- Economy systems
- Map design patterns
- Meta-progression
### RPG & Progression
#### RPG
Character progression with stats, inventory, and quests.
**Examples:** Final Fantasy, The Witcher, Baldur's Gate
**GDD sections:**
- Character stats and leveling
- Inventory and equipment
- Quest system design
- Combat system (action/turn-based)
- Skill trees and builds
#### Roguelike
Procedural generation with permadeath and run-based progression.
**Examples:** Hades, Dead Cells, Spelunky
**GDD sections:**
- Procedural generation rules
- Permadeath and persistence
- Run structure and pacing
- Item/ability synergies
- Meta-progression systems
#### Metroidvania
Interconnected world with ability gating.
**Examples:** Metroid, Castlevania: Symphony of the Night, Ori
**GDD sections:**
- World map connectivity
- Ability gating design
- Backtracking flow
- Secret and collectible placement
- Power-up progression
### Narrative & Story
#### Adventure
Story-driven exploration with puzzle elements.
**Examples:** Monkey Island, Myst, Life is Strange
**GDD sections:**
- Puzzle design
- Narrative delivery
- Exploration mechanics
- Dialogue systems
- Story branching
#### Visual Novel
Narrative choices with branching story.
**Examples:** Doki Doki Literature Club, Phoenix Wright, Steins;Gate
**GDD sections:**
- Branching narrative structure
- Choice and consequence
- Character routes
- UI/presentation
- Save/load states
#### Text-Based
Text input/output games with parser or choice mechanics.
**Examples:** Zork, 80 Days, Dwarf Fortress (adventure mode)
**GDD sections:**
- Parser or choice systems
- World model
- Narrative structure
- Text presentation
- Save state management
### Simulation & Management
#### Simulation
Realistic systems with management and building.
**Examples:** SimCity, RollerCoaster Tycoon, The Sims
**GDD sections:**
- Core simulation loops
- Economy modeling
- AI agents/citizens
- Building/construction
- Failure states
#### Sandbox
Creative freedom with building and minimal objectives.
**Examples:** Minecraft, Terraria, Garry's Mod
**GDD sections:**
- Creation tools
- Physics/interaction systems
- Persistence and saving
- Sharing/community features
- Optional objectives
### Sports & Racing
#### Racing
Vehicle control with tracks and lap times.
**Examples:** Mario Kart, Forza, Need for Speed
**GDD sections:**
- Vehicle physics model
- Track design
- AI opponents
- Progression/career mode
- Multiplayer racing
#### Sports
Team-based or individual sports simulation.
**Examples:** FIFA, NBA 2K, Tony Hawk's Pro Skater
**GDD sections:**
- Sport-specific rules
- Player/team management
- AI opponent behavior
- Season/career modes
- Multiplayer modes
### Multiplayer
#### MOBA
Multiplayer team battles with hero selection.
**Examples:** League of Legends, Dota 2, Smite
**GDD sections:**
- Hero/champion design
- Lane and map design
- Team composition
- Matchmaking
- Economy (gold/items)
#### Party Game
Local multiplayer with minigames.
**Examples:** Mario Party, Jackbox, Overcooked
**GDD sections:**
- Minigame design patterns
- Controller support
- Round/game structure
- Scoring systems
- Player count flexibility
### Horror & Survival
#### Survival
Resource gathering with crafting and persistent threats.
**Examples:** Don't Starve, Subnautica, The Forest
**GDD sections:**
- Resource gathering
- Crafting systems
- Hunger/health/needs
- Threat systems
- Base building
#### Horror
Atmosphere and tension with limited resources.
**Examples:** Resident Evil, Silent Hill, Amnesia
**GDD sections:**
- Fear mechanics
- Resource scarcity
- Sound design
- Lighting and visibility
- Enemy/threat design
### Casual & Progression
#### Puzzle
Logic-based challenges and problem-solving.
**Examples:** Tetris, Portal, The Witness
**GDD sections:**
- Puzzle mechanics
- Difficulty progression
- Hint systems
- Level structure
- Scoring/rating
#### Idle/Incremental
Passive progression with upgrades and automation.
**Examples:** Cookie Clicker, Adventure Capitalist, Clicker Heroes
**GDD sections:**
- Core loop design
- Prestige systems
- Automation unlocks
- Number scaling
- Offline progress
#### Card Game
Deck building with card mechanics.
**Examples:** Slay the Spire, Hearthstone, Magic: The Gathering Arena
**GDD sections:**
- Card design framework
- Deck building rules
- Mana/resource systems
- Rarity and collection
- Competitive balance
### Rhythm
#### Rhythm
Music synchronization with timing-based gameplay.
**Examples:** Guitar Hero, Beat Saber, Crypt of the NecroDancer
**GDD sections:**
- Note/beat mapping
- Scoring systems
- Difficulty levels
- Music licensing
- Input methods
## Hybrid Types
Multiple game types can be combined. GDD sections from all selected types are included.
| Hybrid | Components | Combined Sections |
|--------|------------|-------------------|
| Action RPG | Action Platformer + RPG | Movement, combat, stats, inventory |
| Survival Horror | Survival + Horror | Resources, crafting, atmosphere, fear |
| Roguelike Deckbuilder | Roguelike + Card Game | Run structure, procedural gen, cards |
| Tactical RPG | Turn-Based Tactics + RPG | Grid movement, stats, progression |
| Open World Survival | Sandbox + Survival | Building, crafting, exploration |

113
docs/bmgd/index.md Normal file
View File

@ -0,0 +1,113 @@
---
title: "BMGD Quick Guide"
description: Quick reference for BMad Game Dev Studio
draft: true
---
![BMGD Logo](bmgd-logo.png)
# BMGD Quick Guide
BMad Game Dev Studio (BMGD) extends BMM with game-specific capabilities. Developed by game industry veterans, it guides you through product research, technical design, narrative design, and a full epic-driven production cycle.
## Under Construction
Documentation is under heavy construction catching up with the new beta release. We'll have complete documentation up as soon as possible. For now, please ask in the BMGD section of the Discord if you have any questions.
![BMGD Workflow](workflow.jpg)
## Quick Start
**Install → Game Brief → GDD → (Narrative) → Architecture → Build**
BMGD is an optional module installed via BMAD Method: `npx bmad-method install`
See [How-To Reference](#how-to-reference) for commands.
## Development Phases
| Phase | Name | Key Activities |
|-------|------|----------------|
| 1 | **Preproduction** | Brainstorm Game, Game Brief, market research |
| 2 | **Design** | GDD creation, Narrative Design (for story-driven games) |
| 3 | **Technical** | Game Architecture (engine, systems, patterns) |
| 4 | **Production** | Sprint planning, story development, code review, testing |
## BMGD Agents
| Agent | Purpose |
|-------|---------|
| Game Designer | Game mechanics, balance, player psychology |
| Game Developer | Implementation with engine-specific patterns |
| Game Architect | Engine selection, systems design, technical structure |
| Game Scrum Master | Sprint planning and epic management |
| Game QA | Playtesting, engine-specific testing, performance profiling |
| Game Solo Dev | Full-stack game development for solo projects |
## Key Documents
| Document | Purpose |
|----------|---------|
| **Game Brief** | Vision, market positioning, fundamentals |
| **GDD** | Core loop, mechanics, progression, art/audio direction |
| **Narrative Design** | Story structure, characters, world-building, dialogue |
| **Architecture** | Engine, systems, patterns, project structure |
## Game Type Templates
BMGD includes 24 game type templates that auto-configure GDD sections:
Action, Adventure, Puzzle, RPG, Strategy, Simulation, Sports, Racing, Fighting, Horror, Platformer, Shooter, and more.
Each template provides genre-specific GDD sections, mechanics patterns, testing considerations, and common pitfalls to avoid.
## Explanation: BMGD vs BMM
### When to Use Each
| Use BMGD for | Use BMM for |
|--------------|-------------|
| Video games | Web applications |
| Interactive experiences | APIs and services |
| Game prototyping | Mobile apps (non-game) |
| Game jams | General software projects |
### Phase Mapping
| BMM Phase | BMGD Phase | Key Difference |
|-----------|------------|----------------|
| Analysis | Preproduction | Game concepts, Game Brief instead of Product Brief |
| Planning | Design | GDD instead of PRD; optional Narrative Design |
| Solutioning | Technical | Focus on engine selection, game-specific patterns |
| Implementation | Production | Game QA replaces TEA; engine-specific testing |
### Document Differences
| BMM | BMGD | Notes |
|-----|------|-------|
| Product Brief | Game Brief | Captures vision, market, fundamentals |
| PRD | GDD | Includes mechanics, balance, player experience |
| N/A | Narrative Design | Story, characters, world (story-driven games) |
| Architecture | Architecture | BMGD version includes engine-specific patterns and considerations |
### Testing Differences
**BMM (TEA):** Web-focused testing with Playwright, Cypress, API testing, E2E for web apps.
**BMGD (Game QA):** Engine-specific frameworks (Unity, Unreal, Godot), gameplay testing, performance profiling, playtest planning, balance validation.
## How-To Reference
| I need to... | Action |
|--------------|--------------------------------------------------------------------------------------------------------|
| Install BMGD | Run `npx bmad-method install` and select BMGD during module installation |
| Start a new game | Run `/bmad-gds-brainstorm-game`, then `/bmad:gds:create-game-brief` |
| Design my game | Run `/bmad-gds-create-gdd`; add `/bmad:gds:narrative` if story-heavy |
| Plan architecture | Run `/bmad-gds-game-architecture` with Game Architect |
| Build my game | Use Phase 4 production workflows - Run `/bmad-help` to see what's next |
| Test an idea quickly | Use [Quick-Flow](quick-flow-workflows.md) for rapid prototyping |
## Further Reading
- [Game Types Guide](game-types.md)
- [Quick-Flow Guide](quick-flow-workflows.md)

View File

@ -0,0 +1,162 @@
---
title: "Quick Flow Workflows"
description: Create tech specs and execute implementations with BMGD Quick Flow
draft: true
---
How to create tech specs and execute implementations with Quick Flow.
## Choosing a Workflow
| Situation | Workflow | Command |
|-----------|----------|---------|
| Need to document before implementing | Quick-Spec | `/bmad-gds-quick-spec` |
| Multiple approaches to evaluate | Quick-Spec | `/bmad-gds-quick-spec` |
| Have a completed tech-spec | Quick-Dev | `/bmad-gds-quick-dev path/to/spec.md` |
| Have clear, direct instructions | Quick-Dev | `/bmad-gds-quick-dev` |
| Building complete game system | Full GDS | `/bmad-gds-workflow-init` |
| Epic-level features | Full GDS | `/bmad-gds-workflow-init` |
---
## How to Create a Tech Spec (Quick-Spec)
### Step 1: Start the workflow
```bash
/bmad-gds-quick-spec
```
### Step 2: Describe your requirement
Provide your feature request. The agent scans the codebase and asks clarifying questions.
**Checkpoint options:**
- `[a]` Advanced Elicitation - explore requirements deeper
- `[c]` Continue to investigation
- `[p]` Party Mode - consult expert agents
### Step 3: Review investigation findings
The agent analyzes the codebase for patterns, constraints, and similar implementations. Review the findings.
**Checkpoint options:**
- `[c]` Continue to spec generation
- `[p]` Party Mode - get technical review
### Step 4: Review generated spec
The agent creates an ordered task list with file paths and acceptance criteria. Verify completeness.
**Checkpoint options:**
- `[c]` Continue to final review
- `[p]` Party Mode - technical review
### Step 5: Finalize
Confirm the spec meets these standards:
- Every task has a file path and specific action
- Tasks ordered by dependency
- Acceptance criteria in Given/When/Then format
- No placeholders or TBD sections
**Options:**
- `[d]` Start Quick-Dev immediately
- `[done]` Save spec and exit
**Output:** `{planning_artifacts}/tech-spec-{slug}.md`
---
## How to Execute Implementation (Quick-Dev)
### With a Tech-Spec
```bash
/bmad-gds-quick-dev path/to/tech-spec-feature.md
```
The agent:
1. Captures baseline git commit
2. Loads and validates the spec
3. Executes tasks in order
4. Runs self-check
5. Performs adversarial review
6. Resolves findings
7. Validates against acceptance criteria
### With Direct Instructions
```bash
/bmad-gds-quick-dev
```
Then describe what you want implemented:
1. Captures baseline git commit
2. Evaluates complexity (may suggest planning)
3. Gathers context from codebase
4. Executes implementation
5. Runs self-check and adversarial review
6. Resolves findings
**Escalation:** If the agent detects complexity (multiple components, system-level scope, uncertainty), it offers:
- `[t]` Create tech-spec first
- `[w]` Use full GDS workflow
- `[e]` Execute anyway
---
## Troubleshooting
### Spec has placeholders or TBD sections
Return to investigation step. Complete missing research, inline all findings, re-run review.
### Workflow lost context mid-step
Check frontmatter for `stepsCompleted`. Resume from last completed step.
### Agent suggested planning but you want to execute
You can override with `[e]`, but document your assumptions. Escalation heuristics exist because planning saves time on complex tasks.
### Tests failing after implementation
Return to the resolve-findings step. Review failures, fix issues, ensure test expectations are correct, re-run full suite.
### Need help
```bash
/bmad-help
```
---
## Reference
### File Locations
| File | Location |
|------|----------|
| Work in progress | `{implementation_artifacts}/tech-spec-wip.md` |
| Completed specs | `{planning_artifacts}/tech-spec-{slug}.md` |
| Archived specs | `{implementation_artifacts}/tech-spec-{slug}-archived-{date}.md` |
| Workflow files | `_bmad/gds/workflows/gds-quick-flow/` |
### Validation Criteria
**Self-check (before adversarial review):**
- All tasks/instructions completed
- Tests written and passing
- Follows existing patterns
- No obvious bugs
- Acceptance criteria met
- Code is readable
**Adversarial review:**
- Correctness
- Security
- Performance
- Maintainability
- Test coverage
- Error handling

BIN
docs/bmgd/workflow.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@ -1,17 +1,11 @@
--- ---
title: "Advanced Elicitation" title: "Advanced Elicitation"
description: Push the LLM to rethink its work using structured reasoning methods description: Push the LLM to rethink its work using structured reasoning methods
sidebar:
order: 6
--- ---
Make the LLM reconsider what it just generated. You pick a reasoning method, it applies that method to its own output, you decide whether to keep the improvements. Make the LLM reconsider what it just generated. You pick a reasoning method, it applies that method to its own output, you decide whether to keep the improvements.
## What is Advanced Elicitation? Dozens of methods are built in - things like First Principles, Red Team vs Blue Team, Pre-mortem Analysis, Socratic Questioning, and more.
A structured second pass. Instead of asking the AI to "try again" or "make it better," you select a specific reasoning method and the AI re-examines its own output through that lens.
The difference matters. Vague requests produce vague revisions. A named method forces a particular angle of attack, surfacing insights that a generic retry would miss.
## When to Use It ## When to Use It
@ -28,22 +22,3 @@ Workflows offer advanced elicitation at decision points - after the LLM has gene
2. You pick one (or reshuffle for different options) 2. You pick one (or reshuffle for different options)
3. Method is applied, improvements shown 3. Method is applied, improvements shown
4. Accept or discard, repeat or continue 4. Accept or discard, repeat or continue
## Built-in Methods
Dozens of reasoning methods are available. A few examples:
- **Pre-mortem Analysis** - Assume the project already failed, work backward to find why
- **First Principles Thinking** - Strip away assumptions, rebuild from ground truth
- **Inversion** - Ask how to guarantee failure, then avoid those things
- **Red Team vs Blue Team** - Attack your own work, then defend it
- **Socratic Questioning** - Challenge every claim with "why?" and "how do you know?"
- **Constraint Removal** - Drop all constraints, see what changes, add them back selectively
- **Stakeholder Mapping** - Re-evaluate from each stakeholder's perspective
- **Analogical Reasoning** - Find parallels in other domains and apply their lessons
And many more. The AI picks the most relevant options for your content - you choose which to run.
:::tip[Start Here]
Pre-mortem Analysis is a good first pick for any spec or plan. It consistently finds gaps that a standard review misses.
:::

View File

@ -1,8 +1,6 @@
--- ---
title: "Adversarial Review" title: "Adversarial Review"
description: Forced reasoning technique that prevents lazy "looks good" reviews description: Forced reasoning technique that prevents lazy "looks good" reviews
sidebar:
order: 5
--- ---
Force deeper analysis by requiring problems to be found. Force deeper analysis by requiring problems to be found.
@ -26,7 +24,7 @@ Normal reviews suffer from confirmation bias. You skim the work, nothing jumps o
## Where It's Used ## Where It's Used
Adversarial review appears throughout BMad workflows - code review, implementation readiness checks, spec validation, and others. Sometimes it's a required step, sometimes optional (like advanced elicitation or party mode). The pattern adapts to whatever artifact needs scrutiny. Adversarial review appears throughout BMAD workflows - code review, implementation readiness checks, spec validation, and others. Sometimes it's a required step, sometimes optional (like advanced elicitation or party mode). The pattern adapts to whatever artifact needs scrutiny.
## Human Filtering Required ## Human Filtering Required

View File

@ -1,8 +1,6 @@
--- ---
title: "Brainstorming" title: "Brainstorming"
description: Interactive creative sessions using 60+ proven ideation techniques description: Interactive creative sessions using 60+ proven ideation techniques
sidebar:
order: 2
--- ---
Unlock your creativity through guided exploration. Unlock your creativity through guided exploration.

View File

@ -1,8 +1,6 @@
--- ---
title: "Established Projects FAQ" title: "Established Projects FAQ"
description: Common questions about using BMad Method on established projects description: Common questions about using BMad Method on established projects
sidebar:
order: 8
--- ---
Quick answers to common questions about working on established projects with the BMad Method (BMM). Quick answers to common questions about working on established projects with the BMad Method (BMM).

View File

@ -1,8 +1,6 @@
--- ---
title: "Party Mode" title: "Party Mode"
description: Multi-agent collaboration - get all your AI agents in one conversation description: Multi-agent collaboration - get all your AI agents in one conversation
sidebar:
order: 7
--- ---
Get all your AI agents in one conversation. Get all your AI agents in one conversation.

View File

@ -1,8 +1,6 @@
--- ---
title: "Preventing Agent Conflicts" title: "Preventing Agent Conflicts"
description: How architecture prevents conflicts when multiple agents implement a system description: How architecture prevents conflicts when multiple agents implement a system
sidebar:
order: 4
--- ---
When multiple AI agents implement different parts of a system, they can make conflicting technical decisions. Architecture documentation prevents this by establishing shared standards. When multiple AI agents implement different parts of a system, they can make conflicting technical decisions. Architecture documentation prevents this by establishing shared standards.
@ -71,7 +69,7 @@ Explicit documentation of:
Think of architecture as the shared context that all agents read before implementing: Think of architecture as the shared context that all agents read before implementing:
```text ```
PRD: "What to build" PRD: "What to build"
Architecture: "How to build it" Architecture: "How to build it"

View File

@ -1,73 +1,27 @@
--- ---
title: "Quick Flow" title: "Quick Flow"
description: Fast-track for small changes - skip the full methodology description: Fast-track for small changes - skip the full methodology
sidebar:
order: 1
--- ---
Skip the ceremony. Quick Flow takes you from idea to working code in two commands - no Product Brief, no PRD, no Architecture doc. Quick Flow is for when you don't need the full BMad Method. Skip Product Brief, PRD, and Architecture - go straight to implementation.
## When to Use It
- Bug fixes and patches
- Refactoring existing code
- Small, well-understood features
- Prototyping and spikes
- Single-agent work where one developer can hold the full scope
## When NOT to Use It
- New products or platforms that need stakeholder alignment
- Major features spanning multiple components or teams
- Work that requires architectural decisions (database schema, API contracts, service boundaries)
- Anything where requirements are unclear or contested
:::caution[Scope Creep]
If you start a Quick Flow and realize the scope is bigger than expected, `quick-dev` will detect this and offer to escalate. You can switch to a full PRD workflow at any point without losing your work.
:::
## How It Works ## How It Works
Quick Flow has two commands, each backed by a structured workflow. You can run them together or independently. 1. **Run `quick-spec`** — generates a focused tech-spec
2. **Run `quick-dev`** — implements it
### quick-spec: Plan That's it.
Run `quick-spec` and Barry (the Quick Flow agent) walks you through a conversational discovery process: ## When to Use It
1. **Understand** - You describe what you want to build. Barry scans the codebase to ask informed questions, then captures a problem statement, solution approach, and scope boundaries. - Bug fixes
2. **Investigate** - Barry reads relevant files, maps code patterns, identifies files to modify, and documents the technical context. - Refactoring
3. **Generate** - Produces a complete tech-spec with ordered implementation tasks (specific file paths and actions), acceptance criteria in Given/When/Then format, testing strategy, and dependencies. - Small features
4. **Review** - Presents the full spec for your sign-off. You can edit, ask questions, run adversarial review, or refine with advanced elicitation before finalizing. - Prototyping
The output is a `tech-spec-{slug}.md` file saved to your project's implementation artifacts folder. It contains everything a fresh agent needs to implement the feature - no conversation history required. ## When to Use Full BMad Method Instead
### quick-dev: Build - New products
- Major features
Run `quick-dev` and Barry implements the work. It operates in two modes: - Multiple teams involved
- Stakeholder alignment needed
- **Tech-spec mode** - Point it at a spec file (`quick-dev tech-spec-auth.md`) and it executes every task in order, writes tests, and verifies acceptance criteria.
- **Direct mode** - Give it instructions directly (`quick-dev "refactor the auth middleware"`) and it gathers context, builds a mental plan, and executes.
After implementation, `quick-dev` runs a self-check audit against all tasks and acceptance criteria, then triggers an adversarial code review of the diff. Findings are presented for you to resolve before wrapping up.
:::tip[Fresh Context]
For best results, run `quick-dev` in a new conversation after finishing `quick-spec`. This gives the implementation agent clean context focused solely on building.
:::
## What Quick Flow Skips
The full BMad Method produces a Product Brief, PRD, Architecture doc, and Epic/Story breakdown before any code is written. Quick Flow replaces all of that with a single tech-spec. This works because Quick Flow targets changes where:
- The product direction is already established
- Architecture decisions are already made
- A single developer can reason about the full scope
- Requirements fit in one conversation
## Escalating to Full BMad Method
Quick Flow includes built-in guardrails for scope detection. When you run `quick-dev` with a direct request, it evaluates signals like multi-component mentions, system-level language, and uncertainty about approach. If it detects the work is bigger than a quick flow:
- **Light escalation** - Recommends running `quick-spec` first to create a plan
- **Heavy escalation** - Recommends switching to the full BMad Method PRD process
You can also escalate manually at any time. Your tech-spec work carries forward - it becomes input for the broader planning process rather than being discarded.

View File

@ -1,8 +1,6 @@
--- ---
title: "Why Solutioning Matters" title: "Why Solutioning Matters"
description: Understanding why the solutioning phase is critical for multi-epic projects description: Understanding why the solutioning phase is critical for multi-epic projects
sidebar:
order: 3
--- ---
@ -10,7 +8,7 @@ Phase 3 (Solutioning) translates **what** to build (from Planning) into **how**
## The Problem Without Solutioning ## The Problem Without Solutioning
```text ```
Agent 1 implements Epic 1 using REST API Agent 1 implements Epic 1 using REST API
Agent 2 implements Epic 2 using GraphQL Agent 2 implements Epic 2 using GraphQL
Result: Inconsistent API design, integration nightmare Result: Inconsistent API design, integration nightmare
@ -20,7 +18,7 @@ When multiple agents implement different parts of a system without shared archit
## The Solution With Solutioning ## The Solution With Solutioning
```text ```
architecture workflow decides: "Use GraphQL for all APIs" architecture workflow decides: "Use GraphQL for all APIs"
All agents follow architecture decisions All agents follow architecture decisions
Result: Consistent implementation, no conflicts Result: Consistent implementation, no conflicts

View File

@ -1,35 +1,34 @@
--- ---
title: "How to Customize BMad" title: "BMad Method Customization Guide"
description: Customize agents, workflows, and modules while preserving update compatibility description: Customize agents, workflows, and modules while preserving update compatibility
sidebar:
order: 7
--- ---
Use the `.customize.yaml` files to tailor agent behavior, personas, and menus while preserving your changes across updates. The ability to customize the BMad Method and its core to your needs, while still being able to get updates and enhancements is a critical idea within the BMad Ecosystem.
## When to Use This The Customization Guidance outlined here, while targeted at understanding BMad Method customization, applies to any other module use within the BMad Method.
- You want to change an agent's name, personality, or communication style ## Types of Customization
- You need agents to remember project-specific context
- You want to add custom menu items that trigger your own workflows or prompts
- You want agents to perform specific actions every time they start up
:::note[Prerequisites] Customization includes Agent Customization, Workflow/Skill customization, the addition of new MCPs or Skills to be used by existing agents. Aside from all of this, a whole other realm of customization involves creating / adding your own relevant BMad Builder workflows, skills, agents and maybe even your own net new modules to compliment the BMad Method Module.
- BMad installed in your project (see [How to Install BMad](./install-bmad.md))
- A text editor for YAML files
:::
:::caution[Keep Your Customizations Safe] Warning: The reason for customizing as this guide will prescribe will allow you to continue getting updates without worrying about losing your customization changes. And by continuing to get updates as BMad modules advance, you will be able to continue to evolve as the system improves.
Always use the `.customize.yaml` files described here rather than editing agent files directly. The installer overwrites agent files during updates, but preserves your `.customize.yaml` changes.
:::
## Steps ## Agent Customization
### 1. Locate Customization Files ### Agent Customization Areas
After installation, find one `.customize.yaml` file per agent in: - Change agent names, personas or manner of speech
- Add project-specific memories or context
- Add custom menu items to custom or inline prompts, skills or custom BMad workflows
- Define critical actions that occur agent startup for consistent behavior
```text ## How to customize an agent.
**1. Locate Customization Files**
After installation, find agent customization files in:
```
_bmad/_config/agents/ _bmad/_config/agents/
├── core-bmad-master.customize.yaml ├── core-bmad-master.customize.yaml
├── bmm-dev.customize.yaml ├── bmm-dev.customize.yaml
@ -37,22 +36,28 @@ _bmad/_config/agents/
└── ... (one file per installed agent) └── ... (one file per installed agent)
``` ```
### 2. Edit the Customization File **2. Edit Any Agent**
Open the `.customize.yaml` file for the agent you want to modify. Every section is optional -- customize only what you need. Open the `.customize.yaml` file for the agent you want to modify. All sections are optional - customize only what you need.
| Section | Behavior | Purpose | **3. Rebuild the Agent**
| ------------------- | ------------ | ---------------------------------------------- |
| `agent.metadata` | Replaces | Override the agent's display name |
| `persona` | Replaces | Set role, identity, style, and principles |
| `memories` | Appends | Add persistent context the agent always recalls |
| `menu` | Appends | Add custom menu items for workflows or prompts |
| `critical_actions` | Appends | Define startup instructions for the agent |
| `prompts` | Appends | Create reusable prompts for menu actions |
Sections marked **Replaces** overwrite the agent's defaults entirely. Sections marked **Appends** add to the existing configuration. After editing, IT IS CRITICAL to rebuild the agent to apply changes:
**Agent Name** ```bash
npx bmad-method install
```
You can either then:
- Select `Quick Update` - This will also ensure all packages are up to date AND compile all agents to include any updates or customizations
- Select `Rebuild Agents` - This will only rebuild and apply customizations to agents, without pulling the latest
There will be additional tools shortly after beta launch to allow install of individual agents, workflows, skills and modules without the need for using the full bmad installer.
### What Agent Properties Can Be Customized?
#### Agent Name
Change how the agent introduces itself: Change how the agent introduces itself:
@ -62,7 +67,7 @@ agent:
name: 'Spongebob' # Default: "Amelia" name: 'Spongebob' # Default: "Amelia"
``` ```
**Persona** #### Persona
Replace the agent's personality, role, and communication style: Replace the agent's personality, role, and communication style:
@ -76,9 +81,9 @@ persona:
- 'Favor composition over inheritance' - 'Favor composition over inheritance'
``` ```
The `persona` section replaces the entire default persona, so include all four fields if you set it. **Note:** The persona section replaces the entire default persona (not merged).
**Memories** #### Memories
Add persistent context the agent will always remember: Add persistent context the agent will always remember:
@ -86,12 +91,12 @@ Add persistent context the agent will always remember:
memories: memories:
- 'Works at Krusty Krab' - 'Works at Krusty Krab'
- 'Favorite Celebrity: David Hasslehoff' - 'Favorite Celebrity: David Hasslehoff'
- 'Learned in Epic 1 that it is not cool to just pretend that tests have passed' - 'Learned in Epic 1 that its not cool to just pretend that tests have passed'
``` ```
**Menu Items** ### Custom Menu Items
Add custom entries to the agent's display menu. Each item needs a `trigger`, a target (`workflow` path or `action` reference), and a `description`: Any custom items you add here will be included in the agents display menu.
```yaml ```yaml
menu: menu:
@ -103,18 +108,18 @@ menu:
description: Deploy to production description: Deploy to production
``` ```
**Critical Actions** ### Critical Actions
Define instructions that run when the agent starts up: Add instructions that execute before the agent starts:
```yaml ```yaml
critical_actions: critical_actions:
- 'Check the CI Pipelines with the XYZ Skill and alert user on wake if anything is urgently needing attention' - 'Check the CI Pipelines with the XYZ Skill and alert user on wake if anything is urgently needing attention'
``` ```
**Custom Prompts** ### Custom Prompts
Create reusable prompts that menu items can reference with `action="#id"`: Define reusable prompts for `action="#id"` menu handlers:
```yaml ```yaml
prompts: prompts:
@ -126,47 +131,29 @@ prompts:
3. Execute deployment script 3. Execute deployment script
``` ```
### 3. Apply Your Changes
After editing, recompile the agent to apply changes:
```bash
npx bmad-method install
```
The installer detects the existing installation and offers these options:
| Option | What It Does |
| --------------------- | ------------------------------------------------------------------- |
| **Quick Update** | Updates all modules to the latest version and recompiles all agents |
| **Recompile Agents** | Applies customizations only, without updating module files |
| **Modify BMad Installation** | Full installation flow for adding or removing modules |
For customization-only changes, **Recompile Agents** is the fastest option.
## Troubleshooting ## Troubleshooting
**Changes not appearing?** **Changes not appearing?**
- Run `npx bmad-method install` and select **Recompile Agents** to apply changes - Make sure you ran `npx bmad-method build <agent-name>` after editing
- Check that your YAML syntax is valid (indentation matters) - Check YAML syntax is valid (indentation matters!)
- Verify you edited the correct `.customize.yaml` file for the agent - Verify the agent name matches the file name pattern
**Agent not loading?** **Agent not loading?**
- Check for YAML syntax errors using an online YAML validator - Check for YAML syntax errors
- Ensure you did not leave fields empty after uncommenting them - Ensure required fields aren't left empty if you uncommented them
- Try reverting to the original template and rebuilding - Try reverting to the template and rebuilding
**Need to reset an agent?** **Need to reset?**
- Clear or delete the agent's `.customize.yaml` file - Remove content from the `.customize.yaml` file (or delete the file)
- Run `npx bmad-method install` and select **Recompile Agents** to restore defaults - Run `npx bmad-method build <agent-name>` to regenerate defaults
## Workflow Customization ## Workflow Customization
Customization of existing BMad Method workflows and skills is coming soon. Information about customizing existing BMad Method workflows and skills are coming soon.
## Module Customization ## Module Customization
Guidance on building expansion modules and customizing existing modules is coming soon. Information on how to build expansion modules that augment BMad, or make other existing module customizations are coming soon.

View File

@ -1,8 +1,6 @@
--- ---
title: "Established Projects" title: "Established Projects"
description: How to use BMad Method on existing codebases description: How to use BMad Method on existing codebases
sidebar:
order: 6
--- ---
Use BMad Method effectively when working on existing projects and legacy codebases, sometimes also referred to as brownfield projects. Use BMad Method effectively when working on existing projects and legacy codebases, sometimes also referred to as brownfield projects.
@ -46,8 +44,8 @@ You have two primary options depending on the scope of changes:
| Scope | Recommended Approach | | Scope | Recommended Approach |
| ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- |
| **Small updates or additions** | Use `quick-flow-solo-dev` to create a tech-spec and implement the change. The full four-phase BMad Method is likely overkill. | | **Small updates or additions** | Use `quick-flow-solo-dev` to create a tech-spec and implement the change. The full four-phase BMad method is likely overkill. |
| **Major changes or additions** | Start with the BMad Method, applying as much or as little rigor as needed. | | **Major changes or additions** | Start with the BMad method, applying as much or as little rigor as needed. |
### During PRD Creation ### During PRD Creation
@ -78,5 +76,5 @@ Pay close attention here to prevent reinventing the wheel or making decisions th
## More Information ## More Information
- **[Quick Fixes](./quick-fixes.md)** - Bug fixes and ad-hoc changes - **[Quick Fixes](/docs/how-to/quick-fixes.md)** - Bug fixes and ad-hoc changes
- **[Established Projects FAQ](../explanation/established-projects-faq.md)** - Common questions about working on established projects - **[Established Projects FAQ](/docs/explanation/established-projects-faq.md)** - Common questions about working on established projects

View File

@ -1,8 +1,6 @@
--- ---
title: "How to Get Answers About BMad" title: "How to Get Answers About BMad"
description: Use an LLM to quickly answer your own BMad questions description: Use an LLM to quickly answer your own BMad questions
sidebar:
order: 4
--- ---
If you have successfully installed BMad and the BMad Method (+ other modules as needed) - the first step in getting answers is `/bmad-help`. This will answer upwards of 80% of all questions and is available to you in the IDE as you are working. If you have successfully installed BMad and the BMad Method (+ other modules as needed) - the first step in getting answers is `/bmad-help`. This will answer upwards of 80% of all questions and is available to you in the IDE as you are working.
@ -40,11 +38,10 @@ The `_bmad` folder is created when you install BMad. If you don't have it yet, c
Fetch `llms-full.txt` into your session: Fetch `llms-full.txt` into your session:
```text ```
https://bmad-code-org.github.io/BMAD-METHOD/llms-full.txt https://bmad-code-org.github.io/BMAD-METHOD/llms-full.txt
``` ```
### 3. Ask Your Question ### 3. Ask Your Question
:::note[Example] :::note[Example]

View File

@ -1,13 +1,11 @@
--- ---
title: "How to Install BMad" title: "How to Install BMad"
description: Step-by-step guide to installing BMad in your project description: Step-by-step guide to installing BMad in your project
sidebar:
order: 1
--- ---
Use the `npx bmad-method install` command to set up BMad in your project with your choice of modules and AI tools. Use the `npx bmad-method install` command to set up BMad in your project with your choice of modules and AI tools.
If you want to use a non interactive installer and provide all install options on the command line, see [this guide](./non-interactive-installation.md). If you want to use a non interactive installer and provide all install options on the command line, [this guide](/docs/non-interactive-installation.md).
## When to Use This ## When to Use This
@ -29,13 +27,6 @@ If you want to use a non interactive installer and provide all install options o
npx bmad-method install npx bmad-method install
``` ```
:::tip[Bleeding edge]
To install the latest from the main branch (may be unstable):
```bash
npx github:bmad-code-org/BMAD-METHOD install
```
:::
### 2. Choose Installation Location ### 2. Choose Installation Location
The installer will ask where to install BMad files: The installer will ask where to install BMad files:
@ -50,7 +41,6 @@ Pick which AI tools you use:
- Claude Code - Claude Code
- Cursor - Cursor
- Windsurf - Windsurf
- Kiro
- Others - Others
Each tool has its own way of integrating commands. The installer creates tiny prompt files to activate workflows and agents — it just puts them where your tool expects to find them. Each tool has its own way of integrating commands. The installer creates tiny prompt files to activate workflows and agents — it just puts them where your tool expects to find them.
@ -65,7 +55,7 @@ The installer guides you through the rest — custom content, settings, etc.
## What You Get ## What You Get
```text ```
your-project/ your-project/
├── _bmad/ ├── _bmad/
│ ├── bmm/ # Your selected modules │ ├── bmm/ # Your selected modules
@ -73,16 +63,22 @@ your-project/
│ ├── core/ # Required core module │ ├── core/ # Required core module
│ └── ... │ └── ...
├── _bmad-output/ # Generated artifacts ├── _bmad-output/ # Generated artifacts
├── .claude/ # Claude Code commands (if using Claude Code) └── .claude/ # Claude Code commands (if using Claude Code)
└── .kiro/ # Kiro steering files (if using Kiro)
``` ```
## Verify Installation ## Verify Installation
Run the `help` workflow (`/bmad-help` on most platforms) to verify everything works and see what to do next. Run the `help` workflow (`/bmad-help` on most platforms) to verify everything works and see what to do next.
**Latest from main branch:**
```bash
npx github:bmad-code-org/BMAD-METHOD install
```
Use these if you want the newest features before they're officially released. Things might break.
## Troubleshooting ## Troubleshooting
**Installer throws an error** — Copy-paste the output into your AI assistant and let it figure it out. **Installer throws an error** — Copy-paste the output into your AI assistant and let it figure it out.
**Installer worked but something doesn't work later** — Your AI needs BMad context to help. See [How to Get Answers About BMad](./get-answers-about-bmad.md) for how to point your AI at the right sources. **Installer worked but something doesn't work later** — Your AI needs BMad context to help. See [How to Get Answers About BMad](/docs/how-to/get-answers-about-bmad.md) for how to point your AI at the right sources.

View File

@ -1,171 +0,0 @@
---
title: Non-Interactive Installation
description: Install BMad using command-line flags for CI/CD pipelines and automated deployments
sidebar:
order: 2
---
Use command-line flags to install BMad non-interactively. This is useful for:
## When to Use This
- Automated deployments and CI/CD pipelines
- Scripted installations
- Batch installations across multiple projects
- Quick installations with known configurations
:::note[Prerequisites]
Requires [Node.js](https://nodejs.org) v20+ and `npx` (included with npm).
:::
## Available Flags
### Installation Options
| Flag | Description | Example |
|------|-------------|---------|
| `--directory <path>` | Installation directory | `--directory ~/projects/myapp` |
| `--modules <modules>` | Comma-separated module IDs | `--modules bmm,bmb` |
| `--tools <tools>` | Comma-separated tool/IDE IDs (use `none` to skip) | `--tools claude-code,cursor` or `--tools none` |
| `--custom-content <paths>` | Comma-separated paths to custom modules | `--custom-content ~/my-module,~/another-module` |
| `--action <type>` | Action for existing installations: `install` (default), `update`, `quick-update`, or `compile-agents` | `--action quick-update` |
### Core Configuration
| Flag | Description | Default |
|------|-------------|---------|
| `--user-name <name>` | Name for agents to use | System username |
| `--communication-language <lang>` | Agent communication language | English |
| `--document-output-language <lang>` | Document output language | English |
| `--output-folder <path>` | Output folder path | _bmad-output |
### Other Options
| Flag | Description |
|------|-------------|
| `-y, --yes` | Accept all defaults and skip prompts |
| `-d, --debug` | Enable debug output for manifest generation |
## Module IDs
Available module IDs for the `--modules` flag:
- `bmm` — BMad Method Master
- `bmb` — BMad Builder
Check the [BMad registry](https://github.com/bmad-code-org) for available external modules.
## Tool/IDE IDs
Available tool IDs for the `--tools` flag:
**Preferred:** `claude-code`, `cursor`, `windsurf`
Run `npx bmad-method install` interactively once to see the full current list of supported tools, or check the [platform codes configuration](https://github.com/bmad-code-org/BMAD-METHOD/blob/main/tools/cli/installers/lib/ide/platform-codes.yaml).
## Installation Modes
| Mode | Description | Example |
|------|-------------|---------|
| Fully non-interactive | Provide all flags to skip all prompts | `npx bmad-method install --directory . --modules bmm --tools claude-code --yes` |
| Semi-interactive | Provide some flags; BMad prompts for the rest | `npx bmad-method install --directory . --modules bmm` |
| Defaults only | Accept all defaults with `-y` | `npx bmad-method install --yes` |
| Without tools | Skip tool/IDE configuration | `npx bmad-method install --modules bmm --tools none` |
## Examples
### CI/CD Pipeline Installation
```bash
#!/bin/bash
# install-bmad.sh
npx bmad-method install \
--directory "${GITHUB_WORKSPACE}" \
--modules bmm \
--tools claude-code \
--user-name "CI Bot" \
--communication-language English \
--document-output-language English \
--output-folder _bmad-output \
--yes
```
### Update Existing Installation
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--action update \
--modules bmm,bmb,custom-module
```
### Quick Update (Preserve Settings)
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--action quick-update
```
### Installation with Custom Content
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--custom-content ~/my-custom-module,~/another-module \
--tools claude-code
```
## What You Get
- A fully configured `_bmad/` directory in your project
- Compiled agents and workflows for your selected modules and tools
- A `_bmad-output/` folder for generated artifacts
## Validation and Error Handling
BMad validates all provided flags:
- **Directory** — Must be a valid path with write permissions
- **Modules** — Warns about invalid module IDs (but won't fail)
- **Tools** — Warns about invalid tool IDs (but won't fail)
- **Custom Content** — Each path must contain a valid `module.yaml` file
- **Action** — Must be one of: `install`, `update`, `quick-update`, `compile-agents`
Invalid values will either:
1. Show an error and exit (for critical options like directory)
2. Show a warning and skip (for optional items like custom content)
3. Fall back to interactive prompts (for missing required values)
:::tip[Best Practices]
- Use absolute paths for `--directory` to avoid ambiguity
- Test flags locally before using in CI/CD pipelines
- Combine with `-y` for truly unattended installations
- Use `--debug` if you encounter issues during installation
:::
## Troubleshooting
### Installation fails with "Invalid directory"
- The directory path must exist (or its parent must exist)
- You need write permissions
- The path must be absolute or correctly relative to the current directory
### Module not found
- Verify the module ID is correct
- External modules must be available in the registry
### Custom content path invalid
Ensure each custom content path:
- Points to a directory
- Contains a `module.yaml` file in the root
- Has a `code` field in the `module.yaml`
:::note[Still stuck?]
Run with `--debug` for detailed output, try interactive mode to isolate the issue, or report at <https://github.com/bmad-code-org/BMAD-METHOD/issues>.
:::

View File

@ -1,123 +1,76 @@
--- ---
title: "Quick Fixes" title: "Quick Fixes"
description: How to make quick fixes and ad-hoc changes description: How to make quick fixes and ad-hoc changes
sidebar:
order: 5
--- ---
Use the **DEV agent** directly for bug fixes, refactorings, or small targeted changes that don't require the full BMad Method or Quick Flow. Use the **DEV agent** directly for bug fixes, refactorings, or small targeted changes that don't require the full BMad method or Quick Flow.
## When to Use This ## When to Use This
- Bug fixes with a clear, known cause - Simple bug fixes
- Small refactorings (rename, extract, restructure) contained within a few files - Small refactorings and changes that don't need extensive ideation, planning, or architectural shifts
- Minor feature tweaks or configuration changes - Larger refactorings or improvement with built in tool planning and execution mode combination, or better yet use quick flow
- Exploratory work to understand an unfamiliar codebase - Learning about your codebase
:::note[Prerequisites]
- BMad Method installed (`npx bmad-method install`)
- An AI-powered IDE (Claude Code, Cursor, Windsurf, or similar)
:::
## Choose Your Approach
| Situation | Agent | Why |
| --- | --- | --- |
| Fix a specific bug or make a small, scoped change | **DEV agent** | Jumps straight into implementation without planning overhead |
| Change touches several files or you want a written plan first | **Quick Flow Solo Dev** | Creates a quick-spec before implementation so the agent stays aligned to your standards |
If you are unsure, start with the DEV agent. You can always escalate to Quick Flow if the change grows.
## Steps ## Steps
### 1. Load the DEV Agent ### 1. Load an Agent
Start a **fresh chat** in your AI IDE and load the DEV agent with its slash command: For quick fixes, you can use:
```text - **DEV agent** - For implementation-focused work
/bmad-agent-bmm-dev - **Quick Flow Solo Dev** - For slightly larger changes that still need a quick-spec to keep the agent aligned to planning and standards
```
This loads the agent's persona and capabilities into the session. If you decide you need Quick Flow instead, load the **Quick Flow Solo Dev** agent in a fresh chat:
```text
/bmad-agent-bmm-quick-flow-solo-dev
```
Once the Solo Dev agent is loaded, describe your change and ask it to create a **quick-spec**. The agent drafts a lightweight spec capturing what you want to change and how. After you approve the quick-spec, tell the agent to start the **Quick Flow dev cycle** -- it will implement the change, run tests, and perform a self-review, all guided by the spec you just approved.
:::tip[Fresh Chats]
Always start a new chat session when loading an agent. Reusing a session from a previous workflow can cause context conflicts.
:::
### 2. Describe the Change ### 2. Describe the Change
Tell the agent what you need in plain language. Be specific about the problem and, if you know it, where the relevant code lives. Simply tell the agent what you need:
:::note[Example Prompts] ```
**Bug fix** -- "Fix the login validation bug that allows empty passwords. The validation logic is in `src/auth/validate.ts`." Fix the login validation bug that allows empty passwords
```
**Refactoring** -- "Refactor the UserService to use async/await instead of callbacks." or
**Configuration change** -- "Update the CI pipeline to cache node_modules between runs." ```
Refactor the UserService to use async/await instead of callbacks
**Dependency update** -- "Upgrade the express dependency to the latest v5 release and fix any breaking changes." ```
:::
You don't need to provide every detail. The agent will read the relevant source files and ask clarifying questions when needed.
### 3. Let the Agent Work ### 3. Let the Agent Work
The agent will: The agent will:
- Read and analyze the relevant source files - Analyze the relevant code
- Propose a solution and explain its reasoning - Propose a solution
- Implement the change across the affected files - Implement the change
- Run your project's test suite if one exists - Run tests (if available)
If your project has tests, the agent runs them automatically after making changes and iterates until tests pass. For projects without a test suite, verify the change manually (run the app, hit the endpoint, check the output). ### 4. Review and Commit
### 4. Review and Verify Review the changes made and commit when satisfied.
Before committing, review what changed:
- Read through the diff to confirm the change matches your intent
- Run the application or tests yourself to double-check
- If something looks wrong, tell the agent what to fix -- it can iterate in the same session
Once satisfied, commit the changes with a clear message describing the fix.
:::caution[If Something Breaks]
If a committed change causes unexpected issues, use `git revert HEAD` to undo the last commit cleanly. Then start a fresh chat with the DEV agent to try a different approach.
:::
## Learning Your Codebase ## Learning Your Codebase
The DEV agent is also useful for exploring unfamiliar code. Load it in a fresh chat and ask questions: This approach is also excellent for exploring unfamiliar code:
:::note[Example Prompts] ```
"Explain how the authentication system works in this codebase." Explain how the authentication system works in this codebase
```
"Show me where error handling happens in the API layer." ```
Show me where error handling happens in the API layer
```
"What does the `ProcessOrder` function do and what calls it?" LLMs are excellent at interpreting and analyzing code, whether it was AI-generated or not. Use the agent to:
:::
Use the agent to learn about your project, understand how components connect, and explore unfamiliar areas before making changes. - Learn about your project
- Understand how things are built
## What You Get - Explore unfamiliar parts of the codebase
- Modified source files with the fix or refactoring applied
- Passing tests (if your project has a test suite)
- A clean commit describing the change
No planning artifacts are produced -- that's the point of this approach.
## When to Upgrade to Formal Planning ## When to Upgrade to Formal Planning
Consider using [Quick Flow](../explanation/quick-flow.md) or the full BMad Method when: Consider using Quick Flow or full BMad Method when:
- The change affects multiple systems or requires coordinated updates across many files - The change affects multiple files or systems
- You are unsure about the scope and need a spec to think it through - You're unsure about the scope
- The fix keeps growing in complexity as you work on it - The fix keeps growing in complexity
- You need documentation or architectural decisions recorded for the team - You need documentation for the change

View File

@ -1,19 +1,15 @@
--- ---
title: "Document Sharding Guide" title: "Document Sharding Guide"
description: Split large markdown files into smaller organized files for better context management description: Split large markdown files into smaller organized files for better context management
sidebar:
order: 8
--- ---
Use the `shard-doc` tool if you need to split large markdown files into smaller, organized files for better context management. Use the `shard-doc` tool if you need to split large markdown files into smaller, organized files for better context management.
:::caution[Deprecated] This is no longer recommended, and soon with updated workflows and most major llms and tools supporting sub processes this will be unnecessary.
This is no longer recommended, and soon with updated workflows and most major LLMs and tools supporting subprocesses this will be unnecessary.
:::
## When to Use This ## When to Use This
Only use this if you notice your chosen tool / model combination is failing to load and read all the documents as input when needed. Only use this if you notice your chosen tool / model combination are failing to load and read all the documents as input when needed.
## What is Document Sharding? ## What is Document Sharding?
@ -21,7 +17,7 @@ Document sharding splits large markdown files into smaller, organized files base
### Architecture ### Architecture
```text ```
Before Sharding: Before Sharding:
docs/ docs/
└── PRD.md (large 50k token file) └── PRD.md (large 50k token file)
@ -41,12 +37,12 @@ docs/
### 1. Run the Shard-Doc Tool ### 1. Run the Shard-Doc Tool
```bash ```bash
/bmad-shard-doc /bmad:core:tools:shard-doc
``` ```
### 2. Follow the Interactive Process ### 2. Follow the Interactive Process
```text ```
Agent: Which document would you like to shard? Agent: Which document would you like to shard?
User: docs/PRD.md User: docs/PRD.md

View File

@ -1,8 +1,6 @@
--- ---
title: "How to Upgrade to v6" title: "How to Upgrade to v6"
description: Migrate from BMad v4 to v6 description: Migrate from BMad v4 to v6
sidebar:
order: 3
--- ---
Use the BMad installer to upgrade from v4 to v6, which includes automatic detection of legacy installations and migration assistance. Use the BMad installer to upgrade from v4 to v6, which includes automatic detection of legacy installations and migration assistance.
@ -22,7 +20,7 @@ Use the BMad installer to upgrade from v4 to v6, which includes automatic detect
### 1. Run the Installer ### 1. Run the Installer
Follow the [Installer Instructions](./install-bmad.md). Follow the [Installer Instructions](/docs/how-to/install-bmad.md).
### 2. Handle Legacy Installation ### 2. Handle Legacy Installation
@ -31,7 +29,7 @@ When v4 is detected, you can:
- Allow the installer to back up and remove `.bmad-method` - Allow the installer to back up and remove `.bmad-method`
- Exit and handle cleanup manually - Exit and handle cleanup manually
If you named your bmad method folder something else - you will need to manually remove the folder yourself. If you named your bmad method folder something else - you will need to manual remove the folder yourself.
### 3. Clean Up IDE Commands ### 3. Clean Up IDE Commands
@ -65,16 +63,16 @@ If you have stories created or implemented:
**v6 unified structure:** **v6 unified structure:**
```text ```
your-project/ your-project/
── _bmad/ # Single installation folder ── _bmad/ # Single installation folder
├── _config/ # Your customizations ├── _config/ # Your customizations
│ └── agents/ # Agent customization files │ └── agents/ # Agent customization files
├── core/ # Universal core framework ├── core/ # Universal core framework
├── bmm/ # BMad Method module ├── bmm/ # BMad Method module
├── bmb/ # BMad Builder ├── bmb/ # BMad Builder
└── cis/ # Creative Intelligence Suite └── cis/ # Creative Intelligence Suite
── _bmad-output/ # Output folder (was doc folder in v4) ── _bmad-output/ # Output folder (was doc folder in v4)
``` ```
## Module Migration ## Module Migration

View File

@ -7,12 +7,14 @@ The BMad Method (**B**reakthrough **M**ethod of **A**gile AI **D**riven Developm
If you're comfortable working with AI coding assistants like Claude, Cursor, or GitHub Copilot, you're ready to get started. If you're comfortable working with AI coding assistants like Claude, Cursor, or GitHub Copilot, you're ready to get started.
---
## New Here? Start with a Tutorial ## New Here? Start with a Tutorial
The fastest way to understand BMad is to try it. The fastest way to understand BMad is to try it.
- **[Get Started with BMad](./tutorials/getting-started.md)** — Install and understand how BMad works - **[Get Started with BMad](/docs/tutorials/getting-started.md)** — Install and understand how BMad works
- **[Workflow Map](./reference/workflow-map.md)** — Visual overview of BMM phases, workflows, and context management. - **[Workflow Map](/docs/reference/workflow-map.md)** — Visual overview of BMM phases, workflows, and context management.
## How to Use These Docs ## How to Use These Docs
@ -25,6 +27,8 @@ These docs are organized into four sections based on what you're trying to do:
| **Explanation** | Understanding-oriented. Deep dives into concepts and architecture. Read when you want to know *why*. | | **Explanation** | Understanding-oriented. Deep dives into concepts and architecture. Read when you want to know *why*. |
| **Reference** | Information-oriented. Technical specifications for agents, workflows, and configuration. | | **Reference** | Information-oriented. Technical specifications for agents, workflows, and configuration. |
---
## What You'll Need ## What You'll Need
BMad works with any AI coding assistant that supports custom system prompts or project context. Popular options include: BMad works with any AI coding assistant that supports custom system prompts or project context. Popular options include:
@ -32,11 +36,12 @@ BMad works with any AI coding assistant that supports custom system prompts or p
- **[Claude Code](https://code.claude.com)** — Anthropic's CLI tool (recommended) - **[Claude Code](https://code.claude.com)** — Anthropic's CLI tool (recommended)
- **[Cursor](https://cursor.sh)** — AI-first code editor - **[Cursor](https://cursor.sh)** — AI-first code editor
- **[Windsurf](https://codeium.com/windsurf)** — Codeium's AI IDE - **[Windsurf](https://codeium.com/windsurf)** — Codeium's AI IDE
- **[Kiro](https://kiro.dev)** — Amazon's AI-powered IDE
- **[Roo Code](https://roocode.com)** — VS Code extension - **[Roo Code](https://roocode.com)** — VS Code extension
You should be comfortable with basic software development concepts like version control, project structure, and agile workflows. No prior experience with BMad-style agent systems is required—that's what these docs are for. You should be comfortable with basic software development concepts like version control, project structure, and agile workflows. No prior experience with BMad-style agent systems is required—that's what these docs are for.
---
## Join the Community ## Join the Community
Get help, share what you're building, or contribute to BMad: Get help, share what you're building, or contribute to BMad:
@ -45,6 +50,8 @@ Get help, share what you're building, or contribute to BMad:
- **[GitHub](https://github.com/bmad-code-org/BMAD-METHOD)** — Source code, issues, and contributions - **[GitHub](https://github.com/bmad-code-org/BMAD-METHOD)** — Source code, issues, and contributions
- **[YouTube](https://www.youtube.com/@BMadCode)** — Video tutorials and walkthroughs - **[YouTube](https://www.youtube.com/@BMadCode)** — Video tutorials and walkthroughs
---
## Next Step ## Next Step
Ready to dive in? **[Get Started with BMad](./tutorials/getting-started.md)** and build your first project. Ready to dive in? **[Get Started with BMad](/docs/tutorials/getting-started.md)** and build your first project.

View File

@ -0,0 +1,314 @@
---
title: Non-Interactive Installation
description: Install BMAD using command-line flags for CI/CD pipelines and automated deployments
---
# Non-Interactive Installation
BMAD now supports non-interactive installation through command-line flags. This is particularly useful for:
- Automated deployments and CI/CD pipelines
- Scripted installations
- Batch installations across multiple projects
- Quick installations with known configurations
## Installation Modes
### 1. Fully Interactive (Default)
Run without any flags to use the traditional interactive prompts:
```bash
npx bmad-method install
```
### 2. Fully Non-Interactive
Provide all required flags to skip all prompts:
```bash
npx bmad-method install \
--directory /path/to/project \
--modules bmm,bmb \
--tools claude-code,cursor \
--user-name "John Doe" \
--communication-language English \
--document-output-language English \
--output-folder _bmad-output
```
### 3. Semi-Interactive (Graceful Fallback)
Provide some flags and let BMAD prompt for the rest:
```bash
npx bmad-method install \
--directory /path/to/project \
--modules bmm
```
In this case, BMAD will:
- Use the provided directory and modules
- Prompt for tool selection
- Prompt for core configuration
### 4. Quick Install with Defaults
Use the `-y` or `--yes` flag to accept all defaults:
```bash
npx bmad-method install --yes
```
This will:
- Install to the current directory
- Skip custom content prompts
- Use default values for all configuration
- Use previously configured tools (or skip tool configuration if none exist)
### 5. Install Without Tools
To skip tool/IDE configuration entirely:
**Option 1: Use --tools none**
```bash
npx bmad-method install --directory ~/myapp --modules bmm --tools none
```
**Option 2: Use --yes flag (if no tools were previously configured)**
```bash
npx bmad-method install --yes
```
**Option 3: Omit --tools and select "None" in the interactive prompt**
```bash
npx bmad-method install --directory ~/myapp --modules bmm
# Then select "⚠ None - I am not installing any tools" when prompted
```
## Available Flags
### Installation Options
| Flag | Description | Example |
|------|-------------|---------|
| `--directory <path>` | Installation directory | `--directory ~/projects/myapp` |
| `--modules <modules>` | Comma-separated module IDs | `--modules bmm,bmb` |
| `--tools <tools>` | Comma-separated tool/IDE IDs (use "none" to skip) | `--tools claude-code,cursor` or `--tools none` |
| `--custom-content <paths>` | Comma-separated paths to custom modules | `--custom-content ~/my-module,~/another-module` |
| `--action <type>` | Action for existing installations | `--action quick-update` |
### Core Configuration
| Flag | Description | Default |
|------|-------------|---------|
| `--user-name <name>` | Name for agents to use | System username |
| `--communication-language <lang>` | Agent communication language | English |
| `--document-output-language <lang>` | Document output language | English |
| `--output-folder <path>` | Output folder path | _bmad-output |
### Other Options
| Flag | Description |
|------|-------------|
| `-y, --yes` | Accept all defaults and skip prompts |
| `-d, --debug` | Enable debug output for manifest generation |
## Action Types
When working with existing installations, use the `--action` flag:
- `install` - Fresh installation (default for new directories)
- `update` - Modify existing installation (change modules/config)
- `quick-update` - Refresh installation without changing configuration
- `compile-agents` - Recompile agents with customizations only
Example:
```bash
npx bmad-method install --action quick-update
```
## Module IDs
Available module IDs for the `--modules` flag:
### Core Modules
- `bmm` - BMad Method Master
- `bmb` - BMad Builder
### External Modules
Check the [BMad registry](https://github.com/bmad-code-org) for available external modules.
## Tool/IDE IDs
Available tool IDs for the `--tools` flag:
- `claude-code` - Claude Code CLI
- `cursor` - Cursor IDE
- `windsurf` - Windsurf IDE
- `vscode` - Visual Studio Code
- `jetbrains` - JetBrains IDEs
- And more...
Run the interactive installer once to see all available tools.
## Examples
### Basic Installation
Install BMM module with Claude Code:
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--tools claude-code \
--user-name "Development Team"
```
### Installation Without Tools
Install without configuring any tools/IDEs:
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--tools none \
--user-name "Development Team"
```
### Full Installation with Multiple Modules
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm,bmb \
--tools claude-code,cursor \
--user-name "John Doe" \
--communication-language English \
--document-output-language English \
--output-folder _output
```
### Update Existing Installation
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--action update \
--modules bmm,bmb,custom-module
```
### Quick Update (Preserve Settings)
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--action quick-update
```
### Installation with Custom Content
```bash
npx bmad-method install \
--directory ~/projects/myapp \
--modules bmm \
--custom-content ~/my-custom-module,~/another-module \
--tools claude-code
```
### CI/CD Pipeline Installation
```bash
#!/bin/bash
# install-bmad.sh
npx bmad-method install \
--directory "${GITHUB_WORKSPACE}" \
--modules bmm \
--tools claude-code \
--user-name "CI Bot" \
--communication-language English \
--document-output-language English \
--output-folder _bmad-output \
--yes
```
## Environment-Specific Installations
### Development Environment
```bash
npx bmad-method install \
--directory . \
--modules bmm,bmb \
--tools claude-code,cursor \
--user-name "${USER}"
```
### Production Environment
```bash
npx bmad-method install \
--directory /opt/app \
--modules bmm \
--tools claude-code \
--user-name "Production Team" \
--output-folder /var/bmad-output
```
## Validation and Error Handling
BMAD validates all provided flags:
- **Directory**: Must be a valid path with write permissions
- **Modules**: Will warn about invalid module IDs (but won't fail)
- **Tools**: Will warn about invalid tool IDs (but won't fail)
- **Custom Content**: Each path must contain a valid `module.yaml` file
- **Action**: Must be one of: install, update, quick-update, compile-agents
Invalid values will either:
1. Show an error and exit (for critical options like directory)
2. Show a warning and skip (for optional items like custom content)
3. Fall back to interactive prompts (for missing required values)
## Tips and Best Practices
1. **Use absolute paths** for `--directory` to avoid ambiguity
2. **Test flags locally** before using in CI/CD pipelines
3. **Combine with `-y`** for truly unattended installations
4. **Check module availability** by running the interactive installer once
5. **Use `--debug`** flag if you encounter issues during installation
6. **Skip tool configuration** with `--tools none` for server/CI environments where IDEs aren't needed
7. **Partial flags are OK** - Omit flags and let BMAD prompt for missing values interactively
## Troubleshooting
### Installation fails with "Invalid directory"
Check that:
- The directory path exists or its parent exists
- You have write permissions
- The path is absolute or correctly relative to current directory
### Module not found
- Verify the module ID is correct (check available modules in interactive mode)
- External modules may need to be available in the registry
### Custom content path invalid
Ensure each custom content path:
- Points to a directory
- Contains a `module.yaml` file in the root
- Has a `code` field in the `module.yaml`
## Feedback and Issues
If you encounter any issues with non-interactive installation:
1. Run with `--debug` flag for detailed output
2. Try the interactive mode to verify the issue
3. Report issues on GitHub: <https://github.com/bmad-code-org/BMAD-METHOD/issues>

View File

@ -1,18 +1,13 @@
--- ---
title: Agents title: Agents
description: Default BMM agents with their menu triggers and primary workflows description: Default BMM agents with their menu triggers and primary workflows
sidebar:
order: 2
--- ---
## Default Agents This page lists the default BMM (Agile suite) agents that install with BMAD Method, along with their menu triggers and primary workflows.
This page lists the default BMM (Agile suite) agents that install with BMad Method, along with their menu triggers and primary workflows.
## Notes
Notes:
- Triggers are the short menu codes (e.g., `CP`) and fuzzy matches shown in each agent menu. - Triggers are the short menu codes (e.g., `CP`) and fuzzy matches shown in each agent menu.
- Slash commands are generated separately. See [Commands](./commands.md) for the slash command list and where they are defined. - Slash commands are generated separately. See `docs/reference/commands.md` for the slash command list and where they are defined.
- QA (Quinn) is the lightweight test automation agent in BMM. The full Test Architect (TEA) lives in its own module. - QA (Quinn) is the lightweight test automation agent in BMM. The full Test Architect (TEA) lives in its own module.
| Agent | Triggers | Primary workflows | | Agent | Triggers | Primary workflows |

View File

@ -1,131 +1,34 @@
--- ---
title: Commands title: Commands
description: Reference for BMad slash commands — what they are, how they work, and where to find them. description: How BMAD commands are generated and where to find them.
sidebar:
order: 3
--- ---
Slash commands are pre-built prompts that load agents, run workflows, or execute tasks inside your IDE. The BMad installer generates them from your installed modules at install time. If you later add, remove, or change modules, re-run the installer to keep commands in sync (see [Troubleshooting](#troubleshooting)). # Commands
## Commands vs. Agent Menu Triggers BMAD slash commands are generated by the installer for your IDE and **reflect the modules you have installed**.
That means the authoritative list lives **in your project**, not in a static docs page.
BMad offers two ways to start work, and they serve different purposes. ## How to Discover Commands (Recommended)
| Mechanism | How you invoke it | What happens | - Type `/bmad` in your IDE and use autocomplete to browse agents/workflows.
| --- | --- | --- | - Run `/bmad-help` to get guided next steps and context-aware recommendations.
| **Slash command** | Type `/bmad-...` in your IDE | Directly loads an agent, runs a workflow, or executes a task |
| **Agent menu trigger** | Load an agent first, then type a short code (e.g. `DS`) | The agent interprets the code and starts the matching workflow while staying in character |
Agent menu triggers require an active agent session. Use slash commands when you know which workflow you want. Use triggers when you are already working with an agent and want to switch tasks without leaving the conversation. ## Where Commands Are Generated
## How Commands Are Generated The installer writes command files into your project (example paths for Claude Code):
When you run `npx bmad-method install`, the installer reads the manifests for every selected module and writes one command file per agent, workflow, task, and tool. Each file is a short markdown prompt that instructs the AI to load the corresponding source file and follow its instructions. - `.claude/commands/bmad/<module>/agents/`
- `.claude/commands/bmad/<module>/workflows/`
The installer uses templates for each command type: These folders are the **canonical, project-specific command list**.
| Command type | What the generated file does | ## Common Commands
| --- | --- |
| **Agent launcher** | Loads the agent persona file, activates its menu, and stays in character |
| **Workflow command** | Loads the workflow engine (`workflow.md`) and passes the workflow config |
| **Task command** | Loads a standalone task file and follows its instructions |
| **Tool command** | Loads a standalone tool file and follows its instructions |
:::note[Re-running the installer] - `/bmad-help` - Interactive help and next-step guidance
If you add or remove modules, run the installer again. It regenerates all command files to match your current module selection. - `/bmad:<module>:agents:<agent>` - Load an agent (e.g. `/bmad:bmm:agents:dev`)
::: - `/bmad:<module>:workflows:<workflow>` - Run a workflow (e.g. `/bmad:bmm:workflows:create-prd`)
## Where Command Files Live ## Why This Page Is Short
The installer writes command files into an IDE-specific directory inside your project. The exact path depends on which IDE you selected during installation. BMAD is modular, so the exact commands vary by install.
Use your IDE's autocomplete or the generated command folders above to see *everything* available.
| IDE / CLI | Command directory |
| --- | --- |
| Claude Code | `.claude/commands/` |
| Cursor | `.cursor/commands/` |
| Windsurf | `.windsurf/workflows/` |
| Other IDEs | See the installer output for the target path |
All IDEs receive a flat set of command files in their command directory. For example, a Claude Code installation looks like:
```text
.claude/commands/
├── bmad-agent-bmm-dev.md
├── bmad-agent-bmm-pm.md
├── bmad-bmm-create-prd.md
├── bmad-editorial-review-prose.md
├── bmad-help.md
└── ...
```
The filename determines the slash command name in your IDE. For example, the file `bmad-agent-bmm-dev.md` registers the command `/bmad-agent-bmm-dev`.
## How to Discover Your Commands
Type `/bmad` in your IDE and use autocomplete to browse available commands.
Run `/bmad-help` for context-aware guidance on your next step.
:::tip[Quick discovery]
The generated command folders in your project are the canonical list. Open them in your file explorer to see every command with its description.
:::
## Command Categories
### Agent Commands
Agent commands load a specialized AI persona with a defined role, communication style, and menu of workflows. Once loaded, the agent stays in character and responds to menu triggers.
| Example command | Agent | Role |
| --- | --- | --- |
| `/bmad-agent-bmm-dev` | Amelia (Developer) | Implements stories with strict adherence to specs |
| `/bmad-agent-bmm-pm` | John (Product Manager) | Creates and validates PRDs |
| `/bmad-agent-bmm-architect` | Winston (Architect) | Designs system architecture |
| `/bmad-agent-bmm-sm` | Bob (Scrum Master) | Manages sprints and stories |
See [Agents](./agents.md) for the full list of default agents and their triggers.
### Workflow Commands
Workflow commands run a structured, multi-step process without loading an agent persona first. They load the workflow engine and pass a specific workflow configuration.
| Example command | Purpose |
| --- | --- |
| `/bmad-bmm-create-prd` | Create a Product Requirements Document |
| `/bmad-bmm-create-architecture` | Design system architecture |
| `/bmad-bmm-dev-story` | Implement a story |
| `/bmad-bmm-code-review` | Run a code review |
| `/bmad-bmm-quick-spec` | Define an ad-hoc change (Quick Flow) |
See [Workflow Map](./workflow-map.md) for the complete workflow reference organized by phase.
### Task and Tool Commands
Tasks and tools are standalone operations that do not require an agent or workflow context.
| Example command | Purpose |
| --- | --- |
| `/bmad-help` | Context-aware guidance and next-step recommendations |
| `/bmad-shard-doc` | Split a large markdown file into smaller sections |
| `/bmad-index-docs` | Index project documentation |
| `/bmad-editorial-review-prose` | Review document prose quality |
## Naming Convention
Command names follow a predictable pattern.
| Pattern | Meaning | Example |
| --- | --- | --- |
| `bmad-agent-<module>-<name>` | Agent launcher | `bmad-agent-bmm-dev` |
| `bmad-<module>-<workflow>` | Workflow command | `bmad-bmm-create-prd` |
| `bmad-<name>` | Core task or tool | `bmad-help` |
Module codes: `bmm` (Agile suite), `bmb` (Builder), `tea` (Test Architect), `cis` (Creative Intelligence), `gds` (Game Dev Studio). See [Modules](./modules.md) for descriptions.
## Troubleshooting
**Commands not appearing after install.** Restart your IDE or reload the window. Some IDEs cache the command list and require a refresh to pick up new files.
**Expected commands are missing.** The installer only generates commands for modules you selected. Run `npx bmad-method install` again and verify your module selection. Check that the command files exist in the expected directory.
**Commands from a removed module still appear.** The installer does not delete old command files automatically. Remove the stale files from your IDE's command directory, or delete the entire command directory and re-run the installer for a clean set.

View File

@ -1,8 +1,6 @@
--- ---
title: Official Modules title: Official Modules
description: Add-on modules for building custom agents, creative intelligence, game development, and testing description: Add-on modules for building custom agents, creative intelligence, game development, and testing
sidebar:
order: 4
--- ---
BMad extends through official modules that you select during installation. These add-on modules provide specialized agents, workflows, and tasks for specific domains beyond the built-in core and BMM (Agile suite). BMad extends through official modules that you select during installation. These add-on modules provide specialized agents, workflows, and tasks for specific domains beyond the built-in core and BMM (Agile suite).

View File

@ -1,106 +1,22 @@
--- ---
title: Testing Options title: Testing Options
description: Comparing the built-in QA agent (Quinn) with the Test Architect (TEA) module for test automation. description: Built-in QA agent and the standalone Test Architect module for advanced testing
sidebar:
order: 5
--- ---
BMad provides two testing paths: a built-in QA agent for fast test generation and an installable Test Architect module for enterprise-grade test strategy. # Testing Options
## Which Should You Use? BMad provides a built-in QA agent for quick test automation and a separate Test Architect (TEA) module for advanced testing.
| Factor | Quinn (Built-in QA) | TEA Module | ## Built-in QA (Quinn)
| --- | --- | --- |
| **Best for** | Small-medium projects, quick coverage | Large projects, regulated or complex domains |
| **Setup** | Nothing to install -- included in BMM | Install separately via `npx bmad-method install` |
| **Approach** | Generate tests fast, iterate later | Plan first, then generate with traceability |
| **Test types** | API and E2E tests | API, E2E, ATDD, NFR, and more |
| **Strategy** | Happy path + critical edge cases | Risk-based prioritization (P0-P3) |
| **Workflow count** | 1 (Automate) | 9 (design, ATDD, automate, review, trace, and others) |
:::tip[Start with Quinn] Use the built-in QA agent for fast, straightforward test coverage:
Most projects should start with Quinn. If you later need test strategy, quality gates, or requirements traceability, install TEA alongside it.
:::
## Built-in QA Agent (Quinn) - Trigger: `QA` or `bmad-bmm-qa-automate`
- Best for: small projects, quick coverage, standard patterns
Quinn is the built-in QA agent in the BMM (Agile suite) module. It generates working tests quickly using your project's existing test framework -- no configuration or additional installation required.
**Trigger:** `QA` or `bmad-bmm-qa-automate`
### What Quinn Does
Quinn runs a single workflow (Automate) that walks through five steps:
1. **Detect test framework** -- scans `package.json` and existing test files for your framework (Jest, Vitest, Playwright, Cypress, or any standard runner). If none exists, analyzes the project stack and suggests one.
2. **Identify features** -- asks what to test or auto-discovers features in the codebase.
3. **Generate API tests** -- covers status codes, response structure, happy path, and 1-2 error cases.
4. **Generate E2E tests** -- covers user workflows with semantic locators and visible-outcome assertions.
5. **Run and verify** -- executes the generated tests and fixes failures immediately.
Quinn produces a test summary saved to your project's implementation artifacts folder.
### Test Patterns
Generated tests follow a "simple and maintainable" philosophy:
- **Standard framework APIs only** -- no external utilities or custom abstractions
- **Semantic locators** for UI tests (roles, labels, text rather than CSS selectors)
- **Independent tests** with no order dependencies
- **No hardcoded waits or sleeps**
- **Clear descriptions** that read as feature documentation
:::note[Scope]
Quinn generates tests only. For code review and story validation, use the Code Review workflow (`CR`) instead.
:::
### When to Use Quinn
- Quick test coverage for a new or existing feature
- Beginner-friendly test automation without advanced setup
- Standard test patterns that any developer can read and maintain
- Small-medium projects where comprehensive test strategy is unnecessary
## Test Architect (TEA) Module ## Test Architect (TEA) Module
TEA is a standalone module that provides an expert agent (Murat) and nine structured workflows for enterprise-grade testing. It goes beyond test generation into test strategy, risk-based planning, quality gates, and requirements traceability. TEA is a standalone module with advanced testing workflows (test design, ATDD, automate, review, trace, NFR assessment).
- **Documentation:** [TEA Module Docs](https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/) - Documentation: <https://bmad-code-org.github.io/bmad-method-test-architecture-enterprise/>
- **Install:** `npx bmad-method install` and select the TEA module - Install: `npx bmad-method@alpha install` and select the TEA module
- **npm:** [`bmad-method-test-architecture-enterprise`](https://www.npmjs.com/package/bmad-method-test-architecture-enterprise)
### What TEA Provides
| Workflow | Purpose |
| --- | --- |
| Test Design | Create a comprehensive test strategy tied to requirements |
| ATDD | Acceptance-test-driven development with stakeholder criteria |
| Automate | Generate tests with advanced patterns and utilities |
| Test Review | Validate test quality and coverage against strategy |
| Traceability | Map tests back to requirements for audit and compliance |
| NFR Assessment | Evaluate non-functional requirements (performance, security) |
| CI Setup | Configure test execution in continuous integration pipelines |
| Framework Scaffolding | Set up test infrastructure and project structure |
| Release Gate | Make data-driven go/no-go release decisions |
TEA also supports P0-P3 risk-based prioritization and optional integrations with Playwright Utils and MCP tooling.
### When to Use TEA
- Projects that require requirements traceability or compliance documentation
- Teams that need risk-based test prioritization across many features
- Enterprise environments with formal quality gates before release
- Complex domains where test strategy must be planned before tests are written
- Projects that have outgrown Quinn's single-workflow approach
## How Testing Fits into Workflows
Quinn's Automate workflow appears in Phase 4 (Implementation) of the BMad Method workflow map. A typical sequence:
1. Implement a story with the Dev workflow (`DS`)
2. Generate tests with Quinn (`QA`) or TEA's Automate workflow
3. Validate implementation with Code Review (`CR`)
Quinn works directly from source code without loading planning documents (PRD, architecture). TEA workflows can integrate with upstream planning artifacts for traceability.
For more on where testing fits in the overall process, see the [Workflow Map](./workflow-map.md).

View File

@ -1,23 +1,19 @@
--- ---
title: "Workflow Map" title: "Workflow Map"
description: Visual reference for BMad Method workflow phases and outputs description: Visual reference for BMad Method workflow phases and outputs
sidebar:
order: 1
--- ---
The BMad Method (BMM) is a module in the BMad Ecosystem, targeted at following the best practices of context engineering and planning. AI agents work best with clear, structured context. The BMM system builds that context progressively across 4 distinct phases - each phase, and multiple workflows optionally within each phase, produce documents that inform the next, so agents always know what to build and why. The BMad Method (BMM) is a module in the BMad Ecosystem, targeted at following the best practices of context engineering and planning. AI agents work best with clear, structured context. The BMM system builds that context progressively across 4 distinct phases - each phase, and multiple workflows optionally within each phase, produce documents that inform the next, so agents always know what to build and why.
The rationale and concepts come from agile methodologies that have been used across the industry with great success as a mental framework. The rationale and concepts come from agile methodologies that have been used across the industry with great success as a mental framework.
If at any time you are unsure what to do, the `/bmad-help` command will help you stay on track or know what to do next. You can always refer to this for reference also - but /bmad-help is fully interactive and much quicker if you have already installed the BMad Method. Additionally, if you are using different modules that have extended the BMad Method or added other complementary non-extension modules - the /bmad-help evolves to know all that is available to give you the best in-the-moment advice. If at anytime you are unsure what to do, the `/bmad-help` command will help you stay on track or know what to do next. You can always refer to this for reference also - but /bmad-help is fully interactive and much quicker if you have already installed the BMadMethod. Additionally, if you are using different modules that have extended the BMad Method or added other complimentary non extension modules - the /bmad-help evolves to know all that is available to give you the best in the moment advice.
Final important note: Every workflow below can be run directly with your tool of choice via slash command or by loading an agent first and using the entry from the agents menu. Final important note: Every workflow below can be run directly with your tool of choice via slash command or by loading an agent first and using the entry from the agents menu.
<iframe src="/workflow-map-diagram.html" title="BMad Method Workflow Map Diagram" width="100%" height="100%" style="border-radius: 8px; border: 1px solid #334155; min-height: 900px;"></iframe> <iframe src="/workflow-map-diagram.html" width="100%" height="100%" frameborder="0" style="border-radius: 8px; border: 1px solid #334155; min-height: 900px;"></iframe>
<p style="font-size: 0.8rem; text-align: right; margin-top: -0.5rem; margin-bottom: 1rem;"> *[Interactive diagram - hover over outputs to see artifact flows]*
<a href="/workflow-map-diagram.html" target="_blank" rel="noopener noreferrer">Open diagram in new tab ↗</a>
</p>
## Phase 1: Analysis (Optional) ## Phase 1: Analysis (Optional)
@ -25,7 +21,7 @@ Explore the problem space and validate ideas before committing to planning.
| Workflow | Purpose | Produces | | Workflow | Purpose | Produces |
| ---------------------- | -------------------------------------------------------------------------- | ------------------------- | | ---------------------- | -------------------------------------------------------------------------- | ------------------------- |
| `brainstorming` | Brainstorm Project Ideas with guided facilitation of a brainstorming coach | `brainstorming-report.md` | | `brainstorm` | Brainstorm Project Ideas with guided facilitation of a brainstorming coach | `brainstorming-report.md` |
| `research` | Validate market, technical, or domain assumptions | Research findings | | `research` | Validate market, technical, or domain assumptions | Research findings |
| `create-product-brief` | Capture strategic vision | `product-brief.md` | | `create-product-brief` | Capture strategic vision | `product-brief.md` |

View File

@ -37,7 +37,7 @@ BMad helps you build software through guided workflows with specialized AI agent
| 3 | Solutioning | Design architecture *(BMad Method/Enterprise only)* | | 3 | Solutioning | Design architecture *(BMad Method/Enterprise only)* |
| 4 | Implementation | Build epic by epic, story by story | | 4 | Implementation | Build epic by epic, story by story |
**[Open the Workflow Map](../reference/workflow-map.md)** to explore phases, workflows, and context management. **[Open the Workflow Map](/docs/reference/workflow-map.md)** to explore phases, workflows, and context management.
Based on your project's complexity, BMad offers three planning tracks: Based on your project's complexity, BMad offers three planning tracks:
@ -149,7 +149,7 @@ You've learned the foundation of building with BMad:
Your project now has: Your project now has:
```text ```
your-project/ your-project/
├── _bmad/ # BMad configuration ├── _bmad/ # BMad configuration
├── _bmad-output/ ├── _bmad-output/

View File

@ -83,7 +83,7 @@ export default [
// CLI scripts under tools/** and test/** // CLI scripts under tools/** and test/**
{ {
files: ['tools/**/*.js', 'tools/**/*.mjs', 'test/**/*.js', 'test/**/*.mjs'], files: ['tools/**/*.js', 'tools/**/*.mjs', 'test/**/*.js'],
rules: { rules: {
// Allow CommonJS patterns for Node CLI scripts // Allow CommonJS patterns for Node CLI scripts
'unicorn/prefer-module': 'off', 'unicorn/prefer-module': 'off',

390
package-lock.json generated
View File

@ -12,15 +12,20 @@
"@clack/core": "^1.0.0", "@clack/core": "^1.0.0",
"@clack/prompts": "^1.0.0", "@clack/prompts": "^1.0.0",
"@kayvan/markdown-tree-parser": "^1.6.1", "@kayvan/markdown-tree-parser": "^1.6.1",
"boxen": "^5.1.2",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"cli-table3": "^0.6.5",
"commander": "^14.0.0", "commander": "^14.0.0",
"csv-parse": "^6.1.0", "csv-parse": "^6.1.0",
"figlet": "^1.8.0",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"glob": "^11.0.3", "glob": "^11.0.3",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"ora": "^5.4.1",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"semver": "^7.6.3", "semver": "^7.6.3",
"wrap-ansi": "^7.0.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"yaml": "^2.7.0" "yaml": "^2.7.0"
}, },
@ -772,6 +777,16 @@
"sisteransi": "^1.0.5" "sisteransi": "^1.0.5"
} }
}, },
"node_modules/@colors/colors": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
"integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/@ctrl/tinycolor": { "node_modules/@ctrl/tinycolor": {
"version": "4.2.0", "version": "4.2.0",
"resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
@ -2014,9 +2029,9 @@
} }
}, },
"node_modules/@isaacs/brace-expansion": { "node_modules/@isaacs/brace-expansion": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@isaacs/balanced-match": "^4.0.1" "@isaacs/balanced-match": "^4.0.1"
@ -3978,7 +3993,6 @@
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz",
"integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==",
"dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"string-width": "^4.1.0" "string-width": "^4.1.0"
@ -4971,6 +4985,26 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.9.19", "version": "2.9.19",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz",
@ -5008,12 +5042,59 @@
"url": "https://github.com/sponsors/wooorm" "url": "https://github.com/sponsors/wooorm"
} }
}, },
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/bl/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/boolbase": { "node_modules/boolbase": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/boxen": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/boxen/-/boxen-5.1.2.tgz",
"integrity": "sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==",
"license": "MIT",
"dependencies": {
"ansi-align": "^3.0.0",
"camelcase": "^6.2.0",
"chalk": "^4.1.0",
"cli-boxes": "^2.2.1",
"string-width": "^4.2.2",
"type-fest": "^0.20.2",
"widest-line": "^3.1.0",
"wrap-ansi": "^7.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@ -5082,6 +5163,30 @@
"node-int64": "^0.4.0" "node-int64": "^0.4.0"
} }
}, },
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -5150,7 +5255,6 @@
"version": "6.3.0", "version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=10"
@ -5329,6 +5433,18 @@
"node": ">=0.8.0" "node": ">=0.8.0"
} }
}, },
"node_modules/cli-boxes": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz",
"integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-cursor": { "node_modules/cli-cursor": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz",
@ -5345,6 +5461,33 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/cli-spinners": {
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz",
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
"license": "MIT",
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/cli-table3": {
"version": "0.6.5",
"resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz",
"integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==",
"license": "MIT",
"dependencies": {
"string-width": "^4.2.0"
},
"engines": {
"node": "10.* || >= 12.*"
},
"optionalDependencies": {
"@colors/colors": "1.5.0"
}
},
"node_modules/cli-truncate": { "node_modules/cli-truncate": {
"version": "5.1.1", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz", "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-5.1.1.tgz",
@ -5417,6 +5560,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/clone": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz",
"integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==",
"license": "MIT",
"engines": {
"node": ">=0.8"
}
},
"node_modules/clsx": { "node_modules/clsx": {
"version": "2.1.1", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@ -5790,6 +5942,18 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/defaults": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz",
"integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==",
"license": "MIT",
"dependencies": {
"clone": "^1.0.2"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/defu": { "node_modules/defu": {
"version": "6.1.4", "version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
@ -6870,6 +7034,21 @@
} }
} }
}, },
"node_modules/figlet": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/figlet/-/figlet-1.10.0.tgz",
"integrity": "sha512-aktIwEZZ6Gp9AWdMXW4YCi0J2Ahuxo67fNJRUIWD81w8pQ0t9TS8FFpbl27ChlTLF06VkwjDesZSzEVzN75rzA==",
"license": "MIT",
"dependencies": {
"commander": "^14.0.0"
},
"bin": {
"figlet": "bin/index.js"
},
"engines": {
"node": ">= 17.0.0"
}
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@ -7727,6 +7906,26 @@
"@babel/runtime": "^7.23.2" "@babel/runtime": "^7.23.2"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "7.0.5", "version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
@ -7823,7 +8022,6 @@
"version": "2.0.4", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/ini": { "node_modules/ini": {
@ -8008,6 +8206,15 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-interactive": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz",
"integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/is-number": { "node_modules/is-number": {
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@ -8043,6 +8250,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/is-unicode-supported": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz",
"integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/is-wsl": { "node_modules/is-wsl": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz",
@ -9304,6 +9523,22 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/log-symbols": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
"integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.0",
"is-unicode-supported": "^0.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/log-update": { "node_modules/log-update": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz",
@ -10750,7 +10985,6 @@
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@ -11062,7 +11296,6 @@
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz",
"integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"mimic-fn": "^2.1.0" "mimic-fn": "^2.1.0"
@ -11111,6 +11344,81 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/ora": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz",
"integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==",
"license": "MIT",
"dependencies": {
"bl": "^4.1.0",
"chalk": "^4.1.0",
"cli-cursor": "^3.1.0",
"cli-spinners": "^2.5.0",
"is-interactive": "^1.0.0",
"is-unicode-supported": "^0.1.0",
"log-symbols": "^4.1.0",
"strip-ansi": "^6.0.0",
"wcwidth": "^1.0.1"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ora/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/ora/node_modules/cli-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
"integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==",
"license": "MIT",
"dependencies": {
"restore-cursor": "^3.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ora/node_modules/restore-cursor": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz",
"integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==",
"license": "MIT",
"dependencies": {
"onetime": "^5.1.0",
"signal-exit": "^3.0.2"
},
"engines": {
"node": ">=8"
}
},
"node_modules/ora/node_modules/signal-exit": {
"version": "3.0.7",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
"license": "ISC"
},
"node_modules/ora/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1"
},
"engines": {
"node": ">=8"
}
},
"node_modules/p-limit": { "node_modules/p-limit": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz",
@ -12418,6 +12726,26 @@
"queue-microtask": "^1.2.2" "queue-microtask": "^1.2.2"
} }
}, },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/sax": { "node_modules/sax": {
"version": "1.4.4", "version": "1.4.4",
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz",
@ -12770,6 +13098,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/string-argv": { "node_modules/string-argv": {
"version": "0.3.2", "version": "0.3.2",
"resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz",
@ -13354,6 +13691,18 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/type-fest": {
"version": "0.20.2",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/uc.micro": { "node_modules/uc.micro": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz",
@ -13803,7 +14152,6 @@
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/v8-to-istanbul": { "node_modules/v8-to-istanbul": {
@ -13969,6 +14317,15 @@
"makeerror": "1.0.12" "makeerror": "1.0.12"
} }
}, },
"node_modules/wcwidth": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz",
"integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==",
"license": "MIT",
"dependencies": {
"defaults": "^1.0.3"
}
},
"node_modules/web-namespaces": { "node_modules/web-namespaces": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",
@ -14005,6 +14362,18 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/widest-line": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz",
"integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==",
"license": "MIT",
"dependencies": {
"string-width": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/word-wrap": { "node_modules/word-wrap": {
"version": "1.2.5", "version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
@ -14019,7 +14388,6 @@
"version": "7.0.0", "version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-styles": "^4.0.0", "ansi-styles": "^4.0.0",
@ -14076,7 +14444,6 @@
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
@ -14086,7 +14453,6 @@
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"ansi-regex": "^5.0.1" "ansi-regex": "^5.0.1"

View File

@ -25,11 +25,13 @@
}, },
"scripts": { "scripts": {
"bmad:install": "node tools/cli/bmad-cli.js install", "bmad:install": "node tools/cli/bmad-cli.js install",
"bundle": "node tools/cli/bundlers/bundle-web.js all",
"docs:build": "node tools/build-docs.mjs", "docs:build": "node tools/build-docs.mjs",
"docs:dev": "astro dev --root website", "docs:dev": "astro dev --root website",
"docs:fix-links": "node tools/fix-doc-links.js", "docs:fix-links": "node tools/fix-doc-links.js",
"docs:preview": "astro preview --root website", "docs:preview": "astro preview --root website",
"docs:validate-links": "node tools/validate-doc-links.js", "docs:validate-links": "node tools/validate-doc-links.js",
"flatten": "node tools/flattener/main.js",
"format:check": "prettier --check \"**/*.{js,cjs,mjs,json,yaml}\"", "format:check": "prettier --check \"**/*.{js,cjs,mjs,json,yaml}\"",
"format:fix": "prettier --write \"**/*.{js,cjs,mjs,json,yaml}\"", "format:fix": "prettier --write \"**/*.{js,cjs,mjs,json,yaml}\"",
"format:fix:staged": "prettier --write", "format:fix:staged": "prettier --write",
@ -39,10 +41,9 @@
"lint:md": "markdownlint-cli2 \"**/*.md\"", "lint:md": "markdownlint-cli2 \"**/*.md\"",
"prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0", "prepare": "command -v husky >/dev/null 2>&1 && husky || exit 0",
"rebundle": "node tools/cli/bundlers/bundle-web.js rebundle", "rebundle": "node tools/cli/bundlers/bundle-web.js rebundle",
"test": "npm run test:schemas && npm run test:refs && npm run test:install && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check", "test": "npm run test:schemas && npm run test:install && npm run validate:schemas && npm run lint && npm run lint:md && npm run format:check",
"test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas", "test:coverage": "c8 --reporter=text --reporter=html npm run test:schemas",
"test:install": "node test/test-installation-components.js", "test:install": "node test/test-installation-components.js",
"test:refs": "node test/test-file-refs-csv.js",
"test:schemas": "node test/test-agent-schema.js", "test:schemas": "node test/test-agent-schema.js",
"validate:refs": "node tools/validate-file-refs.js", "validate:refs": "node tools/validate-file-refs.js",
"validate:schemas": "node tools/validate-agent-schema.js" "validate:schemas": "node tools/validate-agent-schema.js"
@ -67,15 +68,20 @@
"@clack/core": "^1.0.0", "@clack/core": "^1.0.0",
"@clack/prompts": "^1.0.0", "@clack/prompts": "^1.0.0",
"@kayvan/markdown-tree-parser": "^1.6.1", "@kayvan/markdown-tree-parser": "^1.6.1",
"boxen": "^5.1.2",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"cli-table3": "^0.6.5",
"commander": "^14.0.0", "commander": "^14.0.0",
"csv-parse": "^6.1.0", "csv-parse": "^6.1.0",
"figlet": "^1.8.0",
"fs-extra": "^11.3.0", "fs-extra": "^11.3.0",
"glob": "^11.0.3", "glob": "^11.0.3",
"ignore": "^7.0.5", "ignore": "^7.0.5",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"ora": "^5.4.1",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"semver": "^7.6.3", "semver": "^7.6.3",
"wrap-ansi": "^7.0.0",
"xml2js": "^0.6.2", "xml2js": "^0.6.2",
"yaml": "^2.7.0" "yaml": "^2.7.0"
}, },

View File

@ -15,9 +15,9 @@
## EXECUTION PROTOCOLS: ## EXECUTION PROTOCOLS:
- 🎯 Provide a brief rationale before taking any action - 🎯 Show your analysis before taking any action
- 🌐 Search the web to verify technology versions and options - 🌐 Search the web to verify technology versions and options
- ⚠️ Present A/P/C menu after drafting decision content (and when user requests refinement) - ⚠️ Present A/P/C menu after each major decision category
- 💾 ONLY save when user chooses C (Continue) - 💾 ONLY save when user chooses C (Continue)
- 📖 Update frontmatter `stepsCompleted: [1, 2, 3, 4]` before loading next step - 📖 Update frontmatter `stepsCompleted: [1, 2, 3, 4]` before loading next step
- 🚫 FORBIDDEN to load next step until C is selected - 🚫 FORBIDDEN to load next step until C is selected
@ -44,7 +44,7 @@ This step will generate content and present choices for each decision category:
- Project context file may contain technical preferences and rules - Project context file may contain technical preferences and rules
- Technical preferences discovered in step 3 are available - Technical preferences discovered in step 3 are available
- Focus on decisions not already made by starter template or existing preferences - Focus on decisions not already made by starter template or existing preferences
- Collaborative decision making first; recommendations are allowed only with explicit rationale and user confirmation - Collaborative decision making, not recommendations
## YOUR TASK: ## YOUR TASK:
@ -58,7 +58,7 @@ Facilitate collaborative architectural decision making, leveraging existing tech
"Based on our technical preferences discussion in step 3, let's build on those foundations: "Based on our technical preferences discussion in step 3, let's build on those foundations:
**Your Technical Preferences:** **Your Technical Preferences:**
{{user_technical_preferences}} {{user_technical_preferences_from_step_3}}
**Starter Template Decisions:** **Starter Template Decisions:**
{{starter_template_decisions}} {{starter_template_decisions}}
@ -72,7 +72,7 @@ Based on technical preferences, starter template choice, and project context, id
**Already Decided (Don't re-decide these):** **Already Decided (Don't re-decide these):**
- {{starter_template_decisions}} - {{starter_template_decisions}}
- {{user_technical_preferences}} - {{user_technology_preferences}}
- {{project_context_technical_rules}} - {{project_context_technical_rules}}
**Critical Decisions:** Must be decided before implementation can proceed **Critical Decisions:** Must be decided before implementation can proceed
@ -165,7 +165,7 @@ If decision involves specific technology:
``` ```
Search the web: "{{technology}} latest stable version" Search the web: "{{technology}} latest stable version"
Search the web: "{{technology}} current LTS version" (only if the technology publishes an LTS track) Search the web: "{{technology}} current LTS version"
Search the web: "{{technology}} production readiness" Search the web: "{{technology}} production readiness"
``` ```

View File

@ -36,7 +36,7 @@ Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
### Paths ### Paths
- `installed_path` = `{project-root}/_bmad/bmm/workflows/3-solutioning/create-architecture` - `installed_path` = `{project-root}/_bmad/bmm/workflows/3-solutioning/architecture`
- `template_path` = `{installed_path}/architecture-decision-template.md` - `template_path` = `{installed_path}/architecture-decision-template.md`
- `data_files_path` = `{installed_path}/data/` - `data_files_path` = `{installed_path}/data/`

View File

@ -88,18 +88,11 @@ Search for required documents using these patterns (sharded means a large docume
1. `{planning_artifacts}/*ux*.md` (whole document) 1. `{planning_artifacts}/*ux*.md` (whole document)
2. `{planning_artifacts}/*ux*/index.md` (sharded version) 2. `{planning_artifacts}/*ux*/index.md` (sharded version)
**Deterministic Selection Rules (required):**
- When multiple files match the same priority level, prefer exact filename matches (`prd.md`, `architecture.md`, `ux-design.md`) before wildcard matches.
- If multiple wildcard matches remain, prefer files under `{planning_artifacts}` root before nested paths.
- If ambiguity still remains, present candidates to the user and require explicit selection before extraction.
- Record the final selected files in `inputDocuments` frontmatter to keep downstream steps deterministic.
Before proceeding, Ask the user if there are any other documents to include for analysis, and if anything found should be excluded. Wait for user confirmation. Once confirmed, create the {outputFile} from the {epicsTemplate} and in the front matter list the files in the array of `inputDocuments: []`. Before proceeding, Ask the user if there are any other documents to include for analysis, and if anything found should be excluded. Wait for user confirmation. Once confirmed, create the {outputFile} from the {epicsTemplate} and in the front matter list the files in the array of `inputDocuments: []`.
### 3. Extract Functional Requirements (FRs) ### 3. Extract Functional Requirements (FRs)
From the PRD document (full or sharded), read the entire document and extract ALL functional requirements: From the PRD document (full or sharded), read then entire document and extract ALL functional requirements:
**Extraction Method:** **Extraction Method:**

View File

@ -12,10 +12,9 @@ reviewFindingsFile: '{story_dir}/review-findings.json'
<action>Initialize findings artifacts: <action>Initialize findings artifacts:
- Set {{review_findings}} = [] (in-memory array) - Set {{review_findings}} = [] (in-memory array)
- Set {{review_findings_file}} = {reviewFindingsFile} - Set {{review_findings_file}} = {reviewFindingsFile}
- Set {{review_findings_schema}} = "id,severity,type,summary,detail,file_line,proof,suggested_fix,reviewer,timestamp"
- Each finding record MUST contain: - Each finding record MUST contain:
id, severity, type, summary, detail, file_line, proof, suggested_fix, reviewer, timestamp id, severity, type, summary, detail, file_line, proof, suggested_fix, reviewer, timestamp
- `file_line` is the required `file:line` locator field and MUST use `path/to/file:line` format - `file_line` format MUST be `path/to/file:line`
- `reviewer` value MUST be `senior-dev-review` - `reviewer` value MUST be `senior-dev-review`
- `timestamp` MUST use system ISO datetime - `timestamp` MUST use system ISO datetime
</action> </action>
@ -36,67 +35,28 @@ reviewFindingsFile: '{story_dir}/review-findings.json'
1. Read the AC requirement 1. Read the AC requirement
2. Search implementation files for evidence 2. Search implementation files for evidence
3. Determine: IMPLEMENTED, PARTIAL, or MISSING using this algorithm: 3. Determine: IMPLEMENTED, PARTIAL, or MISSING using this algorithm:
- Parse each AC into explicit clauses (single requirement statements).
- Evaluate each clause independently, then derive overall AC status from clause outcomes.
- IMPLEMENTED: - IMPLEMENTED:
- EVERY clause has direct code evidence tied to the story execution path, and - Direct code evidence exists for ALL AC clauses, and
- Evidence includes at least one strong corroborator for AC behavior (automated test, integration test, or reproducible runtime proof), and - At least one corroborating test OR deterministic runtime verification exists, and
- Weak evidence (docs/comments/README) is only supplemental. - Any docs/comments are supported by code/test evidence.
- PARTIAL: - PARTIAL:
- One or more clauses have direct evidence, but at least one clause lacks direct evidence, OR - Some AC clauses have direct implementation evidence but one or more clauses are missing OR only indirectly covered, or
- Coverage is only indirect (helper/generic utility not proven wired), OR - Evidence is helper/utility code not clearly wired to the story path, or
- Evidence is mostly weak and not corroborated by code/tests. - Evidence is docs/comments only without strong corroboration.
- MISSING: - MISSING:
- No clause has credible direct implementation evidence, and - No credible code/test/docs evidence addresses the AC clauses.
- No test/runtime proof demonstrates AC behavior. 4. Evidence-strength rules:
4. Evidence-type rules: - Code + tests = strong evidence
- Strong evidence: implementation code plus validating tests/runtime proof. - Code only = medium evidence
- Medium evidence: implementation code without validating tests. - Docs/comments/README only = weak evidence (cannot justify IMPLEMENTED alone)
- Weak evidence: comments, README/docs, design notes, screenshots, or unverifiable logs.
- Weak evidence alone cannot qualify an AC as IMPLEMENTED.
5. Indirect evidence rules: 5. Indirect evidence rules:
- Helper functions/utilities count as indirect until explicit call sites or integration coverage prove the AC path. - Generic helpers/utilities count as PARTIAL unless explicitly wired by call sites OR integration tests.
- Generic capability not wired to this story remains PARTIAL.
6. Severity mapping for AC gaps: 6. Severity mapping for AC gaps:
- MISSING + security/data-loss/compliance/core user flow risk -> HIGH. - MISSING critical-path AC → HIGH
- MISSING + non-core behavior or secondary UX/documentation requirement -> MEDIUM. - MISSING non-critical AC → MEDIUM
- PARTIAL + security/data-integrity/compliance risk -> HIGH. - PARTIAL critical-path AC → HIGH
- PARTIAL + degraded core behavior -> MEDIUM. - PARTIAL non-critical AC → MEDIUM
- PARTIAL + optional/non-critical behavior gap with safe fallback -> LOW. 7. If AC is PARTIAL or MISSING, append a finding object to {{review_findings}}.
7. Classification examples:
- IMPLEMENTED example: AC requires validation + error response, and code path plus passing test covers all clauses.
- PARTIAL example: helper exists and one clause passes, but integration path for another clause is unproven.
- MISSING example: AC text exists, but no matching code path or tests are found.
8. If AC is PARTIAL or MISSING, append a finding object to {{review_findings}} with status, severity, and clause-level proof.
</action>
<action>When creating findings from any action above, populate fields using this mapping:
- id:
- Git discrepancy: `GIT-DIFF-{{index}}`
- AC gap: `AC-{{ac_id}}-{{status}}-{{index}}`
- Task mismatch: `TASK-{{task_id}}-MISMATCH-{{index}}`
- Code-quality issue: `CQ-{{category}}-{{index}}`
- severity:
- Use explicit severity rule from the originating action block
- type:
- `story-sync` for git/story discrepancies
- `acceptance-criteria` for AC gaps
- `task-audit` for task completion mismatches
- `code-quality` for quality/security/performance/test issues
- summary:
- One-line, user-facing issue statement
- detail:
- Include violated expectation plus observed behavior
- file_line:
- `path/to/file:line` evidence anchor (use most relevant file and line)
- proof:
- Concrete evidence snippet (code, test output, or git command result)
- suggested_fix:
- Actionable implementation guidance
- reviewer:
- `senior-dev-review`
- timestamp:
- System ISO datetime at finding creation time
</action> </action>
<!-- Task Completion Audit --> <!-- Task Completion Audit -->
@ -134,8 +94,7 @@ reviewFindingsFile: '{story_dir}/review-findings.json'
<action>Persist findings contract for downstream step: <action>Persist findings contract for downstream step:
- Save {{review_findings}} as JSON array to {{review_findings_file}} - Save {{review_findings}} as JSON array to {{review_findings_file}}
- Ensure JSON is valid and each finding includes all required fields - Ensure JSON is valid and each finding includes all required fields
- Set {{findings_contract}} = "JSON array at {{review_findings_file}} with schema {{review_findings_schema}}" - Set {{findings_contract}} = "JSON array at {{review_findings_file}}"
- Step 4 MUST load findings from {{review_findings_file}} and validate against {{review_findings_schema}} before presenting or resolving
</action> </action>
<action>Example finding record (must match real records): <action>Example finding record (must match real records):

View File

@ -6,20 +6,11 @@ reviewFindingsFile: '{story_dir}/review-findings.json'
--- ---
<step n="4" goal="Present findings and fix them"> <step n="4" goal="Present findings and fix them">
<action>Resolve findings artifact input: <action>Load structured findings from {reviewFindingsFile}</action>
- Use {{review_findings_file}} from step 3 when present
- Otherwise fallback to {reviewFindingsFile}
- Set {{review_findings_schema}} = "id,severity,type,summary,detail,file_line,proof,suggested_fix,reviewer,timestamp" if not already set
</action>
<action>Load structured findings JSON array from {{review_findings_file}}</action>
<action>Validate findings schema for each entry: <action>Validate findings schema for each entry:
id, severity, type, summary, detail, file_line, proof, suggested_fix, reviewer, timestamp id, severity, type, summary, detail, file_line, proof, suggested_fix, reviewer, timestamp
</action> </action>
<action>Validation contract: <action>If findings file missing or malformed: HALT with explicit error and return to step 3 generation</action>
- `file_line` is the required `file:line` locator in `path/to/file:line` format
- Reject non-array JSON, missing required keys, or invalid file_line formatting
- If findings file missing/unreadable/malformed: HALT with explicit error and return to step 3 generation
</action>
<action>Categorize findings: HIGH (must fix), MEDIUM (should fix), LOW (nice to fix)</action> <action>Categorize findings: HIGH (must fix), MEDIUM (should fix), LOW (nice to fix)</action>
<action>Set {{fixed_count}} = 0</action> <action>Set {{fixed_count}} = 0</action>
<action>Set {{action_count}} = 0</action> <action>Set {{action_count}} = 0</action>

View File

@ -3,22 +3,6 @@ name: code-review
description: "Perform an adversarial senior developer code review with concrete findings across quality, tests, architecture, security, and performance" description: "Perform an adversarial senior developer code review with concrete findings across quality, tests, architecture, security, and performance"
main_config: '{project-root}/_bmad/bmm/config.yaml' main_config: '{project-root}/_bmad/bmm/config.yaml'
web_bundle: false web_bundle: false
input_file_patterns:
architecture:
description: "System architecture for review context"
whole: "{planning_artifacts}/*architecture*.md"
sharded: "{planning_artifacts}/*architecture*/*.md"
load_strategy: "FULL_LOAD"
ux_design:
description: "UX design specification (if UI review)"
whole: "{planning_artifacts}/*ux*.md"
sharded: "{planning_artifacts}/*ux*/*.md"
load_strategy: "FULL_LOAD"
epics:
description: "Epic and story requirements for review context"
whole: "{planning_artifacts}/*epic*.md"
sharded: "{planning_artifacts}/*epic*/*.md"
load_strategy: "FULL_LOAD"
--- ---
# Code Review Workflow # Code Review Workflow

View File

@ -3,35 +3,6 @@ name: correct-course
description: "Navigate significant changes during sprint execution by analyzing impact, proposing solutions, and routing for implementation" description: "Navigate significant changes during sprint execution by analyzing impact, proposing solutions, and routing for implementation"
main_config: '{project-root}/_bmad/bmm/config.yaml' main_config: '{project-root}/_bmad/bmm/config.yaml'
web_bundle: false web_bundle: false
input_file_patterns:
prd:
description: "Product requirements for impact analysis"
whole: "{planning_artifacts}/*prd*.md"
sharded: "{planning_artifacts}/*prd*/*.md"
load_strategy: "FULL_LOAD"
epics:
description: "All epics to analyze change impact"
whole: "{planning_artifacts}/*epic*.md"
sharded: "{planning_artifacts}/*epic*/*.md"
load_strategy: "FULL_LOAD"
architecture:
description: "System architecture and decisions"
whole: "{planning_artifacts}/*architecture*.md"
sharded: "{planning_artifacts}/*architecture*/*.md"
load_strategy: "FULL_LOAD"
ux_design:
description: "UX design specification (if UI impacts)"
whole: "{planning_artifacts}/*ux*.md"
sharded: "{planning_artifacts}/*ux*/*.md"
load_strategy: "FULL_LOAD"
tech_spec:
description: "Technical specification"
whole: "{planning_artifacts}/*tech-spec*.md"
load_strategy: "FULL_LOAD"
document_project:
description: "Brownfield project documentation (optional)"
sharded: "{project_knowledge}/index.md"
load_strategy: "INDEX_GUIDED"
--- ---
## Initialization ## Initialization
@ -47,24 +18,16 @@ input_file_patterns:
- `sprint_status` = `{implementation_artifacts}/sprint-status.yaml` - `sprint_status` = `{implementation_artifacts}/sprint-status.yaml`
- `date` (system-generated) - `date` (system-generated)
- `installed_path` = `{project-root}/_bmad/bmm/workflows/4-implementation/correct-course` - `installed_path` = `{project-root}/_bmad/bmm/workflows/4-implementation/correct-course`
- `source_path` = `{project-root}/src/bmm/workflows/4-implementation/correct-course`
- Note: `installed_path` targets the installed runtime tree under `_bmad/...`; `source_path` is the repository authoring path.
- `default_output_file` = `{planning_artifacts}/sprint-change-proposal-{date}.md` - `default_output_file` = `{planning_artifacts}/sprint-change-proposal-{date}.md`
<workflow> <workflow>
<critical>Communicate all responses in {communication_language} and generate all documents in {document_output_language}</critical> <critical>Communicate all responses in {communication_language} and generate all documents in {document_output_language}</critical>
<step n="1" goal="Analyze changes and propose corrective actions"> <step n="1" goal="Analyze changes and propose corrective actions">
<action>Resolve workflow content path: <action>Read and follow instructions at: {installed_path}/instructions.md</action>
- If `{installed_path}/instructions.md` exists and is readable, set {{workflow_path}} = `{installed_path}`
- Else if `{source_path}/instructions.md` exists and is readable, set {{workflow_path}} = `{source_path}`
- Else emit an error listing both paths and HALT
</action>
<action>Read and follow instructions at: {{workflow_path}}/instructions.md</action>
</step> </step>
<step n="2" goal="Validate proposal quality"> <step n="2" goal="Validate proposal quality">
<action>If {{workflow_path}} is not set from step 1, repeat path resolution using checklist.md</action> <invoke-task>Validate against checklist at {installed_path}/checklist.md using {project-root}/_bmad/core/tasks/validate-workflow.md</invoke-task>
<invoke-task>Validate against checklist at {{workflow_path}}/checklist.md using {project-root}/_bmad/core/tasks/validate-workflow.md</invoke-task>
</step> </step>
</workflow> </workflow>

View File

@ -3,28 +3,6 @@ name: create-story
description: "Create the next user story from epics+stories with enhanced context analysis and direct ready-for-dev marking" description: "Create the next user story from epics+stories with enhanced context analysis and direct ready-for-dev marking"
main_config: '{project-root}/_bmad/bmm/config.yaml' main_config: '{project-root}/_bmad/bmm/config.yaml'
web_bundle: false web_bundle: false
input_file_patterns:
prd:
description: "PRD (fallback - epics file should have most content)"
whole: "{planning_artifacts}/*prd*.md"
sharded: "{planning_artifacts}/*prd*/*.md"
load_strategy: "SELECTIVE_LOAD"
architecture:
description: "Architecture (fallback - epics file should have relevant sections)"
whole: "{planning_artifacts}/*architecture*.md"
sharded: "{planning_artifacts}/*architecture*/*.md"
load_strategy: "SELECTIVE_LOAD"
ux:
description: "UX design (fallback - epics file should have relevant sections)"
whole: "{planning_artifacts}/*ux*.md"
sharded: "{planning_artifacts}/*ux*/*.md"
load_strategy: "SELECTIVE_LOAD"
epics:
description: "Enhanced epics+stories file with BDD and source hints"
whole: "{planning_artifacts}/*epic*.md"
sharded_index: "{planning_artifacts}/*epic*/index.md"
sharded_single: "{planning_artifacts}/*epic*/epic-{{epic_num}}.md"
load_strategy: "SELECTIVE_LOAD"
--- ---
## Initialization ## Initialization
@ -157,7 +135,7 @@ input_file_patterns:
</step> </step>
<step n="2" goal="Load and analyze core artifacts"> <step n="2" goal="Load and analyze core artifacts">
<critical>🔬 EXHAUSTIVE ARTIFACT ANALYSIS - This is where you prevent future developer mistakes!</critical> <critical>🔬 EXHAUSTIVE ARTIFACT ANALYSIS - This is where you prevent future developer fuckups!</critical>
<!-- Load all available content through discovery protocol --> <!-- Load all available content through discovery protocol -->
<invoke-protocol <invoke-protocol

View File

@ -10,14 +10,8 @@ nextStepFile: './step-09-mark-review-ready.md'
<action>Initialize review-tracking variables before checks: <action>Initialize review-tracking variables before checks:
- If {{resolved_review_items}} is undefined: set {{resolved_review_items}} = [] - If {{resolved_review_items}} is undefined: set {{resolved_review_items}} = []
- If {{unresolved_review_items}} is undefined: set {{unresolved_review_items}} = [] - If {{unresolved_review_items}} is undefined: set {{unresolved_review_items}} = []
- Set {{review_continuation}} = false - Set {{review_continuation}} by checking current task title/original task list for prefix "[AI-Review]"
- If current {{task_title}} starts with "[AI-Review]", set {{review_continuation}} = true - Set {{date}} from system-generated timestamp in project date format
- Else scan {{original_task_list}}; if any item starts with "[AI-Review]", set {{review_continuation}} = true
- Set {{date}} from system-generated timestamp formatted for project change log entries
- Set {{resolved_count}} = length({{resolved_review_items}})
- Set {{review_match_threshold}} = 0.60
- Define normalize(text): lowercase, trim, remove "[AI-Review]" prefix and punctuation, collapse whitespace
- Define token_set(text): unique whitespace-separated normalized tokens
</action> </action>
<!-- VALIDATION GATES --> <!-- VALIDATION GATES -->
@ -29,58 +23,28 @@ nextStepFile: './step-09-mark-review-ready.md'
<!-- REVIEW FOLLOW-UP HANDLING --> <!-- REVIEW FOLLOW-UP HANDLING -->
<check if="task is review follow-up (has [AI-Review] prefix)"> <check if="task is review follow-up (has [AI-Review] prefix)">
<action>Extract review item details (severity, description, related AC/file)</action> <action>Extract review item details (severity, description, related AC/file)</action>
<action>Load all items from "Senior Developer Review (AI) → Action Items" as candidate list {{review_action_items}}</action> <action>Add current review task to resolution tracking list: append structured entry to {{resolved_review_items}}</action>
<action>Set {{task_text_norm}} = normalize(current review follow-up task description)</action>
<action>Initialize {{best_match}} = null, {{best_score}} = 0, {{best_shared_tokens}} = 0, {{tie_candidates}} = []</action>
<action>For each candidate action item:
1. Set {{candidate_text_norm}} = normalize(candidate text)
2. If {{task_text_norm}} == {{candidate_text_norm}} OR either contains the other:
- set {{candidate_score}} = 1.0 and mark as strong match
3. Else:
- compute Jaccard score = |token_set(task) ∩ token_set(candidate)| / |token_set(task) token_set(candidate)|
- set {{candidate_score}} to computed score
4. Track shared-token count for tie-breaking
5. Keep highest score candidate; if same score, keep candidate with more shared tokens
6. If score and shared-token count both tie, add candidate to {{tie_candidates}}
</action>
<action>Set {{match_found}} = true only if {{best_score}} >= {{review_match_threshold}}</action>
<!-- Mark task in Review Follow-ups section (always, regardless of action-item match result) --> <!-- Mark task in Review Follow-ups section -->
<action>Mark task checkbox [x] in "Tasks/Subtasks → Review Follow-ups (AI)" section</action> <action>Mark task checkbox [x] in "Tasks/Subtasks → Review Follow-ups (AI)" section</action>
<check if="{{match_found}} == true"> <!-- CRITICAL: Also mark corresponding action item in review section -->
<action>Mark matched action item checkbox [x] in "Senior Developer Review (AI) → Action Items"</action> <action>Find matching action item in "Senior Developer Review (AI) → Action Items" using fuzzy matching:
<action>Append structured entry to {{resolved_review_items}}: 1. Normalize strings (lowercase, trim, remove "[AI-Review]" prefix/punctuation)
- task: current review follow-up task 2. Try exact and substring matches first
- matched_action_item: {{best_match}} 3. If none, compute token-overlap/Jaccard score per candidate
- match_score: {{best_score}} 4. Select highest-scoring candidate when score >= 0.60
- resolved_at: {{date}} 5. If tie at best score, prefer the candidate with more shared tokens; log ambiguity
- status: "matched"
</action> </action>
<check if="{{tie_candidates}} is not empty"> <check if="matching action item found">
<action>Log ambiguity warning with tied candidates and selected best_match</action> <action>Mark that action item checkbox [x] as resolved</action>
</check> </check>
<action>Add to Dev Agent Record → Completion Notes: "✅ Resolved review finding [{{severity}}]: {{description}} (matched action item, score {{best_score}})"</action> <check if="no candidate meets threshold">
<action>Log warning and append task to {{unresolved_review_items}}</action>
<action>Add resolution note in Dev Agent Record that no corresponding action item was found</action>
</check> </check>
<check if="{{match_found}} == false"> <action>Add to Dev Agent Record → Completion Notes: "✅ Resolved review finding [{{severity}}]: {{description}}"</action>
<action>Log warning: no candidate met threshold {{review_match_threshold}} for task "{{task_text_norm}}"</action>
<action>Append structured entry to {{resolved_review_items}}:
- task: current review follow-up task
- matched_action_item: null
- match_score: {{best_score}}
- resolved_at: {{date}}
- status: "unmatched"
</action>
<action>Append structured entry to {{unresolved_review_items}}:
- task: current review follow-up task
- reason: "No corresponding action item met fuzzy-match threshold"
- best_candidate: {{best_match}}
- best_score: {{best_score}}
- recorded_at: {{date}}
</action>
<action>Add resolution note in Dev Agent Record that no corresponding action item was found, while follow-up checkbox was still marked complete</action>
</check>
</check> </check>
<!-- ONLY MARK COMPLETE IF ALL VALIDATION PASS --> <!-- ONLY MARK COMPLETE IF ALL VALIDATION PASS -->
@ -92,12 +56,7 @@ nextStepFile: './step-09-mark-review-ready.md'
<check if="ANY validation fails"> <check if="ANY validation fails">
<action>DO NOT mark task complete - fix issues first</action> <action>DO NOT mark task complete - fix issues first</action>
<action>If unable to fix validation failures, invoke HALT protocol from dev-story/workflow.md with: <action>HALT if unable to fix validation failures</action>
- reason_code: DEV-STORY-STEP-08-VALIDATION-FAIL
- step_id: step-08-mark-task-complete
- message: "Task completion validation failed and remediation was unsuccessful."
- required_action: "Fix failing validations/tests, then resume."
</action>
</check> </check>
<check if="review_continuation == true and {{resolved_review_items}} is not empty"> <check if="review_continuation == true and {{resolved_review_items}} is not empty">

View File

@ -10,14 +10,9 @@ nextStepFile: './step-10-closeout.md'
<action>Confirm File List includes every changed file</action> <action>Confirm File List includes every changed file</action>
<action>Execute enhanced definition-of-done validation</action> <action>Execute enhanced definition-of-done validation</action>
<action>Update the story Status to: "review"</action> <action>Update the story Status to: "review"</action>
<action>Initialize sprint tracking state deterministically before any sprint-status check: <action>Initialize sprint tracking state:
- Set {{current_sprint_status}} = "no-sprint-tracking" - If {sprint_status} exists and is readable, load file and set {{current_sprint_status}} from tracking mode/content
- Set {{sprint_tracking_enabled}} = false - If file does not exist, unreadable, or indicates no sprint tracking, set {{current_sprint_status}} = "no-sprint-tracking"
- If {sprint_status} exists and is readable:
- Load the FULL file: {sprint_status}
- If file content indicates tracking disabled OR development_status section is missing, keep "no-sprint-tracking"
- Else set {{current_sprint_status}} = "enabled" and {{sprint_tracking_enabled}} = true
- If file missing/unreadable, keep defaults and continue with story-only status update
</action> </action>
<!-- Enhanced Definition of Done Validation --> <!-- Enhanced Definition of Done Validation -->
@ -36,31 +31,31 @@ nextStepFile: './step-10-closeout.md'
</action> </action>
<!-- Mark story ready for review - sprint status conditional --> <!-- Mark story ready for review - sprint status conditional -->
<check if="{{sprint_tracking_enabled}} == true"> <check if="{sprint_status} file exists AND {{current_sprint_status}} != 'no-sprint-tracking'">
<action>Load the FULL file: {sprint_status}</action>
<action>Find development_status key matching {{story_key}}</action> <action>Find development_status key matching {{story_key}}</action>
<check if="story key found in sprint status">
<action>Verify current status is "in-progress" (expected previous state)</action> <action>Verify current status is "in-progress" (expected previous state)</action>
<action>Update development_status[{{story_key}}] = "review"</action> <action>Update development_status[{{story_key}}] = "review"</action>
<action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action> <action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action>
<output>✅ Story status updated to "review" in sprint-status.yaml</output> <output>✅ Story status updated to "review" in sprint-status.yaml</output>
</check> </check>
<check if="{sprint_status} file does NOT exist OR {{current_sprint_status}} == 'no-sprint-tracking'">
<output> Story status updated to "review" in story file (no sprint tracking configured)</output>
</check>
<check if="story key not found in sprint status"> <check if="story key not found in sprint status">
<output>⚠️ Story file updated, but sprint-status update failed: {{story_key}} not found <output>⚠️ Story file updated, but sprint-status update failed: {{story_key}} not found
Story status is set to "review" in file, but sprint-status.yaml may be out of sync. Story status is set to "review" in file, but sprint-status.yaml may be out of sync.
</output> </output>
</check> </check>
</check>
<check if="{{sprint_tracking_enabled}} == false">
<output> Story status updated to "review" in story file (no sprint tracking configured)</output>
</check>
<!-- Final validation gates --> <!-- Final validation gates -->
<action if="any task is incomplete">Invoke HALT protocol (reason_code: DEV-STORY-STEP-09-INCOMPLETE-TASKS, step_id: step-09-mark-review-ready, message: "Incomplete tasks remain before review-ready transition.", required_action: "Complete all tasks/subtasks and rerun validations.")</action> <action if="any task is incomplete">HALT - Complete remaining tasks before marking ready for review</action>
<action if="regression failures exist">Invoke HALT protocol (reason_code: DEV-STORY-STEP-09-REGRESSION-FAIL, step_id: step-09-mark-review-ready, message: "Regression suite has failures.", required_action: "Fix failing tests and rerun full regression suite.")</action> <action if="regression failures exist">HALT - Fix regression issues before completing</action>
<action if="File List is incomplete">Invoke HALT protocol (reason_code: DEV-STORY-STEP-09-FILE-LIST-INCOMPLETE, step_id: step-09-mark-review-ready, message: "File List does not include all changed files.", required_action: "Update File List with all added/modified/deleted paths.")</action> <action if="File List is incomplete">HALT - Update File List with all changed files</action>
<action if="definition-of-done validation fails">Invoke HALT protocol (reason_code: DEV-STORY-STEP-09-DOD-FAIL, step_id: step-09-mark-review-ready, message: "Definition-of-done checks failed.", required_action: "Address DoD failures and rerun validation.")</action> <action if="definition-of-done validation fails">HALT - Address DoD failures before completing</action>
</step> </step>
## Next ## Next

View File

@ -2,7 +2,7 @@
name: dev-story name: dev-story
description: "Execute a story by implementing tasks/subtasks, writing tests, validating, and updating the story file per acceptance criteria" description: "Execute a story by implementing tasks/subtasks, writing tests, validating, and updating the story file per acceptance criteria"
projectRoot: '{project-root}' projectRoot: '{project-root}'
main_config: '{project-root}/_bmad/bmm/config.yaml' main_config: '{projectRoot}/_bmad/bmm/config.yaml'
web_bundle: false web_bundle: false
--- ---
@ -45,36 +45,22 @@ Implement a ready story end-to-end with strict validation gates, accurate progre
- Change Log - Change Log
- Status - Status
- Execute steps in order and do not skip validation gates. - Execute steps in order and do not skip validation gates.
- Continue until the story is complete unless the HALT protocol below is triggered. - Continue until the story is complete unless a defined HALT condition triggers.
## HALT Protocol (Normative) ## HALT Definition
- Scope: - HALT triggers:
- Every `HALT` instruction in this workflow and all `steps/*.md` files MUST use this protocol. - Required inputs/files are missing or unreadable.
- Operational definition: - Validation gates fail and cannot be remediated in current step.
- HALT is a deterministic hard-stop event raised when execution cannot safely continue. - Test/regression failures persist after fix attempts.
- A HALT event MUST include: - Story state becomes inconsistent (e.g., malformed task structure preventing safe updates).
- `reason_code` (stable machine-readable code)
- `step_id` (current step file + step number)
- `message` (human-readable failure summary)
- `required_action` (what user/operator must do before resume)
- Trigger criteria:
- Required inputs/files are missing, unreadable, or malformed.
- Validation gates fail and cannot be remediated in the current step.
- Test/regression failures persist after attempted fixes.
- Story state is inconsistent (for example malformed task structure preventing safe updates).
- HALT behavior: - HALT behavior:
- Stop execution immediately and skip all downstream steps. - Stop executing further steps immediately.
- Persist workflow checkpoint: current step id, resolved variables, and pending task context. - Persist current story-file edits and workflow state safely.
- Persist only already-applied safe edits; do not apply new partial completion marks after HALT. - Emit explicit user-facing error message describing trigger and remediation needed.
- Emit logger event exactly in this format: - Do not apply partial completion marks after HALT.
- `HALT[{reason_code}] step={step_id} story={story_key|unknown} detail=\"{message}\"`
- Emit user-facing prompt exactly in this format:
- `Workflow HALTED at {step_id} ({reason_code}): {message}. Required action: {required_action}. Reply RESUME after remediation.`
- Resume semantics: - Resume semantics:
- Manual resume only (no automatic retry loop). - Manual resume only after user confirms the blocking issue is resolved.
- Resume is checkpoint-based: restart from the halted step after user confirms remediation. - Resume from the last incomplete step checkpoint, re-running validations before progressing.
- Re-run the failed validation/input check before executing further actions.
- If the same HALT condition repeats, stop again with updated evidence.
## Execution ## Execution
Read fully and follow: `steps/step-01-find-story.md`. Read fully and follow: `steps/step-01-find-story.md`.

View File

@ -3,32 +3,6 @@ name: retrospective
description: "Run after epic completion to review overall success, extract lessons learned, and surface impact on upcoming work" description: "Run after epic completion to review overall success, extract lessons learned, and surface impact on upcoming work"
main_config: '{project-root}/_bmad/bmm/config.yaml' main_config: '{project-root}/_bmad/bmm/config.yaml'
web_bundle: false web_bundle: false
agent_manifest: '{project-root}/_bmad/_config/agent-manifest.csv'
input_file_patterns:
epics:
description: "The completed epic for retrospective"
whole: "{planning_artifacts}/*epic*.md"
sharded_index: "{planning_artifacts}/*epic*/index.md"
sharded_single: "{planning_artifacts}/*epic*/epic-{{epic_num}}.md"
load_strategy: "SELECTIVE_LOAD"
previous_retrospective:
description: "Previous epic's retrospective (optional)"
pattern: "{implementation_artifacts}/**/epic-{{prev_epic_num}}-retro-*.md"
load_strategy: "SELECTIVE_LOAD"
architecture:
description: "System architecture for context"
whole: "{planning_artifacts}/*architecture*.md"
sharded: "{planning_artifacts}/*architecture*/*.md"
load_strategy: "FULL_LOAD"
prd:
description: "Product requirements for context"
whole: "{planning_artifacts}/*prd*.md"
sharded: "{planning_artifacts}/*prd*/*.md"
load_strategy: "FULL_LOAD"
document_project:
description: "Brownfield project documentation (optional)"
sharded: "{planning_artifacts}/*.md"
load_strategy: "INDEX_GUIDED"
--- ---
## Initialization ## Initialization
@ -40,7 +14,6 @@ input_file_patterns:
- `user_skill_level` - `user_skill_level`
- `planning_artifacts` - `planning_artifacts`
- `implementation_artifacts` - `implementation_artifacts`
- `agent_manifest` = `{project-root}/_bmad/_config/agent-manifest.csv`
- `sprint_status_file` = `{implementation_artifacts}/sprint-status.yaml` - `sprint_status_file` = `{implementation_artifacts}/sprint-status.yaml`
- `story_directory` = `{implementation_artifacts}` - `story_directory` = `{implementation_artifacts}`
- `retrospectives_folder` = `{implementation_artifacts}` - `retrospectives_folder` = `{implementation_artifacts}`

View File

@ -3,14 +3,6 @@ name: sprint-planning
description: "Generate and manage the sprint status tracking file for Phase 4 implementation, extracting all epics and stories from epic files and tracking their status through the development lifecycle" description: "Generate and manage the sprint status tracking file for Phase 4 implementation, extracting all epics and stories from epic files and tracking their status through the development lifecycle"
main_config: '{project-root}/_bmad/bmm/config.yaml' main_config: '{project-root}/_bmad/bmm/config.yaml'
web_bundle: false web_bundle: false
epics_location: "{planning_artifacts}"
epics_pattern: "epic*.md"
input_file_patterns:
epics:
description: "All epics with user stories"
whole: "{epics_location}/{epics_pattern}"
sharded: "{epics_location}/epic*/*.md"
load_strategy: "FULL_LOAD"
--- ---
## Initialization ## Initialization
@ -22,8 +14,6 @@ input_file_patterns:
- `planning_artifacts` - `planning_artifacts`
- `project_name` - `project_name`
- `date` (system-generated) - `date` (system-generated)
- `epics_location` = `{planning_artifacts}`
- `epics_pattern` = `epic*.md`
- `installed_path` = `{project-root}/_bmad/bmm/workflows/4-implementation/sprint-planning` - `installed_path` = `{project-root}/_bmad/bmm/workflows/4-implementation/sprint-planning`
- `status_file` = `{implementation_artifacts}/sprint-status.yaml` - `status_file` = `{implementation_artifacts}/sprint-status.yaml`
- `default_output_file` = `{status_file}` - `default_output_file` = `{status_file}`

View File

@ -10,26 +10,10 @@
<step n="1" goal="Validate workflow and get project info"> <step n="1" goal="Validate workflow and get project info">
<action>Initialize status defaults: <invoke-workflow path="{project-root}/_bmad/bmm/workflows/workflow-status">
- Set status_exists = false <param>mode: data</param>
- Set status_file_found = false <param>data_request: project_config</param>
- Set standalone_mode = true </invoke-workflow>
- Set warning = ""
- Set suggestion = ""
- Set next_workflow = ""
- Set next_agent = ""
</action>
<action>Attempt to load workflow status directly from `{output_folder}/bmm-workflow-status.yaml`:
- If file exists, is readable, and parses correctly:
- Set status_exists = true
- Set status_file_found = true
- Set standalone_mode = false
- Set status_file_path = `{output_folder}/bmm-workflow-status.yaml`
- Extract field_type, warning, suggestion, next_workflow, next_agent if present
- If file is missing, unreadable, or malformed:
- Keep defaults and continue in standalone mode
</action>
<check if="status_exists == false"> <check if="status_exists == false">
<output>{{suggestion}}</output> <output>{{suggestion}}</output>
@ -51,11 +35,11 @@
</check> </check>
</check> </check>
<!-- Validate sequencing from loaded status fields --> <!-- Now validate sequencing -->
<action>Validate sequencing locally from loaded status fields: <invoke-workflow path="{project-root}/_bmad/bmm/workflows/workflow-status">
- If warning is empty, continue <param>mode: validate</param>
- If warning contains guidance, require explicit user confirmation before continuing <param>calling_workflow: document-project</param>
</action> </invoke-workflow>
<check if="warning != ''"> <check if="warning != ''">
<output>{{warning}}</output> <output>{{warning}}</output>
@ -74,23 +58,12 @@
<critical>SMART LOADING STRATEGY: Check state file FIRST before loading any CSV files</critical> <critical>SMART LOADING STRATEGY: Check state file FIRST before loading any CSV files</critical>
<action>Check for existing state file at: {output_folder}/project-scan-report.json</action> <action>Check for existing state file at: {output_folder}/project-scan-report.json</action>
<action>Set resume_mode = false</action>
<check if="project-scan-report.json exists"> <check if="project-scan-report.json exists">
<action>Read state file and extract: timestamps, mode, scan_level, current_step, completed_steps, project_classification</action> <action>Read state file and extract: timestamps, mode, scan_level, current_step, completed_steps, project_classification</action>
<action>Extract cached project_type_id(s) from state file if present</action> <action>Extract cached project_type_id(s) from state file if present</action>
<action>Calculate age of state file (current time - last_updated)</action> <action>Calculate age of state file (current time - last_updated)</action>
<check if="state file age >= 24 hours">
<action>Display: "Found old state file (>24 hours). Starting fresh scan."</action>
<action>Create archive directory: {output_folder}/.archive/</action>
<action>Archive old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
<action>Set resume_mode = false</action>
<action>Continue to Step 3</action>
</check>
<check if="state file age < 24 hours">
<ask>I found an in-progress workflow state from {{last_updated}}. <ask>I found an in-progress workflow state from {{last_updated}}.
**Current Progress:** **Current Progress:**
@ -113,7 +86,6 @@ Your choice [1/2/3]:
<check if="user selects 1"> <check if="user selects 1">
<action>Set resume_mode = true</action> <action>Set resume_mode = true</action>
<action>Set workflow_mode = {{mode}}</action> <action>Set workflow_mode = {{mode}}</action>
<action>Set subworkflow_success = false</action>
<action>Load findings summaries from state file</action> <action>Load findings summaries from state file</action>
<action>Load cached project_type_id(s) from state file</action> <action>Load cached project_type_id(s) from state file</action>
@ -126,27 +98,10 @@ Your choice [1/2/3]:
<check if="workflow_mode == deep_dive"> <check if="workflow_mode == deep_dive">
<action>Read fully and follow: {installed_path}/workflows/deep-dive-instructions.md with resume context</action> <action>Read fully and follow: {installed_path}/workflows/deep-dive-instructions.md with resume context</action>
<action>Set subworkflow_success = true only if delegated workflow completed without HALT/error</action>
<check if="subworkflow_success != true">
<output>Sub-workflow failed or was aborted during resume deep-dive mode. Exiting without marking completion.</output>
<action>Exit workflow</action>
</check>
<action>After sub-workflow completes, continue to Step 4</action>
</check> </check>
<check if="workflow_mode == initial_scan OR workflow_mode == full_rescan"> <check if="workflow_mode == initial_scan OR workflow_mode == full_rescan">
<action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md with resume context</action> <action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md with resume context</action>
<action>Set subworkflow_success = true only if delegated workflow completed without HALT/error</action>
<check if="subworkflow_success != true">
<output>Sub-workflow failed or was aborted during resume full-scan mode. Exiting without marking completion.</output>
<action>Exit workflow</action>
</check>
<action>After sub-workflow completes, continue to Step 4</action>
</check>
<check if="workflow_mode != deep_dive AND workflow_mode != initial_scan AND workflow_mode != full_rescan">
<output>Invalid resume workflow mode '{{workflow_mode}}'. Exiting without marking completion.</output>
<action>Exit workflow</action>
</check> </check>
</check> </check>
@ -155,7 +110,7 @@ Your choice [1/2/3]:
<action>Create archive directory: {output_folder}/.archive/</action> <action>Create archive directory: {output_folder}/.archive/</action>
<action>Move old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action> <action>Move old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
<action>Set resume_mode = false</action> <action>Set resume_mode = false</action>
<action>Continue to Step 3</action> <action>Continue to Step 0.5</action>
</check> </check>
<check if="user selects 3"> <check if="user selects 3">
@ -163,18 +118,12 @@ Your choice [1/2/3]:
<action>Exit workflow</action> <action>Exit workflow</action>
</check> </check>
<check if="user input is not 1, 2, or 3"> <check if="state file age >= 24 hours">
<output>Invalid selection. Expected 1, 2, or 3. Exiting workflow without changes.</output> <action>Display: "Found old state file (>24 hours). Starting fresh scan."</action>
<action>Exit workflow</action> <action>Archive old state file to: {output_folder}/.archive/project-scan-report-{{timestamp}}.json</action>
</check>
</check>
</check>
<check if="project-scan-report.json does not exist">
<action>Set resume_mode = false</action> <action>Set resume_mode = false</action>
<action>Continue to Step 3</action> <action>Continue to Step 0.5</action>
</check> </check>
</step> </step>
@ -200,11 +149,6 @@ Your choice [1/2/3]:
<action>Set workflow_mode = "full_rescan"</action> <action>Set workflow_mode = "full_rescan"</action>
<action>Display: "Starting full project rescan..."</action> <action>Display: "Starting full project rescan..."</action>
<action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md</action> <action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md</action>
<action>Set subworkflow_success = true only if delegated workflow completed without HALT/error</action>
<check if="subworkflow_success != true">
<output>Sub-workflow failed or was aborted during full rescan. Exiting without marking completion.</output>
<action>Exit workflow</action>
</check>
<action>After sub-workflow completes, continue to Step 4</action> <action>After sub-workflow completes, continue to Step 4</action>
</check> </check>
@ -213,11 +157,6 @@ Your choice [1/2/3]:
<action>Set scan_level = "exhaustive"</action> <action>Set scan_level = "exhaustive"</action>
<action>Display: "Starting deep-dive documentation mode..."</action> <action>Display: "Starting deep-dive documentation mode..."</action>
<action>Read fully and follow: {installed_path}/workflows/deep-dive-instructions.md</action> <action>Read fully and follow: {installed_path}/workflows/deep-dive-instructions.md</action>
<action>Set subworkflow_success = true only if delegated workflow completed without HALT/error</action>
<check if="subworkflow_success != true">
<output>Sub-workflow failed or was aborted during deep-dive mode. Exiting without marking completion.</output>
<action>Exit workflow</action>
</check>
<action>After sub-workflow completes, continue to Step 4</action> <action>After sub-workflow completes, continue to Step 4</action>
</check> </check>
@ -225,22 +164,12 @@ Your choice [1/2/3]:
<action>Display message: "Keeping existing documentation. Exiting workflow."</action> <action>Display message: "Keeping existing documentation. Exiting workflow."</action>
<action>Exit workflow</action> <action>Exit workflow</action>
</check> </check>
<check if="user input is not 1, 2, or 3">
<output>Invalid selection. Expected 1, 2, or 3. Exiting workflow without changes.</output>
<action>Exit workflow</action>
</check>
</check> </check>
<check if="index.md does not exist"> <check if="index.md does not exist">
<action>Set workflow_mode = "initial_scan"</action> <action>Set workflow_mode = "initial_scan"</action>
<action>Display: "No existing documentation found. Starting initial project scan..."</action> <action>Display: "No existing documentation found. Starting initial project scan..."</action>
<action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md</action> <action>Read fully and follow: {installed_path}/workflows/full-scan-instructions.md</action>
<action>Set subworkflow_success = true only if delegated workflow completed without HALT/error</action>
<check if="subworkflow_success != true">
<output>Sub-workflow failed or was aborted during initial scan. Exiting without marking completion.</output>
<action>Exit workflow</action>
</check>
<action>After sub-workflow completes, continue to Step 4</action> <action>After sub-workflow completes, continue to Step 4</action>
</check> </check>
@ -248,25 +177,16 @@ Your choice [1/2/3]:
<step n="4" goal="Update status and complete"> <step n="4" goal="Update status and complete">
<check if="resume_mode == true AND subworkflow_success != true">
<output>Resume flow did not complete a delegated sub-workflow successfully. Exiting without completion update.</output>
<action>Exit workflow</action>
</check>
<check if="status_file_found == true"> <check if="status_file_found == true">
<action>Attempt status update in {{status_file_path}}: <invoke-workflow path="{project-root}/_bmad/bmm/workflows/workflow-status">
- Mark workflow `document-project` as completed <param>mode: update</param>
- Persist updated timestamp and completion metadata <param>action: complete_workflow</param>
- Set status_update_success = true on success <param>workflow_name: document-project</param>
- If write fails, set status_update_success = false and capture status_update_error </invoke-workflow>
</action>
<check if="status_update_success == true"> <check if="success == true">
<output>Status updated!</output> <output>Status updated!</output>
</check> </check>
<check if="status_update_success != true">
<output>⚠️ Status update skipped: {{status_update_error}}</output>
</check>
</check> </check>
<output>**✅ Document Project Workflow Complete, {user_name}!** <output>**✅ Document Project Workflow Complete, {user_name}!**
@ -276,21 +196,25 @@ Your choice [1/2/3]:
- Mode: {{workflow_mode}} - Mode: {{workflow_mode}}
- Scan Level: {{scan_level}} - Scan Level: {{scan_level}}
- Output: {output_folder}/index.md and related files - Output: {output_folder}/index.md and related files
</output>
<check if="status_file_found == true AND status_update_success == true"> {{#if status_file_found}}
<output>**Status Updated:** Progress tracking updated. **Status Updated:**
- Progress tracking updated
**Next Steps:** **Next Steps:**
- **Next required:** {{next_workflow}} ({{next_agent}} agent)
- Run `bmad-help` if you need recommended next workflows.</output>
</check>
<check if="status_file_found == false OR status_update_success != true"> - **Next required:** {{next_workflow}} ({{next_agent}} agent)
<output>**Next Steps:**
Check status anytime with: `workflow-status`
{{else}}
**Next Steps:**
Since no workflow is in progress:
- Refer to the BMM workflow guide if unsure what to do next - Refer to the BMM workflow guide if unsure what to do next
- Run `bmad-help` to get guided workflow recommendations</output> - Or run `workflow-init` to create a workflow path and get guided next steps
</check> {{/if}}
</output>
</step> </step>

View File

@ -35,15 +35,13 @@ Determine what was just completed:
1. **Load catalog** — Load `{project-root}/_bmad/_config/bmad-help.csv` 1. **Load catalog** — Load `{project-root}/_bmad/_config/bmad-help.csv`
2. **Resolve output locations and config** — Scan each folder under `_bmad/` (except `_config`) for `config.yaml`. For each workflow row, resolve its `output-location` variables against that module's config so artifact paths can be searched. Also extract `communication_language` and `project_knowledge` from each scanned module's config. 2. **Resolve output locations** — Scan each folder under `_bmad/` (except `_config`) for `config.yaml`. For each workflow row, resolve its `output-location` variables against that module's config so artifact paths can be searched.
3. **Ground in project knowledge** — If `project_knowledge` resolves to an existing path, read available documentation files (architecture docs, project overview, tech stack references) for grounding context. Use discovered project facts when composing any project-specific output. Never fabricate project-specific details. If documentation is unavailable, state that clearly. 3. **Analyze input** — Task may provide a workflow name/code, conversational phrase, or nothing. Infer what was just completed using INPUT ANALYSIS above.
4. **Analyze input** — Task may provide a workflow name/code, conversational phrase, or nothing. Infer what was just completed using INPUT ANALYSIS above. 4. **Detect active module** — Use MODULE DETECTION above to determine which module the user is working in.
5. **Detect active module** — Use MODULE DETECTION above to determine which module the user is working in. 5. **Present recommendations** — Show next steps based on completed workflows, phase/sequence ordering (KEY RULES), and artifact detection. Format per the following
6. **Present recommendations** — Show next steps based on completed workflows, phase/sequence ordering (KEY RULES), and artifact detection. Format per the following
## RECOMMENDED OUTPUT FORMAT ## RECOMMENDED OUTPUT FORMAT
@ -51,28 +49,14 @@ Determine what was just completed:
**Required items next** — List the next required workflow **Required items next** — List the next required workflow
For each item show: For each item show:
- Workflow **name** - Workflow **name**
- **Command** when `command` is present - **Command** (use the catalog command name; present it in your platform's command format, e.g., `bmad-example-build-prototype`)
- **Agent load instruction + code** when `command` is empty
- **Agent** title and display name from the CSV (e.g., "🎨 Alex (Designer)") - **Agent** title and display name from the CSV (e.g., "🎨 Alex (Designer)")
- Brief **description** - Brief **description**
### Display branching rules
- When `command` has a value:
- Present the command in your platform's command format (for example: `bmad-example-build-prototype`)
- When `command` is empty:
- Do not invent a slash command
- Show how to load the agent from `agent` and then invoke using the workflow/code description
- Example:
- `Explain Concept (EC)`
- `Load: /tech-writer, then ask to "EC about [topic]"`
- `Agent: Tech Writer`
- `Description: Create clear technical explanations with examples`
### Additional response output guidance to convey: ### Additional response output guidance to convey:
- Present all output in `{communication_language}` when available in module config
- Run each workflow in a **fresh context window** - Run each workflow in a **fresh context window**
- Load the agent using the platform's command format for `agent-command`, or run the workflow command directly - Load the agent using the platform's command format for `agent-command`, or run the workflow command directly
- For **validation workflows**: recommend using a different high-quality LLM if available - For **validation workflows**: recommend using a different high-quality LLM if available
- For conversational requests: match the user's tone while presenting clearly - For conversational requests: match the user's tone while presenting clearly
7. Return to the calling process after presenting recommendations. 6. Return to the calling process after presenting recommendations.

View File

@ -13,8 +13,8 @@ standalone: true
- Parse YAML and fail fast with explicit error if parsing fails. - Parse YAML and fail fast with explicit error if parsing fails.
- Require `user_name`; if missing, abort initialization with descriptive error. - Require `user_name`; if missing, abort initialization with descriptive error.
- Apply explicit defaults when optional keys are absent: - Apply explicit defaults when optional keys are absent:
- `communication_language = "English"` - `communication_language = "en"`
- `document_output_language = "English"` - `document_output_language = "en"`
- Log resolved values and config source path. - Log resolved values and config source path.
## Purpose ## Purpose

View File

@ -28,7 +28,7 @@ When called during template workflow processing:
<workflow> <workflow>
<step n="1" title="Method Registry Loading"> <step n="1" title="Method Registry Loading">
<action>Load and read {{methods}} and {{agent_manifest}}</action> <action>Load and read {{methods}} and {{agent-party}}</action>
<csv-structure> <csv-structure>
<i>category: Method grouping (core, structural, risk, etc.)</i> <i>category: Method grouping (core, structural, risk, etc.)</i>

View File

@ -1,3 +0,0 @@
module,phase,name,workflow-file,description
bmm,anytime,Document,,Analyze project
bmm,1-analysis,Brainstorm,,Brainstorm ideas
1 module phase name workflow-file description
2 bmm anytime Document Analyze project
3 bmm 1-analysis Brainstorm Brainstorm ideas

View File

@ -1 +0,0 @@
module,phase,name,workflow-file,description
1 module phase name workflow-file description

View File

@ -1,3 +0,0 @@
name,code,description,agent
brainstorm,BSP,"Generate ideas",analyst
party,PM,"Multi-agent",facilitator
1 name code description agent
2 brainstorm BSP Generate ideas analyst
3 party PM Multi-agent facilitator

View File

@ -1,3 +0,0 @@
module,phase,name,workflow-file,description
bmm,anytime,Template Var,{output_folder}/something.md,Has unresolvable template var
bmm,anytime,Normal Ref,_bmad/core/tasks/help.md,Normal resolvable ref
1 module phase name workflow-file description
2 bmm anytime Template Var {output_folder}/something.md Has unresolvable template var
3 bmm anytime Normal Ref _bmad/core/tasks/help.md Normal resolvable ref

View File

@ -1,3 +0,0 @@
module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs,
bmm,anytime,Document Project,DP,,_bmad/bmm/workflows/document-project/workflow.yaml,bmad-bmm-document-project,false,analyst,Create Mode,"Analyze project",project-knowledge,*,
bmm,1-analysis,Brainstorm Project,BP,10,_bmad/core/workflows/brainstorming/workflow.md,bmad-brainstorming,false,analyst,data=template.md,"Brainstorming",planning_artifacts,"session",
1 module phase name code sequence workflow-file command required agent options description output-location outputs
2 bmm anytime Document Project DP _bmad/bmm/workflows/document-project/workflow.yaml bmad-bmm-document-project false analyst Create Mode Analyze project project-knowledge *
3 bmm 1-analysis Brainstorm Project BP 10 _bmad/core/workflows/brainstorming/workflow.md bmad-brainstorming false analyst data=template.md Brainstorming planning_artifacts session

View File

@ -1,3 +0,0 @@
module,phase,name,code,sequence,workflow-file,command,required,agent,options,description,output-location,outputs
core,anytime,Brainstorming,BSP,,_bmad/core/workflows/brainstorming/workflow.md,bmad-brainstorming,false,analyst,,"Generate ideas",{output_folder}/brainstorming.md,
core,anytime,Party Mode,PM,,_bmad/core/workflows/party-mode/workflow.md,bmad-party-mode,false,facilitator,,"Multi-agent discussion",,
1 module phase name code sequence workflow-file command required agent options description output-location outputs
2 core anytime Brainstorming BSP _bmad/core/workflows/brainstorming/workflow.md bmad-brainstorming false analyst Generate ideas {output_folder}/brainstorming.md
3 core anytime Party Mode PM _bmad/core/workflows/party-mode/workflow.md bmad-party-mode false facilitator Multi-agent discussion

View File

@ -1,2 +0,0 @@
name,workflow-file,description
test,_bmad/core/tasks/help.md,A test entry
1 name workflow-file description
2 test _bmad/core/tasks/help.md A test entry

View File

@ -1,134 +0,0 @@
/**
* CSV File Reference Extraction Test Runner
*
* Tests extractCsvRefs() from validate-file-refs.js against fixtures.
* Verifies correct extraction of workflow-file references from CSV files.
*
* Usage: node test/test-file-refs-csv.js
* Exit codes: 0 = all tests pass, 1 = test failures
*/
const fs = require('node:fs');
const path = require('node:path');
const { extractCsvRefs } = require('../tools/validate-file-refs.js');
// ANSI color codes
const colors = {
reset: '\u001B[0m',
green: '\u001B[32m',
red: '\u001B[31m',
cyan: '\u001B[36m',
dim: '\u001B[2m',
};
const FIXTURES = path.join(__dirname, 'fixtures/file-refs-csv');
let totalTests = 0;
let passedTests = 0;
const failures = [];
function test(name, fn) {
totalTests++;
try {
fn();
passedTests++;
console.log(` ${colors.green}\u2713${colors.reset} ${name}`);
} catch (error) {
console.log(` ${colors.red}\u2717${colors.reset} ${name} ${colors.red}${error.message}${colors.reset}`);
failures.push({ name, message: error.message });
}
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
function loadFixture(relativePath) {
const fullPath = path.join(FIXTURES, relativePath);
const content = fs.readFileSync(fullPath, 'utf-8');
return { fullPath, content };
}
// --- Valid fixtures ---
console.log(`\n${colors.cyan}CSV File Reference Extraction Tests${colors.reset}\n`);
console.log(`${colors.cyan}Valid fixtures${colors.reset}`);
test('bmm-style.csv: extracts workflow-file refs with trailing commas', () => {
const { fullPath, content } = loadFixture('valid/bmm-style.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 2, `Expected 2 refs, got ${refs.length}`);
// Keep this legacy .yaml reference to verify backward-compatible CSV parsing.
assert(refs[0].raw === '_bmad/bmm/workflows/document-project/workflow.yaml', `Wrong raw[0]: ${refs[0].raw}`);
assert(refs[1].raw === '_bmad/core/workflows/brainstorming/workflow.md', `Wrong raw[1]: ${refs[1].raw}`);
assert(refs[0].type === 'project-root', `Wrong type: ${refs[0].type}`);
assert(refs[0].line === 2, `Wrong line for row 0: ${refs[0].line}`);
assert(refs[1].line === 3, `Wrong line for row 1: ${refs[1].line}`);
assert(refs[0].file === fullPath, 'Wrong file path');
});
test('core-style.csv: extracts refs from core module-help format', () => {
const { fullPath, content } = loadFixture('valid/core-style.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 2, `Expected 2 refs, got ${refs.length}`);
assert(refs[0].raw === '_bmad/core/workflows/brainstorming/workflow.md', `Wrong raw[0]: ${refs[0].raw}`);
assert(refs[1].raw === '_bmad/core/workflows/party-mode/workflow.md', `Wrong raw[1]: ${refs[1].raw}`);
});
test('minimal.csv: extracts refs from minimal 3-column CSV', () => {
const { fullPath, content } = loadFixture('valid/minimal.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 1, `Expected 1 ref, got ${refs.length}`);
assert(refs[0].raw === '_bmad/core/tasks/help.md', `Wrong raw: ${refs[0].raw}`);
assert(refs[0].line === 2, `Wrong line: ${refs[0].line}`);
});
// --- Invalid fixtures ---
console.log(`\n${colors.cyan}Invalid fixtures (expect 0 refs)${colors.reset}`);
test('no-workflow-column.csv: returns 0 refs when workflow-file column missing', () => {
const { fullPath, content } = loadFixture('invalid/no-workflow-column.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 0, `Expected 0 refs, got ${refs.length}`);
});
test('empty-data.csv: returns 0 refs when CSV has header only', () => {
const { fullPath, content } = loadFixture('invalid/empty-data.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 0, `Expected 0 refs, got ${refs.length}`);
});
test('all-empty-workflow.csv: returns 0 refs when all workflow-file cells empty', () => {
const { fullPath, content } = loadFixture('invalid/all-empty-workflow.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 0, `Expected 0 refs, got ${refs.length}`);
});
test('unresolvable-vars.csv: filters out template variables, keeps normal refs', () => {
const { fullPath, content } = loadFixture('invalid/unresolvable-vars.csv');
const refs = extractCsvRefs(fullPath, content);
assert(refs.length === 1, `Expected 1 ref, got ${refs.length}`);
assert(refs[0].raw === '_bmad/core/tasks/help.md', `Wrong raw: ${refs[0].raw}`);
});
// --- Summary ---
console.log(`\n${colors.cyan}${'═'.repeat(55)}${colors.reset}`);
console.log(`${colors.cyan}Test Results:${colors.reset}`);
console.log(` Total: ${totalTests}`);
console.log(` Passed: ${colors.green}${passedTests}${colors.reset}`);
console.log(` Failed: ${passedTests === totalTests ? colors.green : colors.red}${totalTests - passedTests}${colors.reset}`);
console.log(`${colors.cyan}${'═'.repeat(55)}${colors.reset}\n`);
if (failures.length > 0) {
console.log(`${colors.red}FAILED TESTS:${colors.reset}\n`);
for (const failure of failures) {
console.log(`${colors.red}\u2717${colors.reset} ${failure.name}`);
console.log(` ${failure.message}\n`);
}
process.exit(1);
}
console.log(`${colors.green}All tests passed!${colors.reset}\n`);
process.exit(0);

View File

@ -14,15 +14,12 @@
const path = require('node:path'); const path = require('node:path');
const os = require('node:os'); const os = require('node:os');
const fs = require('fs-extra'); const fs = require('fs-extra');
const csv = require('csv-parse/sync');
const yaml = require('yaml'); const yaml = require('yaml');
const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder'); const { YamlXmlBuilder } = require('../tools/cli/lib/yaml-xml-builder');
const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator'); const { ManifestGenerator } = require('../tools/cli/installers/lib/core/manifest-generator');
const { WorkflowCommandGenerator } = require('../tools/cli/installers/lib/ide/shared/workflow-command-generator'); const { WorkflowCommandGenerator } = require('../tools/cli/installers/lib/ide/shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('../tools/cli/installers/lib/ide/shared/task-tool-command-generator'); const { TaskToolCommandGenerator } = require('../tools/cli/installers/lib/ide/shared/task-tool-command-generator');
const { ConfigDrivenIdeSetup } = require('../tools/cli/installers/lib/ide/_config-driven');
const { IdeManager } = require('../tools/cli/installers/lib/ide/manager'); const { IdeManager } = require('../tools/cli/installers/lib/ide/manager');
const { CodexSetup } = require('../tools/cli/installers/lib/ide/codex');
const { ModuleManager } = require('../tools/cli/installers/lib/modules/manager'); const { ModuleManager } = require('../tools/cli/installers/lib/modules/manager');
const { BMAD_FOLDER_NAME } = require('../tools/cli/installers/lib/ide/shared/path-utils'); const { BMAD_FOLDER_NAME } = require('../tools/cli/installers/lib/ide/shared/path-utils');
@ -200,19 +197,16 @@ async function runTests() {
try { try {
const builder = new YamlXmlBuilder(); const builder = new YamlXmlBuilder();
// Basic path-variable substitution contract used across workflow templates // Test path resolution logic (if exposed)
const testPath = '{project-root}/_bmad/bmm/config.yaml'; // This would test {project-root}, {installed_path}, {config_source} resolution
const projectRootStub = path.join(os.tmpdir(), 'bmad-test-project');
const resolvedPath = testPath.replace('{project-root}', projectRootStub);
assert(builder && typeof builder.deepMerge === 'function', 'Path suite uses initialized YamlXmlBuilder instance'); const testPath = '{project-root}/bmad/bmm/config.yaml';
const expectedPattern = /\/bmad\/bmm\/config\.yaml$/;
assert(resolvedPath.startsWith(projectRootStub), 'Path variable replaces {project-root} with resolved root');
assert( assert(
resolvedPath.endsWith(path.join(BMAD_FOLDER_NAME, 'bmm', 'config.yaml')), true, // Placeholder - would test actual resolution
'Path variable resolution preserves canonical BMAD folder path', 'Path variable resolution pattern matches expected format',
`Resolved path was: ${resolvedPath}`, 'Note: This test validates path resolution logic exists',
); );
} catch (error) { } catch (error) {
assert(false, 'Path resolution works', error.message); assert(false, 'Path resolution works', error.message);
@ -354,9 +348,10 @@ async function runTests() {
]; ];
const allowedExtensions = new Set(['.md', '.yaml', '.yml', '.xml']); const allowedExtensions = new Set(['.md', '.yaml', '.yml', '.xml']);
const forbiddenRef = 'validate-workflow.xml'; const forbiddenRef = 'validate-workflow.xml';
const excludedFile = path.join(projectRoot, 'src', 'core', 'tasks', 'validate-workflow.xml');
const offenders = []; const offenders = [];
const files = await collectFiles(searchTargets, allowedExtensions); const files = await collectFiles(searchTargets, allowedExtensions, new Set([excludedFile]));
for (const fullPath of files) { for (const fullPath of files) {
const content = await fs.readFile(fullPath, 'utf8'); const content = await fs.readFile(fullPath, 'utf8');
if (content.includes(forbiddenRef)) { if (content.includes(forbiddenRef)) {
@ -683,179 +678,6 @@ internal: true
console.log(''); console.log('');
// ============================================================
// Test 17: Help Task Agent-Only Guidance Guard
// ============================================================
console.log(`${colors.yellow}Test Suite 17: Help Task Agent-Only Guidance Guard${colors.reset}\n`);
try {
const helpTaskPath = path.join(projectRoot, 'src', 'core', 'tasks', 'help.md');
const moduleHelpPath = path.join(projectRoot, 'src', 'bmm', 'module-help.csv');
const helpTaskContent = await fs.readFile(helpTaskPath, 'utf8');
const moduleHelpRows = csv.parse(await fs.readFile(moduleHelpPath, 'utf8'), {
columns: true,
skip_empty_lines: true,
});
const hasAgentOnlyRows = moduleHelpRows.some((row) => !row.command && row.agent);
assert(hasAgentOnlyRows, 'Help catalog includes agent-only rows with empty command values');
assert(
helpTaskContent.includes('When `command` is empty') && helpTaskContent.includes('Do not invent a slash command'),
'Help task includes explicit guidance for agent-only rows without commands',
);
} catch (error) {
assert(false, 'Help task agent-only guidance guard runs', error.message);
}
console.log('');
// ============================================================
// Test 18: Codex Task Visibility Guard
// ============================================================
console.log(`${colors.yellow}Test Suite 18: Codex Task Visibility Guard${colors.reset}\n`);
try {
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-codex-visibility-'));
const projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.ensureDir(projectDir);
await fs.copy(path.join(projectRoot, 'src', 'core'), path.join(bmadDir, 'core'));
await fs.copy(path.join(projectRoot, 'src', 'bmm'), path.join(bmadDir, 'bmm'));
const manifestGenerator = new ManifestGenerator();
await manifestGenerator.generateManifests(bmadDir, ['bmm'], [], { ides: ['codex'] });
const codexSetup = new CodexSetup();
await codexSetup.setup(projectDir, bmadDir, {
selectedModules: ['bmm'],
preCollectedConfig: { installLocation: 'project' },
});
const promptsDir = path.join(projectDir, '.codex', 'prompts');
const generated = await fs.readdir(promptsDir);
assert(!generated.includes('bmad-workflow.md'), 'Codex export excludes internal workflow runner task prompt', generated.join(', '));
await fs.remove(tmpRoot);
} catch (error) {
assert(false, 'Codex task visibility guard runs', error.message);
}
console.log('');
// ============================================================
// Test 19: Empty Target Artifact Filter Guard
// ============================================================
console.log(`${colors.yellow}Test Suite 19: Empty Artifact Target Guard${colors.reset}\n`);
try {
const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'bmad-empty-target-'));
const projectDir = path.join(tmpRoot, 'project');
const bmadDir = path.join(tmpRoot, BMAD_FOLDER_NAME);
await fs.ensureDir(projectDir);
await fs.ensureDir(bmadDir);
const setup = new ConfigDrivenIdeSetup('test', {
name: 'Test IDE',
preferred: false,
installer: { target_dir: '.test', template_type: 'default' },
});
const result = await setup.installToTarget(
projectDir,
bmadDir,
{ target_dir: '.test', template_type: 'default', artifact_types: [] },
{ silent: true },
);
assert(
result.success &&
result.results.agents === 0 &&
result.results.workflows === 0 &&
result.results.tasks === 0 &&
result.results.tools === 0,
'Installer short-circuits explicit empty artifact target',
);
assert(!(await fs.pathExists(path.join(projectDir, '.test'))), 'Installer does not create output directory for empty artifact target');
await fs.remove(tmpRoot);
} catch (error) {
assert(false, 'Empty artifact target guard runs', error.message);
}
console.log('');
// ============================================================
// Test 20: Split Template Extension Override Guard
// ============================================================
console.log(`${colors.yellow}Test Suite 20: Split Template Extension Guard${colors.reset}\n`);
try {
const setup = new ConfigDrivenIdeSetup('test', {
name: 'Test IDE',
preferred: false,
installer: { target_dir: '.test', template_type: 'default' },
});
setup.loadSplitTemplates = async () => 'template-content';
const loaded = await setup.loadTemplateWithMetadata('default', 'workflow', {
header_template: 'header.md',
extension: 'toml',
});
assert(loaded.extension === '.toml', 'Split template loader preserves configured extension override');
} catch (error) {
assert(false, 'Split template extension guard runs', error.message);
}
console.log('');
// ============================================================
// Test 21: Workflow Path Normalization Guard
// ============================================================
console.log(`${colors.yellow}Test Suite 21: Workflow Path Normalization Guard${colors.reset}\n`);
try {
const generator = new WorkflowCommandGenerator();
generator.loadWorkflowManifest = async () => [
{
name: 'create-story',
description: 'Create Story',
module: 'bmm',
path: String.raw`C:\repo\src\bmm\workflows\4-implementation\create-story\workflow.md`,
},
{
name: 'validate-workflow',
description: 'Validate Workflow',
module: 'core',
path: String.raw`C:\repo\_bmad\core\workflows\validate-workflow\workflow.md`,
},
];
generator.generateCommandContent = async () => 'content';
const { artifacts } = await generator.collectWorkflowArtifacts('/tmp');
const createStory = artifacts.find((artifact) => artifact.name === 'create-story');
const validateWorkflow = artifacts.find((artifact) => artifact.name === 'validate-workflow');
assert(
createStory?.workflowPath === 'bmm/workflows/4-implementation/create-story/workflow.md',
'Workflow artifact path normalizes Windows src path to module-relative path',
createStory?.workflowPath,
);
assert(
validateWorkflow?.workflowPath === 'core/workflows/validate-workflow/workflow.md',
'Workflow artifact path strips _bmad prefix after separator normalization',
validateWorkflow?.workflowPath,
);
} catch (error) {
assert(false, 'Workflow path normalization guard runs', error.message);
}
console.log('');
// ============================================================ // ============================================================
// Summary // Summary
// ============================================================ // ============================================================

File diff suppressed because it is too large Load Diff

View File

@ -52,12 +52,6 @@ const LLM_EXCLUDE_PATTERNS = [
*/ */
async function main() { async function main() {
if (process.platform === 'win32') {
console.error('Error: The docs build pipeline does not support Windows.');
console.error('Please build on Linux, macOS, or WSL.');
process.exit(1);
}
console.log(); console.log();
printBanner('BMAD Documentation Build Pipeline'); printBanner('BMAD Documentation Build Pipeline');
console.log(); console.log();
@ -124,6 +118,9 @@ function buildAstroSite() {
runAstroBuild(); runAstroBuild();
copyArtifactsToSite(artifactsDir, siteDir); copyArtifactsToSite(artifactsDir, siteDir);
// No longer needed: Inject AI agents banner into every HTML page
// injectAgentBanner(siteDir);
console.log(); console.log();
console.log(` \u001B[32m✓\u001B[0m Astro build complete`); console.log(` \u001B[32m✓\u001B[0m Astro build complete`);
@ -155,18 +152,20 @@ function generateLlmsTxt(outputDir) {
'', '',
'## Quick Start', '## Quick Start',
'', '',
`- **[Getting Started](${siteUrl}/tutorials/getting-started/)** - Tutorial: install and learn how BMad works`, `- **[Quick Start](${siteUrl}/docs/modules/bmm/quick-start)** - Get started with BMAD Method`,
`- **[Installation](${siteUrl}/how-to/install-bmad/)** - How to install BMad Method`, `- **[Installation](${siteUrl}/docs/getting-started/installation)** - Installation guide`,
'', '',
'## Core Concepts', '## Core Concepts',
'', '',
`- **[Quick Flow](${siteUrl}/explanation/quick-flow/)** - Fast development workflow`, `- **[Scale Adaptive System](${siteUrl}/docs/modules/bmm/scale-adaptive-system)** - Understand BMAD scaling`,
`- **[Party Mode](${siteUrl}/explanation/party-mode/)** - Multi-agent collaboration`, `- **[Quick Flow](${siteUrl}/docs/modules/bmm/bmad-quick-flow)** - Fast development workflow`,
`- **[Workflow Map](${siteUrl}/reference/workflow-map/)** - Visual overview of phases and workflows`, `- **[Party Mode](${siteUrl}/docs/modules/bmm/party-mode)** - Multi-agent collaboration`,
'', '',
'## Modules', '## Modules',
'', '',
`- **[Official Modules](${siteUrl}/reference/modules/)** - BMM, BMB, BMGD, and more`, `- **[BMM - Method](${siteUrl}/docs/modules/bmm/quick-start)** - Core methodology module`,
`- **[BMB - Builder](${siteUrl}/docs/modules/bmb/)** - Agent and workflow builder`,
`- **[BMGD - Game Dev](${siteUrl}/docs/modules/bmgd/quick-start)** - Game development module`,
'', '',
'---', '---',
'', '',
@ -402,6 +401,32 @@ function formatFileSize(bytes) {
return `${bytes}B`; return `${bytes}B`;
} }
// =============================================================================
// Post-build Injection
/**
* Recursively collects all files with the given extension under a directory.
*
* @param {string} dir - Root directory to search.
* @param {string} ext - File extension to match (include the leading dot, e.g. ".md").
* @returns {string[]} An array of file paths for files ending with `ext` found under `dir`.
*/
function getAllFilesByExtension(dir, ext) {
const result = [];
const entries = fs.readdirSync(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
result.push(...getAllFilesByExtension(fullPath, ext));
} else if (entry.name.endsWith(ext)) {
result.push(fullPath);
}
}
return result;
}
// ============================================================================= // =============================================================================
// File System Utilities // File System Utilities
/** /**
@ -419,6 +444,33 @@ function cleanBuildDirectory() {
fs.mkdirSync(BUILD_DIR, { recursive: true }); fs.mkdirSync(BUILD_DIR, { recursive: true });
} }
/**
* Recursively copies all files and subdirectories from one directory to another, creating the destination if needed.
*
* @param {string} src - Path to the source directory to copy from.
* @param {string} dest - Path to the destination directory to copy to.
* @param {string[]} [exclude=[]] - List of file or directory names (not paths) to skip while copying.
* @returns {boolean} `true` if the source existed and copying proceeded, `false` if the source did not exist.
*/
function copyDirectory(src, dest, exclude = []) {
if (!fs.existsSync(src)) return false;
fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
if (exclude.includes(entry.name)) continue;
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDirectory(srcPath, destPath, exclude);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
return true;
}
// ============================================================================= // =============================================================================
// Console Output Formatting // Console Output Formatting
// ============================================================================= // =============================================================================
@ -444,7 +496,7 @@ function printBanner(title) {
/** /**
* Verify internal documentation links by running the link-checking script. * Verify internal documentation links by running the link-checking script.
* *
* Executes the Node script tools/validate-doc-links.js from the project root and * Executes the Node script tools/check-doc-links.js from the project root and
* exits the process with code 1 if the check fails. * exits the process with code 1 if the check fails.
*/ */

View File

@ -2,14 +2,6 @@ const { program } = require('commander');
const path = require('node:path'); const path = require('node:path');
const fs = require('node:fs'); const fs = require('node:fs');
const { execSync } = require('node:child_process'); const { execSync } = require('node:child_process');
const prompts = require('./lib/prompts');
// The installer flow uses many sequential @clack/prompts, each adding keypress
// listeners to stdin. Raise the limit to avoid spurious EventEmitter warnings.
if (process.stdin?.setMaxListeners) {
const currentLimit = process.stdin.getMaxListeners();
process.stdin.setMaxListeners(Math.max(currentLimit, 50));
}
// Check for updates - do this asynchronously so it doesn't block startup // Check for updates - do this asynchronously so it doesn't block startup
const packageJson = require('../../package.json'); const packageJson = require('../../package.json');
@ -35,17 +27,17 @@ async function checkForUpdate() {
}).trim(); }).trim();
if (result && result !== packageJson.version) { if (result && result !== packageJson.version) {
const color = await prompts.getColor(); console.warn('');
const updateMsg = [ console.warn(' ╔═══════════════════════════════════════════════════════════════════════════════╗');
`You are using version ${packageJson.version} but ${result} is available.`, console.warn(' ║ UPDATE AVAILABLE ║');
'', console.warn(' ║ ║');
'To update, exit and first run:', console.warn(` ║ You are using version ${packageJson.version} but ${result} is available. ║`);
` npm cache clean --force && npx bmad-method@${tag} install`, console.warn(' ║ ║');
].join('\n'); console.warn(' ║ To update,exir and first run: ║');
await prompts.box(updateMsg, 'Update Available', { console.warn(` ║ npm cache clean --force && npx bmad-method@${tag} install ║`);
rounded: true, console.warn(' ║ ║');
formatBorder: color.yellow, console.warn(' ╚═══════════════════════════════════════════════════════════════════════════════╝');
}); console.warn('');
} }
} catch { } catch {
// Silently fail - network issues or npm not available // Silently fail - network issues or npm not available

View File

@ -1,5 +1,5 @@
const chalk = require('chalk');
const path = require('node:path'); const path = require('node:path');
const prompts = require('../lib/prompts');
const { Installer } = require('../installers/lib/core/installer'); const { Installer } = require('../installers/lib/core/installer');
const { UI } = require('../lib/ui'); const { UI } = require('../lib/ui');
@ -30,14 +30,14 @@ module.exports = {
// Set debug flag as environment variable for all components // Set debug flag as environment variable for all components
if (options.debug) { if (options.debug) {
process.env.BMAD_DEBUG_MANIFEST = 'true'; process.env.BMAD_DEBUG_MANIFEST = 'true';
await prompts.log.info('Debug mode enabled'); console.log(chalk.cyan('Debug mode enabled\n'));
} }
const config = await ui.promptInstall(options); const config = await ui.promptInstall(options);
// Handle cancel // Handle cancel
if (config.actionType === 'cancel') { if (config.actionType === 'cancel') {
await prompts.log.warn('Installation cancelled.'); console.log(chalk.yellow('Installation cancelled.'));
process.exit(0); process.exit(0);
return; return;
} }
@ -45,13 +45,13 @@ module.exports = {
// 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);
await prompts.log.success('Quick update complete!'); console.log(chalk.green('\n✨ Quick update complete!'));
await prompts.log.info(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`); console.log(chalk.cyan(`Updated ${result.moduleCount} modules with preserved settings (${result.modules.join(', ')})`));
// Display version-specific end message // Display version-specific end message
const { MessageLoader } = require('../installers/lib/message-loader'); const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader(); const messageLoader = new MessageLoader();
await messageLoader.displayEndMessage(); messageLoader.displayEndMessage();
process.exit(0); process.exit(0);
return; return;
@ -60,8 +60,8 @@ module.exports = {
// Handle compile agents separately // Handle compile agents separately
if (config.actionType === 'compile-agents') { if (config.actionType === 'compile-agents') {
const result = await installer.compileAgents(config); const result = await installer.compileAgents(config);
await prompts.log.success('Agent recompilation complete!'); console.log(chalk.green('\n✨ Agent recompilation complete!'));
await prompts.log.info(`Recompiled ${result.agentCount} agents with customizations applied`); console.log(chalk.cyan(`Recompiled ${result.agentCount} agents with customizations applied`));
process.exit(0); process.exit(0);
return; return;
} }
@ -80,22 +80,21 @@ module.exports = {
// Display version-specific end message from install-messages.yaml // Display version-specific end message from install-messages.yaml
const { MessageLoader } = require('../installers/lib/message-loader'); const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader(); const messageLoader = new MessageLoader();
await messageLoader.displayEndMessage(); messageLoader.displayEndMessage();
process.exit(0); process.exit(0);
} }
} catch (error) { } catch (error) {
try { // Check if error has a complete formatted message
if (error.fullMessage) { if (error.fullMessage) {
await prompts.log.error(error.fullMessage); console.error(error.fullMessage);
} else {
await prompts.log.error(`Installation failed: ${error.message}`);
}
if (error.stack) { if (error.stack) {
await prompts.log.message(error.stack); console.error('\n' + chalk.dim(error.stack));
} }
} catch { } else {
console.error(error.fullMessage || error.message || error); // Generic error handling for all other errors
console.error(chalk.red('Installation failed:'), error.message);
console.error(chalk.dim(error.stack));
} }
process.exit(1); process.exit(1);
} }

View File

@ -1,5 +1,5 @@
const chalk = require('chalk');
const path = require('node:path'); const path = require('node:path');
const prompts = require('../lib/prompts');
const { Installer } = require('../installers/lib/core/installer'); const { Installer } = require('../installers/lib/core/installer');
const { Manifest } = require('../installers/lib/core/manifest'); const { Manifest } = require('../installers/lib/core/manifest');
const { UI } = require('../lib/ui'); const { UI } = require('../lib/ui');
@ -21,9 +21,9 @@ module.exports = {
// Check if bmad directory exists // Check if bmad directory exists
const fs = require('fs-extra'); const fs = require('fs-extra');
if (!(await fs.pathExists(bmadDir))) { if (!(await fs.pathExists(bmadDir))) {
await prompts.log.warn('No BMAD installation found in the current directory.'); console.log(chalk.yellow('No BMAD installation found in the current directory.'));
await prompts.log.message(`Expected location: ${bmadDir}`); console.log(chalk.dim(`Expected location: ${bmadDir}`));
await prompts.log.message('Run "bmad install" to set up a new installation.'); console.log(chalk.dim('\nRun "bmad install" to set up a new installation.'));
process.exit(0); process.exit(0);
return; return;
} }
@ -32,8 +32,8 @@ module.exports = {
const manifestData = await manifest._readRaw(bmadDir); const manifestData = await manifest._readRaw(bmadDir);
if (!manifestData) { if (!manifestData) {
await prompts.log.warn('No BMAD installation manifest found.'); console.log(chalk.yellow('No BMAD installation manifest found.'));
await prompts.log.message('Run "bmad install" to set up a new installation.'); console.log(chalk.dim('\nRun "bmad install" to set up a new installation.'));
process.exit(0); process.exit(0);
return; return;
} }
@ -46,7 +46,7 @@ module.exports = {
const availableUpdates = await manifest.checkForUpdates(bmadDir); const availableUpdates = await manifest.checkForUpdates(bmadDir);
// Display status // Display status
await ui.displayStatus({ ui.displayStatus({
installation, installation,
modules, modules,
availableUpdates, availableUpdates,
@ -55,9 +55,9 @@ module.exports = {
process.exit(0); process.exit(0);
} catch (error) { } catch (error) {
await prompts.log.error(`Status check failed: ${error.message}`); console.error(chalk.red('Status check failed:'), error.message);
if (process.env.BMAD_DEBUG) { if (process.env.BMAD_DEBUG) {
await prompts.log.message(error.stack); console.error(chalk.dim(error.stack));
} }
process.exit(1); process.exit(1);
} }

View File

@ -1,6 +1,7 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const yaml = require('yaml'); const yaml = require('yaml');
const chalk = require('chalk');
const { getProjectRoot, getModulePath } = require('../../../lib/project-root'); const { getProjectRoot, getModulePath } = require('../../../lib/project-root');
const { CLIUtils } = require('../../../lib/cli-utils'); const { CLIUtils } = require('../../../lib/cli-utils');
const prompts = require('../../../lib/prompts'); const prompts = require('../../../lib/prompts');
@ -259,9 +260,15 @@ class ConfigCollector {
// If module has no config keys at all, handle it specially // If module has no config keys at all, handle it specially
if (hasNoConfig && moduleConfig.subheader) { if (hasNoConfig && moduleConfig.subheader) {
// Add blank line for better readability (matches other modules)
console.log();
const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`; const moduleDisplayName = moduleConfig.header || `${moduleName.toUpperCase()} Module`;
await prompts.log.step(moduleDisplayName);
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`); // Display the module name in color first (matches other modules)
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
// Show the subheader since there's no configuration to ask about
console.log(chalk.dim(`${moduleConfig.subheader}`));
return false; // No new fields return false; // No new fields
} }
@ -315,7 +322,7 @@ class ConfigCollector {
} }
// Show "no config" message for modules with no new questions (that have config keys) // Show "no config" message for modules with no new questions (that have config keys)
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module already up to date`); console.log(chalk.dim(` ${moduleName.toUpperCase()} module already up to date`));
return false; // No new fields return false; // No new fields
} }
@ -343,15 +350,15 @@ class ConfigCollector {
if (questions.length > 0) { if (questions.length > 0) {
// Only show header if we actually have questions // Only show header if we actually have questions
await CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader); CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
await prompts.log.message(''); console.log(); // Line break before questions
const promptedAnswers = await prompts.prompt(questions); const promptedAnswers = await prompts.prompt(questions);
// Merge prompted answers with static answers // Merge prompted answers with static answers
Object.assign(allAnswers, promptedAnswers); Object.assign(allAnswers, promptedAnswers);
} else if (newStaticKeys.length > 0) { } else if (newStaticKeys.length > 0) {
// Only static fields, no questions - show no config message // Only static fields, no questions - show no config message
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configuration updated`); console.log(chalk.dim(` ${moduleName.toUpperCase()} module configuration updated`));
} }
// Store all answers for cross-referencing // Store all answers for cross-referencing
@ -581,7 +588,7 @@ class ConfigCollector {
// Skip prompts mode: use all defaults without asking // Skip prompts mode: use all defaults without asking
if (this.skipPrompts) { if (this.skipPrompts) {
await prompts.log.info(`Using default configuration for ${moduleDisplayName}`); console.log(chalk.cyan('Using default configuration for'), chalk.magenta(moduleDisplayName));
// Use defaults for all questions // Use defaults for all questions
for (const question of questions) { for (const question of questions) {
const hasDefault = question.default !== undefined && question.default !== null && question.default !== ''; const hasDefault = question.default !== undefined && question.default !== null && question.default !== '';
@ -590,10 +597,12 @@ class ConfigCollector {
} }
} }
} else { } else {
await prompts.log.step(moduleDisplayName); console.log();
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
let customize = true; let customize = true;
if (moduleName === 'core') { if (moduleName === 'core') {
// Core module: no confirm prompt, continues directly // Core module: no confirm prompt, so add spacing manually to match visual style
console.log(chalk.gray('│'));
} else { } else {
// Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing) // Non-core modules: show "Accept Defaults?" confirm prompt (clack adds spacing)
const customizeAnswer = await prompts.prompt([ const customizeAnswer = await prompts.prompt([
@ -612,7 +621,7 @@ class ConfigCollector {
const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === ''); const questionsWithoutDefaults = questions.filter((q) => q.default === undefined || q.default === null || q.default === '');
if (questionsWithoutDefaults.length > 0) { if (questionsWithoutDefaults.length > 0) {
await prompts.log.message(` Asking required questions for ${moduleName.toUpperCase()}...`); console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
const promptedAnswers = await prompts.prompt(questionsWithoutDefaults); const promptedAnswers = await prompts.prompt(questionsWithoutDefaults);
Object.assign(allAnswers, promptedAnswers); Object.assign(allAnswers, promptedAnswers);
} }
@ -738,15 +747,32 @@ class ConfigCollector {
const hasNoConfig = actualConfigKeys.length === 0; const hasNoConfig = actualConfigKeys.length === 0;
if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) { if (hasNoConfig && (moduleConfig.subheader || moduleConfig.header)) {
await prompts.log.step(moduleDisplayName); // Module explicitly has no configuration - show with special styling
// Add blank line for better readability (matches other modules)
console.log();
// Display the module name in color first (matches other modules)
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
// Ask user if they want to accept defaults or customize on the next line
const { customize } = await prompts.prompt([
{
type: 'confirm',
name: 'customize',
message: 'Accept Defaults (no to customize)?',
default: true,
},
]);
// Show the subheader if available, otherwise show a default message
if (moduleConfig.subheader) { if (moduleConfig.subheader) {
await prompts.log.message(` \u2713 ${moduleConfig.subheader}`); console.log(chalk.dim(` ${moduleConfig.subheader}`));
} else { } else {
await prompts.log.message(` \u2713 No custom configuration required`); console.log(chalk.dim(` ✓ No custom configuration required`));
} }
} else { } else {
// Module has config but just no questions to ask // Module has config but just no questions to ask
await prompts.log.message(` \u2713 ${moduleName.toUpperCase()} module configured`); console.log(chalk.dim(` ${moduleName.toUpperCase()} module configured`));
} }
} }
@ -955,15 +981,14 @@ class ConfigCollector {
} }
// Add current value indicator for existing configs // Add current value indicator for existing configs
const color = await prompts.getColor();
if (existingValue !== null && existingValue !== undefined) { if (existingValue !== null && existingValue !== undefined) {
if (typeof existingValue === 'boolean') { if (typeof existingValue === 'boolean') {
message += color.dim(` (current: ${existingValue ? 'true' : 'false'})`); message += chalk.dim(` (current: ${existingValue ? 'true' : 'false'})`);
} else if (Array.isArray(existingValue)) { } else if (Array.isArray(existingValue)) {
message += color.dim(` (current: ${existingValue.join(', ')})`); message += chalk.dim(` (current: ${existingValue.join(', ')})`);
} else if (questionType !== 'list') { } else if (questionType !== 'list') {
// Show the cleaned value (without {project-root}/) for display // Show the cleaned value (without {project-root}/) for display
message += color.dim(` (current: ${existingValue})`); message += chalk.dim(` (current: ${existingValue})`);
} }
} else if (item.example && questionType === 'input') { } else if (item.example && questionType === 'input') {
// Show example for input fields // Show example for input fields
@ -973,7 +998,7 @@ class ConfigCollector {
exampleText = this.replacePlaceholders(exampleText, moduleName, moduleConfig); exampleText = this.replacePlaceholders(exampleText, moduleName, moduleConfig);
exampleText = exampleText.replace('{project-root}/', ''); exampleText = exampleText.replace('{project-root}/', '');
} }
message += color.dim(` (e.g., ${exampleText})`); message += chalk.dim(` (e.g., ${exampleText})`);
} }
// Build the question object // Build the question object

View File

@ -1,8 +1,8 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('node:path'); const path = require('node:path');
const glob = require('glob'); const glob = require('glob');
const chalk = require('chalk');
const yaml = require('yaml'); const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
/** /**
* Dependency Resolver for BMAD modules * Dependency Resolver for BMAD modules
@ -24,7 +24,7 @@ class DependencyResolver {
*/ */
async resolve(bmadDir, selectedModules = [], options = {}) { async resolve(bmadDir, selectedModules = [], options = {}) {
if (options.verbose) { if (options.verbose) {
await prompts.log.info('Resolving module dependencies...'); console.log(chalk.cyan('Resolving module dependencies...'));
} }
// Always include core as base // Always include core as base
@ -50,7 +50,7 @@ class DependencyResolver {
// Report results (only in verbose mode) // Report results (only in verbose mode)
if (options.verbose) { if (options.verbose) {
await this.reportResults(organizedFiles, selectedModules); this.reportResults(organizedFiles, selectedModules);
} }
return { return {
@ -90,12 +90,8 @@ class DependencyResolver {
} }
} }
if (!moduleDir) {
continue;
}
if (!(await fs.pathExists(moduleDir))) { if (!(await fs.pathExists(moduleDir))) {
await prompts.log.warn('Module directory not found: ' + moduleDir); console.warn(chalk.yellow(`Module directory not found: ${moduleDir}`));
continue; continue;
} }
@ -183,7 +179,7 @@ class DependencyResolver {
} }
} }
} catch (error) { } catch (error) {
await prompts.log.warn('Failed to parse frontmatter in ' + file.name + ': ' + error.message); console.warn(chalk.yellow(`Failed to parse frontmatter in ${file.name}: ${error.message}`));
} }
} }
@ -662,8 +658,8 @@ class DependencyResolver {
/** /**
* Report resolution results * Report resolution results
*/ */
async reportResults(organized, selectedModules) { reportResults(organized, selectedModules) {
await prompts.log.success('Dependency resolution complete'); console.log(chalk.green('\n✓ Dependency resolution complete'));
for (const [module, files] of Object.entries(organized)) { for (const [module, files] of Object.entries(organized)) {
const isSelected = selectedModules.includes(module) || module === 'core'; const isSelected = selectedModules.includes(module) || module === 'core';
@ -671,31 +667,31 @@ class DependencyResolver {
files.agents.length + files.tasks.length + files.tools.length + files.templates.length + files.data.length + files.other.length; files.agents.length + files.tasks.length + files.tools.length + files.templates.length + files.data.length + files.other.length;
if (totalFiles > 0) { if (totalFiles > 0) {
await prompts.log.info(` ${module.toUpperCase()} module:`); console.log(chalk.cyan(`\n ${module.toUpperCase()} module:`));
await prompts.log.message(` Status: ${isSelected ? 'Selected' : 'Dependencies only'}`); console.log(chalk.dim(` Status: ${isSelected ? 'Selected' : 'Dependencies only'}`));
if (files.agents.length > 0) { if (files.agents.length > 0) {
await prompts.log.message(` Agents: ${files.agents.length}`); console.log(chalk.dim(` Agents: ${files.agents.length}`));
} }
if (files.tasks.length > 0) { if (files.tasks.length > 0) {
await prompts.log.message(` Tasks: ${files.tasks.length}`); console.log(chalk.dim(` Tasks: ${files.tasks.length}`));
} }
if (files.templates.length > 0) { if (files.templates.length > 0) {
await prompts.log.message(` Templates: ${files.templates.length}`); console.log(chalk.dim(` Templates: ${files.templates.length}`));
} }
if (files.data.length > 0) { if (files.data.length > 0) {
await prompts.log.message(` Data files: ${files.data.length}`); console.log(chalk.dim(` Data files: ${files.data.length}`));
} }
if (files.other.length > 0) { if (files.other.length > 0) {
await prompts.log.message(` Other files: ${files.other.length}`); console.log(chalk.dim(` Other files: ${files.other.length}`));
} }
} }
} }
if (this.missingDependencies.size > 0) { if (this.missingDependencies.size > 0) {
await prompts.log.warn('Missing dependencies:'); console.log(chalk.yellow('\n ⚠ Missing dependencies:'));
for (const missing of this.missingDependencies) { for (const missing of this.missingDependencies) {
await prompts.log.warn(` - ${missing}`); console.log(chalk.yellow(` - ${missing}`));
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const chalk = require('chalk');
const yaml = require('yaml'); const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
const { FileOps } = require('../../../lib/file-ops'); const { FileOps } = require('../../../lib/file-ops');
const { XmlHandler } = require('../../../lib/xml-handler'); const { XmlHandler } = require('../../../lib/xml-handler');
@ -88,7 +88,7 @@ class CustomHandler {
try { try {
config = yaml.parse(configContent); config = yaml.parse(configContent);
} catch (parseError) { } catch (parseError) {
await prompts.log.warn('YAML parse error in ' + configPath + ': ' + parseError.message); console.warn(chalk.yellow(`Warning: YAML parse error in ${configPath}:`, parseError.message));
return null; return null;
} }
@ -111,7 +111,7 @@ class CustomHandler {
isInstallConfig: isInstallConfig, // Track which type this is isInstallConfig: isInstallConfig, // Track which type this is
}; };
} catch (error) { } catch (error) {
await prompts.log.warn('Failed to read ' + configPath + ': ' + error.message); console.warn(chalk.yellow(`Warning: Failed to read ${configPath}:`, error.message));
return null; return null;
} }
} }
@ -268,13 +268,14 @@ class CustomHandler {
} }
results.filesCopied++; results.filesCopied++;
if (entry.name.endsWith('.md')) {
results.workflowsInstalled++;
}
if (fileTrackingCallback) { if (fileTrackingCallback) {
fileTrackingCallback(targetPath); fileTrackingCallback(targetPath);
} }
} }
if (entry.name.endsWith('.md')) {
results.workflowsInstalled++;
}
} catch (error) { } catch (error) {
results.errors.push(`Failed to copy ${entry.name}: ${error.message}`); results.errors.push(`Failed to copy ${entry.name}: ${error.message}`);
} }
@ -321,7 +322,7 @@ class CustomHandler {
await fs.writeFile(customizePath, templateContent, 'utf8'); await fs.writeFile(customizePath, templateContent, 'utf8');
// Only show customize creation in verbose mode // Only show customize creation in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') { if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(' Created customize: custom-' + agentName + '.customize.yaml'); console.log(chalk.dim(` Created customize: custom-${agentName}.customize.yaml`));
} }
} }
} }
@ -345,10 +346,14 @@ class CustomHandler {
// Only show compilation details in verbose mode // Only show compilation details in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') { if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(' Compiled agent: ' + agentName + ' -> ' + path.relative(targetAgentsPath, targetMdPath)); console.log(
chalk.dim(
` Compiled agent: ${agentName} -> ${path.relative(targetAgentsPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
),
);
} }
} catch (error) { } catch (error) {
await prompts.log.warn(' Failed to compile agent ' + agentName + ': ' + error.message); console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`); results.errors.push(`Failed to compile agent ${agentName}: ${error.message}`);
} }
} }

View File

@ -1,7 +1,7 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const chalk = require('chalk');
const { XmlHandler } = require('../../../lib/xml-handler'); const { XmlHandler } = require('../../../lib/xml-handler');
const prompts = require('../../../lib/prompts');
const { getSourcePath } = require('../../../lib/project-root'); const { getSourcePath } = require('../../../lib/project-root');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils'); const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
@ -53,7 +53,7 @@ class BaseIdeSetup {
* Cleanup IDE configuration * Cleanup IDE configuration
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
*/ */
async cleanup(projectDir, options = {}) { async cleanup(projectDir) {
// Default implementation - can be overridden // Default implementation - can be overridden
if (this.configDir) { if (this.configDir) {
const configPath = path.join(projectDir, this.configDir); const configPath = path.join(projectDir, this.configDir);
@ -61,7 +61,7 @@ class BaseIdeSetup {
const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME); const bmadRulesPath = path.join(configPath, BMAD_FOLDER_NAME);
if (await fs.pathExists(bmadRulesPath)) { if (await fs.pathExists(bmadRulesPath)) {
await fs.remove(bmadRulesPath); await fs.remove(bmadRulesPath);
if (!options.silent) await prompts.log.message(`Removed ${this.name} BMAD configuration`); console.log(chalk.dim(`Removed ${this.name} BMAD configuration`));
} }
} }
} }

View File

@ -1,7 +1,7 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const chalk = require('chalk');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const prompts = require('../../../lib/prompts');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
@ -34,10 +34,10 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
* @returns {Promise<Object>} Setup result * @returns {Promise<Object>} Setup result
*/ */
async setup(projectDir, bmadDir, options = {}) { async setup(projectDir, bmadDir, options = {}) {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); console.log(chalk.cyan(`Setting up ${this.name}...`));
// Clean up any old BMAD installation first // Clean up any old BMAD installation first
await this.cleanup(projectDir, options); await this.cleanup(projectDir);
if (!this.installerConfig) { if (!this.installerConfig) {
return { success: false, reason: 'no-config' }; return { success: false, reason: 'no-config' };
@ -66,12 +66,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
*/ */
async installToTarget(projectDir, bmadDir, config, options) { async installToTarget(projectDir, bmadDir, config, options) {
const { target_dir, template_type, artifact_types } = config; const { target_dir, template_type, artifact_types } = config;
// Skip explicitly empty targets to avoid creating empty command directories.
if (Array.isArray(artifact_types) && artifact_types.length === 0) {
return { success: true, results: { agents: 0, workflows: 0, tasks: 0, tools: 0 } };
}
const targetPath = path.join(projectDir, target_dir); const targetPath = path.join(projectDir, target_dir);
await this.ensureDir(targetPath); await this.ensureDir(targetPath);
@ -101,7 +95,7 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
results.tools = taskToolResult.tools; results.tools = taskToolResult.tools;
} }
await this.printSummary(results, target_dir, options); this.printSummary(results, target_dir);
return { success: true, results }; return { success: true, results };
} }
@ -256,13 +250,12 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
// Check for separate header/body templates // Check for separate header/body templates
if (header_template || body_template) { if (header_template || body_template) {
const template = await this.loadSplitTemplates(templateType, artifactType, header_template, body_template); const template = await this.loadSplitTemplates(templateType, artifactType, header_template, body_template);
return { template, extension: this.normalizeExtension(config.extension) }; return { template, extension: '.md' };
} }
// Load combined template with extension detection // Load combined template with extension detection
const templateBaseName = artifactType ? `${templateType}-${artifactType}` : templateType;
for (const extension of supportedExtensions) { for (const extension of supportedExtensions) {
const templateName = `${templateBaseName}${extension}`; const templateName = `${templateType}-${artifactType}${extension}`;
const templatePath = path.join(__dirname, 'templates', 'combined', templateName); const templatePath = path.join(__dirname, 'templates', 'combined', templateName);
if (await fs.pathExists(templatePath)) { if (await fs.pathExists(templatePath)) {
return { return {
@ -336,19 +329,6 @@ class ConfigDrivenIdeSetup extends BaseIdeSetup {
return `${header}\n${body}`; return `${header}\n${body}`;
} }
normalizeExtension(extension) {
if (!extension) {
return '.md';
}
const trimmed = String(extension).trim();
if (trimmed === '') {
return '.md';
}
return trimmed.startsWith('.') ? trimmed : `.${trimmed}`;
}
/** /**
* Get default minimal template * Get default minimal template
* @param {string} artifactType - Artifact type * @param {string} artifactType - Artifact type
@ -434,28 +414,32 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @param {Object} results - Installation results * @param {Object} results - Installation results
* @param {string} targetDir - Target directory (relative) * @param {string} targetDir - Target directory (relative)
*/ */
async printSummary(results, targetDir, options = {}) { printSummary(results, targetDir) {
if (options.silent) return; console.log(chalk.green(`\n${this.name} configured:`));
const parts = []; if (results.agents > 0) {
if (results.agents > 0) parts.push(`${results.agents} agents`); console.log(chalk.dim(` - ${results.agents} agents installed`));
if (results.workflows > 0) parts.push(`${results.workflows} workflows`); }
if (results.tasks > 0) parts.push(`${results.tasks} tasks`); if (results.workflows > 0) {
if (results.tools > 0) parts.push(`${results.tools} tools`); console.log(chalk.dim(` - ${results.workflows} workflow commands generated`));
await prompts.log.success(`${this.name} configured: ${parts.join(', ')}${targetDir}`); }
if (results.tasks > 0 || results.tools > 0) {
console.log(chalk.dim(` - ${results.tasks + results.tools} task/tool commands generated`));
}
console.log(chalk.dim(` - Destination: ${targetDir}`));
} }
/** /**
* Cleanup IDE configuration * Cleanup IDE configuration
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
*/ */
async cleanup(projectDir, options = {}) { async cleanup(projectDir) {
// Clean all target directories // Clean all target directories
if (this.installerConfig?.targets) { if (this.installerConfig?.targets) {
for (const target of this.installerConfig.targets) { for (const target of this.installerConfig.targets) {
await this.cleanupTarget(projectDir, target.target_dir, options); await this.cleanupTarget(projectDir, target.target_dir);
} }
} else if (this.installerConfig?.target_dir) { } else if (this.installerConfig?.target_dir) {
await this.cleanupTarget(projectDir, this.installerConfig.target_dir, options); await this.cleanupTarget(projectDir, this.installerConfig.target_dir);
} }
} }
@ -464,7 +448,7 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
* @param {string} projectDir - Project directory * @param {string} projectDir - Project directory
* @param {string} targetDir - Target directory to clean * @param {string} targetDir - Target directory to clean
*/ */
async cleanupTarget(projectDir, targetDir, options = {}) { async cleanupTarget(projectDir, targetDir) {
const targetPath = path.join(projectDir, targetDir); const targetPath = path.join(projectDir, targetDir);
if (!(await fs.pathExists(targetPath))) { if (!(await fs.pathExists(targetPath))) {
@ -487,22 +471,25 @@ LOAD and execute from: {project-root}/{{bmadFolderName}}/{{path}}
let removedCount = 0; let removedCount = 0;
for (const entry of entries) { for (const entry of entries) {
// Skip non-strings or undefined entries
if (!entry || typeof entry !== 'string') { if (!entry || typeof entry !== 'string') {
continue; continue;
} }
if (entry.startsWith('bmad')) { if (entry.startsWith('bmad')) {
const entryPath = path.join(targetPath, entry); const entryPath = path.join(targetPath, entry);
try { const stat = await fs.stat(entryPath);
if (stat.isFile()) {
await fs.remove(entryPath);
removedCount++;
} else if (stat.isDirectory()) {
await fs.remove(entryPath); await fs.remove(entryPath);
removedCount++; removedCount++;
} catch {
// Skip entries that can't be removed (broken symlinks, permission errors)
} }
} }
} }
if (removedCount > 0 && !options.silent) { if (removedCount > 0) {
await prompts.log.message(` Cleaned ${removedCount} BMAD files from ${targetDir}`); console.log(chalk.dim(` Cleaned ${removedCount} BMAD files from ${targetDir}`));
} }
} }
} }

View File

@ -1,6 +1,7 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const os = require('node:os'); const os = require('node:os');
const chalk = require('chalk');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
@ -42,11 +43,12 @@ class CodexSetup extends BaseIdeSetup {
default: 'global', default: 'global',
}); });
// Show brief confirmation hint (detailed instructions available via verbose) // Display detailed instructions for the chosen option
console.log('');
if (installLocation === 'project') { if (installLocation === 'project') {
await prompts.log.info('Prompts installed to: <project>/.codex/prompts (requires CODEX_HOME)'); console.log(this.getProjectSpecificInstructions());
} else { } else {
await prompts.log.info('Prompts installed to: ~/.codex/prompts'); console.log(this.getGlobalInstructions());
} }
// Confirm the choice // Confirm the choice
@ -56,7 +58,7 @@ class CodexSetup extends BaseIdeSetup {
}); });
if (!confirmed) { if (!confirmed) {
await prompts.log.warn("Let's choose a different installation option."); console.log(chalk.yellow("\n Let's choose a different installation option.\n"));
} }
} }
@ -70,7 +72,7 @@ class CodexSetup extends BaseIdeSetup {
* @param {Object} options - Setup options * @param {Object} options - Setup options
*/ */
async setup(projectDir, bmadDir, options = {}) { async setup(projectDir, bmadDir, options = {}) {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); console.log(chalk.cyan(`Setting up ${this.name}...`));
// Always use CLI mode // Always use CLI mode
const mode = 'cli'; const mode = 'cli';
@ -82,7 +84,7 @@ class CodexSetup extends BaseIdeSetup {
const destDir = this.getCodexPromptDir(projectDir, installLocation); const destDir = this.getCodexPromptDir(projectDir, installLocation);
await fs.ensureDir(destDir); await fs.ensureDir(destDir);
await this.clearOldBmadFiles(destDir, options); await this.clearOldBmadFiles(destDir);
// Collect artifacts and write using underscore format // Collect artifacts and write using underscore format
const agentGen = new AgentCommandGenerator(this.bmadFolderName); const agentGen = new AgentCommandGenerator(this.bmadFolderName);
@ -122,11 +124,16 @@ class CodexSetup extends BaseIdeSetup {
const written = agentCount + workflowCount + tasksWritten; const written = agentCount + workflowCount + tasksWritten;
if (!options.silent) { console.log(chalk.green(`${this.name} configured:`));
await prompts.log.success( console.log(chalk.dim(` - Mode: CLI`));
`${this.name} configured: ${counts.agents} agents, ${counts.workflows} workflows, ${counts.tasks} tasks, ${written} files → ${destDir}`, console.log(chalk.dim(` - ${counts.agents} agents exported`));
); console.log(chalk.dim(` - ${counts.tasks} tasks exported`));
console.log(chalk.dim(` - ${counts.workflows} workflow commands exported`));
if (counts.workflowLaunchers > 0) {
console.log(chalk.dim(` - ${counts.workflowLaunchers} workflow launchers exported`));
} }
console.log(chalk.dim(` - ${written} Codex prompt files written`));
console.log(chalk.dim(` - Destination: ${destDir}`));
return { return {
success: true, success: true,
@ -255,7 +262,7 @@ class CodexSetup extends BaseIdeSetup {
return written; return written;
} }
async clearOldBmadFiles(destDir, options = {}) { async clearOldBmadFiles(destDir) {
if (!(await fs.pathExists(destDir))) { if (!(await fs.pathExists(destDir))) {
return; return;
} }
@ -265,7 +272,7 @@ class CodexSetup extends BaseIdeSetup {
entries = await fs.readdir(destDir); entries = await fs.readdir(destDir);
} catch (error) { } catch (error) {
// Directory exists but can't be read - skip cleanup // Directory exists but can't be read - skip cleanup
if (!options.silent) await prompts.log.warn(`Warning: Could not read directory ${destDir}: ${error.message}`); console.warn(chalk.yellow(`Warning: Could not read directory ${destDir}: ${error.message}`));
return; return;
} }
@ -284,11 +291,15 @@ class CodexSetup extends BaseIdeSetup {
const entryPath = path.join(destDir, entry); const entryPath = path.join(destDir, entry);
try { try {
const stat = await fs.stat(entryPath);
if (stat.isFile()) {
await fs.remove(entryPath);
} else if (stat.isDirectory()) {
await fs.remove(entryPath); await fs.remove(entryPath);
} catch (error) {
if (!options.silent) {
await prompts.log.message(` Skipping ${entry}: ${error.message}`);
} }
} catch (error) {
// Skip files that can't be processed
console.warn(chalk.dim(` Skipping ${entry}: ${error.message}`));
} }
} }
} }
@ -304,16 +315,22 @@ class CodexSetup extends BaseIdeSetup {
*/ */
getGlobalInstructions(destDir) { getGlobalInstructions(destDir) {
const lines = [ const lines = [
'IMPORTANT: Codex Configuration',
'', '',
'/prompts installed globally to your HOME DIRECTORY.', chalk.bold.cyan('═'.repeat(70)),
chalk.bold.yellow(' IMPORTANT: Codex Configuration'),
chalk.bold.cyan('═'.repeat(70)),
'', '',
'These prompts reference a specific _bmad path.', chalk.white(' /prompts installed globally to your HOME DIRECTORY.'),
"To use with other projects, you'd need to copy the _bmad dir.", '',
chalk.yellow(' ⚠️ These prompts reference a specific _bmad path'),
chalk.dim(" To use with other projects, you'd need to copy the _bmad dir"),
'',
chalk.green(' ✓ You can now use /commands in Codex CLI'),
chalk.dim(' Example: /bmad_bmm_pm'),
chalk.dim(' Type / to see all available commands'),
'',
chalk.bold.cyan('═'.repeat(70)),
'', '',
'You can now use /commands in Codex CLI',
' Example: /bmad_bmm_pm',
' Type / to see all available commands',
]; ];
return lines.join('\n'); return lines.join('\n');
} }
@ -328,34 +345,43 @@ class CodexSetup extends BaseIdeSetup {
const isWindows = os.platform() === 'win32'; const isWindows = os.platform() === 'win32';
const commonLines = [ const commonLines = [
'Project-Specific Codex Configuration',
'', '',
`Prompts will be installed to: ${destDir || '<project>/.codex/prompts'}`, chalk.bold.cyan('═'.repeat(70)),
chalk.bold.yellow(' Project-Specific Codex Configuration'),
chalk.bold.cyan('═'.repeat(70)),
'', '',
'REQUIRED: You must set CODEX_HOME to use these prompts', chalk.white(' Prompts will be installed to: ') + chalk.cyan(destDir || '<project>/.codex/prompts'),
'',
chalk.bold.yellow(' ⚠️ REQUIRED: You must set CODEX_HOME to use these prompts'),
'', '',
]; ];
const windowsLines = [ const windowsLines = [
'Create a codex.cmd file in your project root:', chalk.bold(' Create a codex.cmd file in your project root:'),
'', '',
' @echo off', chalk.green(' @echo off'),
' set CODEX_HOME=%~dp0.codex', chalk.green(' set CODEX_HOME=%~dp0.codex'),
' codex %*', chalk.green(' codex %*'),
'', '',
String.raw`Then run: .\codex instead of codex`, chalk.dim(String.raw` Then run: .\codex instead of codex`),
'(The %~dp0 gets the directory of the .cmd file)', chalk.dim(' (The %~dp0 gets the directory of the .cmd file)'),
]; ];
const unixLines = [ const unixLines = [
'Add this alias to your ~/.bashrc or ~/.zshrc:', chalk.bold(' Add this alias to your ~/.bashrc or ~/.zshrc:'),
'', '',
' alias codex=\'CODEX_HOME="$PWD/.codex" codex\'', chalk.green(' alias codex=\'CODEX_HOME="$PWD/.codex" codex\''),
'',
chalk.dim(' After adding, run: source ~/.bashrc (or source ~/.zshrc)'),
chalk.dim(' (The $PWD uses your current working directory)'),
];
const closingLines = [
'',
chalk.dim(' This tells Codex CLI to use prompts from this project instead of ~/.codex'),
'',
chalk.bold.cyan('═'.repeat(70)),
'', '',
'After adding, run: source ~/.bashrc (or source ~/.zshrc)',
'(The $PWD uses your current working directory)',
]; ];
const closingLines = ['', 'This tells Codex CLI to use prompts from this project instead of ~/.codex'];
const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines]; const lines = [...commonLines, ...(isWindows ? windowsLines : unixLines), ...closingLines];

View File

@ -1,7 +1,7 @@
const path = require('node:path'); const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide'); const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const yaml = require('yaml'); const yaml = require('yaml');
const prompts = require('../../../lib/prompts');
const { AgentCommandGenerator } = require('./shared/agent-command-generator'); const { AgentCommandGenerator } = require('./shared/agent-command-generator');
const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator'); const { WorkflowCommandGenerator } = require('./shared/workflow-command-generator');
const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator'); const { TaskToolCommandGenerator } = require('./shared/task-tool-command-generator');
@ -23,10 +23,10 @@ class KiloSetup extends BaseIdeSetup {
* @param {Object} options - Setup options * @param {Object} options - Setup options
*/ */
async setup(projectDir, bmadDir, options = {}) { async setup(projectDir, bmadDir, options = {}) {
if (!options.silent) await prompts.log.info(`Setting up ${this.name}...`); console.log(chalk.cyan(`Setting up ${this.name}...`));
// Clean up any old BMAD installation first // Clean up any old BMAD installation first
await this.cleanup(projectDir, options); await this.cleanup(projectDir);
// Load existing config (may contain non-BMAD modes and other settings) // Load existing config (may contain non-BMAD modes and other settings)
const kiloModesPath = path.join(projectDir, this.configFile); const kiloModesPath = path.join(projectDir, this.configFile);
@ -38,7 +38,7 @@ class KiloSetup extends BaseIdeSetup {
config = yaml.parse(existingContent) || {}; config = yaml.parse(existingContent) || {};
} catch { } catch {
// If parsing fails, start fresh but warn user // If parsing fails, start fresh but warn user
await prompts.log.warn('Warning: Could not parse existing .kilocodemodes, starting fresh'); console.log(chalk.yellow('Warning: Could not parse existing .kilocodemodes, starting fresh'));
config = {}; config = {};
} }
} }
@ -88,11 +88,14 @@ class KiloSetup extends BaseIdeSetup {
const taskCount = taskToolCounts.tasks || 0; const taskCount = taskToolCounts.tasks || 0;
const toolCount = taskToolCounts.tools || 0; const toolCount = taskToolCounts.tools || 0;
if (!options.silent) { console.log(chalk.green(`${this.name} configured:`));
await prompts.log.success( console.log(chalk.dim(` - ${addedCount} modes added`));
`${this.name} configured: ${addedCount} modes, ${workflowCount} workflows, ${taskCount} tasks, ${toolCount} tools → ${this.configFile}`, console.log(chalk.dim(` - ${workflowCount} workflows exported`));
); console.log(chalk.dim(` - ${taskCount} tasks exported`));
} console.log(chalk.dim(` - ${toolCount} tools exported`));
console.log(chalk.dim(` - Configuration file: ${this.configFile}`));
console.log(chalk.dim(` - Workflows directory: .kilocode/workflows/`));
console.log(chalk.dim('\n Modes will be available when you open this project in KiloCode'));
return { return {
success: true, success: true,
@ -171,7 +174,7 @@ class KiloSetup extends BaseIdeSetup {
/** /**
* Cleanup KiloCode configuration * Cleanup KiloCode configuration
*/ */
async cleanup(projectDir, options = {}) { async cleanup(projectDir) {
const fs = require('fs-extra'); const fs = require('fs-extra');
const kiloModesPath = path.join(projectDir, this.configFile); const kiloModesPath = path.join(projectDir, this.configFile);
@ -189,12 +192,12 @@ class KiloSetup extends BaseIdeSetup {
if (removedCount > 0) { if (removedCount > 0) {
await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 })); await fs.writeFile(kiloModesPath, yaml.stringify(config, { lineWidth: 0 }));
if (!options.silent) await prompts.log.message(`Removed ${removedCount} BMAD modes from .kilocodemodes`); console.log(chalk.dim(`Removed ${removedCount} BMAD modes from .kilocodemodes`));
} }
} }
} catch { } catch {
// If parsing fails, leave file as-is // If parsing fails, leave file as-is
if (!options.silent) await prompts.log.warn('Warning: Could not parse .kilocodemodes for cleanup'); console.log(chalk.yellow('Warning: Could not parse .kilocodemodes for cleanup'));
} }
} }

View File

@ -0,0 +1,326 @@
const path = require('node:path');
const { BaseIdeSetup } = require('./_base-ide');
const chalk = require('chalk');
const fs = require('fs-extra');
const yaml = require('yaml');
/**
* Kiro CLI setup handler for BMad Method
*/
class KiroCliSetup extends BaseIdeSetup {
constructor() {
super('kiro-cli', 'Kiro CLI', false);
this.configDir = '.kiro';
this.agentsDir = 'agents';
}
/**
* Cleanup old BMAD installation before reinstalling
* @param {string} projectDir - Project directory
*/
async cleanup(projectDir) {
const bmadAgentsDir = path.join(projectDir, this.configDir, this.agentsDir);
if (await fs.pathExists(bmadAgentsDir)) {
// Remove existing BMad agents
const files = await fs.readdir(bmadAgentsDir);
for (const file of files) {
if (file.startsWith('bmad')) {
await fs.remove(path.join(bmadAgentsDir, file));
}
}
console.log(chalk.dim(` Cleaned old BMAD agents from ${this.name}`));
}
}
/**
* Setup Kiro CLI configuration with BMad agents
* @param {string} projectDir - Project directory
* @param {string} bmadDir - BMAD installation directory
* @param {Object} options - Setup options
*/
async setup(projectDir, bmadDir, options = {}) {
console.log(chalk.cyan(`Setting up ${this.name}...`));
await this.cleanup(projectDir);
const kiroDir = path.join(projectDir, this.configDir);
const agentsDir = path.join(kiroDir, this.agentsDir);
await this.ensureDir(agentsDir);
// Create BMad agents from source YAML files
await this.createBmadAgentsFromSource(agentsDir, projectDir);
console.log(chalk.green(`${this.name} configured with BMad agents`));
}
/**
* Create BMad agent definitions from source YAML files
* @param {string} agentsDir - Agents directory
* @param {string} projectDir - Project directory
*/
async createBmadAgentsFromSource(agentsDir, projectDir) {
const sourceDir = path.join(__dirname, '../../../../../src/modules');
// Find all agent YAML files
const agentFiles = await this.findAgentFiles(sourceDir);
for (const agentFile of agentFiles) {
try {
await this.processAgentFile(agentFile, agentsDir, projectDir);
} catch (error) {
console.warn(chalk.yellow(`⚠️ Failed to process ${agentFile}: ${error.message}`));
}
}
}
/**
* Find all agent YAML files in modules and core
* @param {string} sourceDir - Source modules directory
* @returns {Array} Array of agent file paths
*/
async findAgentFiles(sourceDir) {
const agentFiles = [];
// Check core agents
const coreAgentsDir = path.join(__dirname, '../../../../../src/core/agents');
if (await fs.pathExists(coreAgentsDir)) {
const files = await fs.readdir(coreAgentsDir);
for (const file of files) {
if (file.endsWith('.agent.yaml')) {
agentFiles.push(path.join(coreAgentsDir, file));
}
}
}
// Check module agents
if (!(await fs.pathExists(sourceDir))) {
return agentFiles;
}
const modules = await fs.readdir(sourceDir);
for (const module of modules) {
const moduleAgentsDir = path.join(sourceDir, module, 'agents');
if (await fs.pathExists(moduleAgentsDir)) {
const files = await fs.readdir(moduleAgentsDir);
for (const file of files) {
if (file.endsWith('.agent.yaml')) {
agentFiles.push(path.join(moduleAgentsDir, file));
}
}
}
}
return agentFiles;
}
/**
* Validate BMad Core compliance
* @param {Object} agentData - Agent YAML data
* @returns {boolean} True if compliant
*/
validateBmadCompliance(agentData) {
const requiredFields = ['agent.metadata.id', 'agent.persona.role', 'agent.persona.principles'];
for (const field of requiredFields) {
const keys = field.split('.');
let current = agentData;
for (const key of keys) {
if (!current || !current[key]) {
return false;
}
current = current[key];
}
}
return true;
}
/**
* Process individual agent YAML file
* @param {string} agentFile - Path to agent YAML file
* @param {string} agentsDir - Target agents directory
* @param {string} projectDir - Project directory
*/
async processAgentFile(agentFile, agentsDir, projectDir) {
const yamlContent = await fs.readFile(agentFile, 'utf8');
const agentData = yaml.parse(yamlContent);
if (!this.validateBmadCompliance(agentData)) {
return;
}
// Extract module from file path
const normalizedPath = path.normalize(agentFile);
const pathParts = normalizedPath.split(path.sep);
const basename = path.basename(agentFile, '.agent.yaml');
// Find the module name from path
let moduleName = 'unknown';
if (pathParts.includes('src')) {
const srcIndex = pathParts.indexOf('src');
if (srcIndex + 3 < pathParts.length) {
const folderAfterSrc = pathParts[srcIndex + 1];
if (folderAfterSrc === 'core') {
moduleName = 'core';
} else if (folderAfterSrc === 'bmm') {
moduleName = 'bmm';
}
}
}
// Extract the agent name from the ID path in YAML if available
let agentBaseName = basename;
if (agentData.agent && agentData.agent.metadata && agentData.agent.metadata.id) {
const idPath = agentData.agent.metadata.id;
agentBaseName = path.basename(idPath, '.md');
}
const agentName = `bmad-${moduleName}-${agentBaseName}`;
const sanitizedAgentName = this.sanitizeAgentName(agentName);
// Create JSON definition
await this.createAgentDefinitionFromYaml(agentsDir, sanitizedAgentName, agentData);
// Create prompt file
await this.createAgentPromptFromYaml(agentsDir, sanitizedAgentName, agentData, projectDir);
}
/**
* Sanitize agent name for file naming
* @param {string} name - Agent name
* @returns {string} Sanitized name
*/
sanitizeAgentName(name) {
return name
.toLowerCase()
.replaceAll(/\s+/g, '-')
.replaceAll(/[^a-z0-9-]/g, '');
}
/**
* Create agent JSON definition from YAML data
* @param {string} agentsDir - Agents directory
* @param {string} agentName - Agent name (role-based)
* @param {Object} agentData - Agent YAML data
*/
async createAgentDefinitionFromYaml(agentsDir, agentName, agentData) {
const personName = agentData.agent.metadata.name;
const role = agentData.agent.persona.role;
const agentConfig = {
name: agentName,
description: `${personName} - ${role}`,
prompt: `file://./${agentName}-prompt.md`,
tools: ['*'],
mcpServers: {},
useLegacyMcpJson: true,
resources: [],
};
const agentPath = path.join(agentsDir, `${agentName}.json`);
await fs.writeJson(agentPath, agentConfig, { spaces: 2 });
}
/**
* Create agent prompt from YAML data
* @param {string} agentsDir - Agents directory
* @param {string} agentName - Agent name (role-based)
* @param {Object} agentData - Agent YAML data
* @param {string} projectDir - Project directory
*/
async createAgentPromptFromYaml(agentsDir, agentName, agentData, projectDir) {
const promptPath = path.join(agentsDir, `${agentName}-prompt.md`);
// Generate prompt from YAML data
const prompt = this.generatePromptFromYaml(agentData);
await fs.writeFile(promptPath, prompt);
}
/**
* Generate prompt content from YAML data
* @param {Object} agentData - Agent YAML data
* @returns {string} Generated prompt
*/
generatePromptFromYaml(agentData) {
const agent = agentData.agent;
const name = agent.metadata.name;
const icon = agent.metadata.icon || '🤖';
const role = agent.persona.role;
const identity = agent.persona.identity;
const style = agent.persona.communication_style;
const principles = agent.persona.principles;
let prompt = `# ${name} ${icon}\n\n`;
prompt += `## Role\n${role}\n\n`;
if (identity) {
prompt += `## Identity\n${identity}\n\n`;
}
if (style) {
prompt += `## Communication Style\n${style}\n\n`;
}
if (principles) {
prompt += `## Principles\n`;
if (typeof principles === 'string') {
// Handle multi-line string principles
prompt += principles + '\n\n';
} else if (Array.isArray(principles)) {
// Handle array principles
for (const principle of principles) {
prompt += `- ${principle}\n`;
}
prompt += '\n';
}
}
// Add menu items if available
if (agent.menu && agent.menu.length > 0) {
prompt += `## Available Workflows\n`;
for (let i = 0; i < agent.menu.length; i++) {
const item = agent.menu[i];
prompt += `${i + 1}. **${item.trigger}**: ${item.description}\n`;
}
prompt += '\n';
}
prompt += `## Instructions\nYou are ${name}, part of the BMad Method. Follow your role and principles while assisting users with their development needs.\n`;
return prompt;
}
/**
* Check if Kiro CLI is available
* @returns {Promise<boolean>} True if available
*/
async isAvailable() {
try {
const { execSync } = require('node:child_process');
execSync('kiro-cli --version', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}
/**
* Get installation instructions
* @returns {string} Installation instructions
*/
getInstallInstructions() {
return `Install Kiro CLI:
curl -fsSL https://github.com/aws/kiro-cli/releases/latest/download/install.sh | bash
Or visit: https://github.com/aws/kiro-cli`;
}
}
module.exports = { KiroCliSetup };

View File

@ -1,14 +1,14 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('node:path'); const path = require('node:path');
const chalk = require('chalk');
const { BMAD_FOLDER_NAME } = require('./shared/path-utils'); const { BMAD_FOLDER_NAME } = require('./shared/path-utils');
const prompts = require('../../../lib/prompts');
/** /**
* IDE Manager - handles IDE-specific setup * IDE Manager - handles IDE-specific setup
* Dynamically discovers and loads IDE handlers * Dynamically discovers and loads IDE handlers
* *
* Loading strategy: * Loading strategy:
* 1. Custom installer files (codex.js, kilo.js) - for platforms with unique installation logic * 1. Custom installer files (codex.js, kilo.js, kiro-cli.js) - for platforms with unique installation logic
* 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns * 2. Config-driven handlers (from platform-codes.yaml) - for standard IDE installation patterns
*/ */
class IdeManager { class IdeManager {
@ -44,12 +44,12 @@ class IdeManager {
/** /**
* Dynamically load all IDE handlers * Dynamically load all IDE handlers
* 1. Load custom installer files first (codex.js, kilo.js) * 1. Load custom installer files first (codex.js, kilo.js, kiro-cli.js)
* 2. Load config-driven handlers from platform-codes.yaml * 2. Load config-driven handlers from platform-codes.yaml
*/ */
async loadHandlers() { async loadHandlers() {
// Load custom installer files // Load custom installer files
await this.loadCustomInstallerFiles(); this.loadCustomInstallerFiles();
// Load config-driven handlers from platform-codes.yaml // Load config-driven handlers from platform-codes.yaml
await this.loadConfigDrivenHandlers(); await this.loadConfigDrivenHandlers();
@ -59,9 +59,9 @@ class IdeManager {
* Load custom installer files (unique installation logic) * Load custom installer files (unique installation logic)
* These files have special installation patterns that don't fit the config-driven model * These files have special installation patterns that don't fit the config-driven model
*/ */
async loadCustomInstallerFiles() { loadCustomInstallerFiles() {
const ideDir = __dirname; const ideDir = __dirname;
const customFiles = ['codex.js', 'kilo.js']; const customFiles = ['codex.js', 'kilo.js', 'kiro-cli.js'];
for (const file of customFiles) { for (const file of customFiles) {
const filePath = path.join(ideDir, file); const filePath = path.join(ideDir, file);
@ -81,7 +81,7 @@ class IdeManager {
} }
} }
} catch (error) { } catch (error) {
await prompts.log.warn(`Warning: Could not load ${file}: ${error.message}`); console.log(chalk.yellow(` Warning: Could not load ${file}: ${error.message}`));
} }
} }
} }
@ -171,45 +171,17 @@ class IdeManager {
const handler = this.handlers.get(ideName.toLowerCase()); const handler = this.handlers.get(ideName.toLowerCase());
if (!handler) { if (!handler) {
await prompts.log.warn(`IDE '${ideName}' is not yet supported`); console.warn(chalk.yellow(`⚠️ IDE '${ideName}' is not yet supported`));
await prompts.log.message(`Supported IDEs: ${[...this.handlers.keys()].join(', ')}`); console.log(chalk.dim('Supported IDEs:', [...this.handlers.keys()].join(', ')));
return { success: false, ide: ideName, error: 'unsupported IDE' }; return { success: false, reason: 'unsupported' };
} }
try { try {
const handlerResult = await handler.setup(projectDir, bmadDir, options); await handler.setup(projectDir, bmadDir, options);
// Build detail string from handler-returned data return { success: true, ide: ideName };
let detail = '';
if (handlerResult && handlerResult.results) {
// Config-driven handlers return { success, results: { agents, workflows, tasks, tools } }
const r = handlerResult.results;
const parts = [];
if (r.agents > 0) parts.push(`${r.agents} agents`);
if (r.workflows > 0) parts.push(`${r.workflows} workflows`);
if (r.tasks > 0) parts.push(`${r.tasks} tasks`);
if (r.tools > 0) parts.push(`${r.tools} tools`);
detail = parts.join(', ');
} else if (handlerResult && handlerResult.counts) {
// Codex handler returns { success, counts: { agents, workflows, tasks }, written }
const c = handlerResult.counts;
const parts = [];
if (c.agents > 0) parts.push(`${c.agents} agents`);
if (c.workflows > 0) parts.push(`${c.workflows} workflows`);
if (c.tasks > 0) parts.push(`${c.tasks} tasks`);
detail = parts.join(', ');
} else if (handlerResult && handlerResult.modes !== undefined) {
// Kilo handler returns { success, modes, workflows, tasks, tools }
const parts = [];
if (handlerResult.modes > 0) parts.push(`${handlerResult.modes} modes`);
if (handlerResult.workflows > 0) parts.push(`${handlerResult.workflows} workflows`);
if (handlerResult.tasks > 0) parts.push(`${handlerResult.tasks} tasks`);
if (handlerResult.tools > 0) parts.push(`${handlerResult.tools} tools`);
detail = parts.join(', ');
}
return { success: true, ide: ideName, detail, handlerResult };
} catch (error) { } catch (error) {
await prompts.log.error(`Failed to setup ${ideName}: ${error.message}`); console.error(chalk.red(`Failed to setup ${ideName}:`), error.message);
return { success: false, ide: ideName, error: error.message }; return { success: false, error: error.message };
} }
} }
@ -282,7 +254,7 @@ class IdeManager {
const handler = this.handlers.get(ideName.toLowerCase()); const handler = this.handlers.get(ideName.toLowerCase());
if (!handler) { if (!handler) {
await prompts.log.warn(`IDE '${ideName}' is not yet supported for custom agent installation`); console.warn(chalk.yellow(`⚠️ IDE '${ideName}' is not yet supported for custom agent installation`));
continue; continue;
} }
@ -294,7 +266,7 @@ class IdeManager {
} }
} }
} catch (error) { } catch (error) {
await prompts.log.warn(`Failed to install ${ideName} launcher: ${error.message}`); console.warn(chalk.yellow(`⚠️ Failed to install ${ideName} launcher: ${error.message}`));
} }
} }

View File

@ -111,14 +111,12 @@ platforms:
description: "AI coding platform" description: "AI coding platform"
# No installer config - uses custom kilo.js (creates .kilocodemodes file) # No installer config - uses custom kilo.js (creates .kilocodemodes file)
kiro: kiro-cli:
name: "Kiro" name: "Kiro CLI"
preferred: false preferred: false
category: ide category: cli
description: "Amazon's AI-powered IDE" description: "Kiro command-line interface"
installer: # No installer config - uses custom kiro-cli.js (YAML→JSON conversion)
target_dir: .kiro/steering
template_type: kiro
opencode: opencode:
name: "OpenCode" name: "OpenCode"

View File

@ -1,5 +1,6 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const chalk = require('chalk');
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils'); const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
/** /**

View File

@ -1,6 +1,5 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const yaml = require('yaml');
/** /**
* Helpers for gathering BMAD agents/tasks from the installed tree. * Helpers for gathering BMAD agents/tasks from the installed tree.
@ -150,33 +149,8 @@ async function getTasksFromDir(dirPath, moduleName) {
const filePath = path.join(dirPath, file); const filePath = path.join(dirPath, file);
const content = await fs.readFile(filePath, 'utf8'); const content = await fs.readFile(filePath, 'utf8');
let isInternal = false; // Skip internal/engine files (not user-facing tasks)
let isStandalone = true; if (content.includes('internal="true"')) {
if (file.endsWith('.md')) {
// Parse markdown frontmatter for standalone/internal flags.
const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
if (frontmatterMatch) {
try {
const frontmatter = yaml.parse(frontmatterMatch[1]) || {};
isInternal = frontmatter.internal === true || frontmatter.internal === 'true';
if (frontmatter.standalone === false || frontmatter.standalone === 'false') {
isStandalone = false;
}
} catch {
// Keep defaults when frontmatter parsing fails.
}
}
} else {
// XML tasks rely on attributes for standalone/internal visibility.
isInternal = /internal\s*=\s*["']true["']/i.test(content);
if (/standalone\s*=\s*["']false["']/i.test(content)) {
isStandalone = false;
}
}
// Skip internal/engine or explicitly non-standalone tasks.
if (isInternal || !isStandalone) {
continue; continue;
} }

View File

@ -1,6 +1,7 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const csv = require('csv-parse/sync'); const csv = require('csv-parse/sync');
const chalk = require('chalk');
const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils'); const { toColonName, toColonPath, toDashPath, BMAD_FOLDER_NAME } = require('./path-utils');
/** /**

View File

@ -1,7 +1,7 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const csv = require('csv-parse/sync'); const csv = require('csv-parse/sync');
const prompts = require('../../../../lib/prompts'); const chalk = require('chalk');
const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils'); const { toColonPath, toDashPath, customAgentColonName, customAgentDashName, BMAD_FOLDER_NAME } = require('./path-utils');
/** /**
@ -22,7 +22,7 @@ class WorkflowCommandGenerator {
const workflows = await this.loadWorkflowManifest(bmadDir); const workflows = await this.loadWorkflowManifest(bmadDir);
if (!workflows) { if (!workflows) {
await prompts.log.warn('Workflow manifest not found. Skipping command generation.'); console.log(chalk.yellow('Workflow manifest not found. Skipping command generation.'));
return { generated: 0 }; return { generated: 0 };
} }
@ -68,8 +68,7 @@ class WorkflowCommandGenerator {
for (const workflow of allWorkflows) { for (const workflow of allWorkflows) {
const commandContent = await this.generateCommandContent(workflow, bmadDir); const commandContent = await this.generateCommandContent(workflow, bmadDir);
// Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.md) // Calculate the relative workflow path (e.g., bmm/workflows/4-implementation/sprint-planning/workflow.md)
let workflowRelPath = workflow.path || ''; let workflowRelPath = workflow.path;
workflowRelPath = workflowRelPath.replaceAll('\\', '/');
// Remove _bmad/ prefix if present to get relative path from project root // Remove _bmad/ prefix if present to get relative path from project root
// Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...) // Handle both absolute paths (/path/to/_bmad/...) and relative paths (_bmad/...)
if (workflowRelPath.includes('_bmad/')) { if (workflowRelPath.includes('_bmad/')) {
@ -77,11 +76,6 @@ class WorkflowCommandGenerator {
if (parts.length > 1) { if (parts.length > 1) {
workflowRelPath = parts.slice(1).join('/'); workflowRelPath = parts.slice(1).join('/');
} }
} else if (workflowRelPath.includes('/src/')) {
const match = workflowRelPath.match(/\/src\/([^/]+)\/(.+)/);
if (match) {
workflowRelPath = `${match[1]}/${match[2]}`;
}
} }
artifacts.push({ artifacts.push({
type: 'workflow-command', type: 'workflow-command',

View File

@ -1,16 +0,0 @@
---
inclusion: manual
---
# {{name}}
You must fully embody this agent's persona and follow all activation instructions exactly as specified. NEVER break character until given an exit command.
<agent-activation CRITICAL="TRUE">
1. LOAD the FULL agent file from #[[file:{{bmadFolderName}}/{{path}}]]
2. READ its entire contents - this contains the complete agent persona, menu, and instructions
3. FOLLOW every step in the <activation> section precisely
4. DISPLAY the welcome/greeting as instructed
5. PRESENT the numbered menu
6. WAIT for user input before proceeding
</agent-activation>

View File

@ -1,9 +0,0 @@
---
inclusion: manual
---
# {{name}}
Read the entire task file at: #[[file:{{bmadFolderName}}/{{path}}]]
Follow all instructions in the task file exactly as written.

View File

@ -1,9 +0,0 @@
---
inclusion: manual
---
# {{name}}
Read the entire tool file at: #[[file:{{bmadFolderName}}/{{path}}]]
Follow all instructions in the tool file exactly as written.

View File

@ -1,19 +0,0 @@
---
inclusion: manual
---
# {{name}}
IT IS CRITICAL THAT YOU FOLLOW THESE STEPS - while staying in character as the current agent persona you may have loaded:
<steps CRITICAL="TRUE">
1. Always LOAD the FULL #[[file:{{bmadFolderName}}/core/tasks/workflow.md]]
2. READ its entire contents - this is the CORE OS for EXECUTING the specific workflow-config #[[file:{{bmadFolderName}}/{{path}}]]
3. Pass workflow path to workflow.md using YAML parameter key `workflow-config` with value `{{bmadFolderName}}/{{path}}`
Example invocation parameter block:
```yaml
workflow-config: {{bmadFolderName}}/{{path}}
```
4. Follow workflow.md task instructions EXACTLY as written to process and follow the specific workflow config and its instructions
5. Save outputs after EACH section when generating any documents from templates
</steps>

View File

@ -1,7 +0,0 @@
---
inclusion: manual
---
# {{name}}
IT IS CRITICAL THAT YOU FOLLOW THIS COMMAND: LOAD the FULL #[[file:{{bmadFolderName}}/{{path}}]], READ its entire contents and follow its directions exactly!

View File

@ -1,7 +1,7 @@
const fs = require('fs-extra'); const fs = require('fs-extra');
const path = require('node:path'); const path = require('node:path');
const yaml = require('yaml'); const yaml = require('yaml');
const prompts = require('../../lib/prompts'); const chalk = require('chalk');
/** /**
* Load and display installer messages from messages.yaml * Load and display installer messages from messages.yaml
@ -51,20 +51,22 @@ class MessageLoader {
/** /**
* Display the start message (after logo, before prompts) * Display the start message (after logo, before prompts)
*/ */
async displayStartMessage() { displayStartMessage() {
const message = this.getStartMessage(); const message = this.getStartMessage();
if (message) { if (message) {
await prompts.log.info(message); console.log(chalk.cyan(message));
console.log();
} }
} }
/** /**
* Display the end message (after installation completes) * Display the end message (after installation completes)
*/ */
async displayEndMessage() { displayEndMessage() {
const message = this.getEndMessage(); const message = this.getEndMessage();
if (message) { if (message) {
await prompts.log.info(message); console.log();
console.log(chalk.cyan(message));
} }
} }

View File

@ -1,12 +1,12 @@
const path = require('node:path'); const path = require('node:path');
const fs = require('fs-extra'); const fs = require('fs-extra');
const yaml = require('yaml'); const yaml = require('yaml');
const prompts = require('../../../lib/prompts'); const chalk = require('chalk');
const ora = require('ora');
const { XmlHandler } = require('../../../lib/xml-handler'); const { XmlHandler } = require('../../../lib/xml-handler');
const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root'); const { getProjectRoot, getSourcePath, getModulePath } = require('../../../lib/project-root');
const { filterCustomizationData } = require('../../../lib/agent/compiler'); const { filterCustomizationData } = require('../../../lib/agent/compiler');
const { ExternalModuleManager } = require('./external-manager'); const { ExternalModuleManager } = require('./external-manager');
const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
/** /**
* Manages the installation, updating, and removal of BMAD modules. * Manages the installation, updating, and removal of BMAD modules.
@ -16,7 +16,7 @@ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
* @class ModuleManager * @class ModuleManager
* @requires fs-extra * @requires fs-extra
* @requires yaml * @requires yaml
* @requires prompts * @requires chalk
* @requires XmlHandler * @requires XmlHandler
* *
* @example * @example
@ -27,7 +27,7 @@ const { BMAD_FOLDER_NAME } = require('../ide/shared/path-utils');
class ModuleManager { class ModuleManager {
constructor(options = {}) { constructor(options = {}) {
this.xmlHandler = new XmlHandler(); this.xmlHandler = new XmlHandler();
this.bmadFolderName = BMAD_FOLDER_NAME; // Default, can be overridden this.bmadFolderName = 'bmad'; // Default, can be overridden
this.customModulePaths = new Map(); // Initialize custom module paths this.customModulePaths = new Map(); // Initialize custom module paths
this.externalModuleManager = new ExternalModuleManager(); // For external official modules this.externalModuleManager = new ExternalModuleManager(); // For external official modules
} }
@ -151,26 +151,26 @@ class ModuleManager {
// File hasn't been modified by user, safe to update // File hasn't been modified by user, safe to update
await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true); await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true);
if (process.env.BMAD_VERBOSE_INSTALL === 'true') { if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(` Updated sidecar file: ${relativeToBmad}`); console.log(chalk.dim(` Updated sidecar file: ${relativeToBmad}`));
} }
} else { } else {
// User has modified the file, preserve it // User has modified the file, preserve it
if (process.env.BMAD_VERBOSE_INSTALL === 'true') { if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(` Preserving user-modified file: ${relativeToBmad}`); console.log(chalk.dim(` Preserving user-modified file: ${relativeToBmad}`));
} }
} }
} else { } else {
// First time seeing this file in manifest, copy it // First time seeing this file in manifest, copy it
await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true); await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true);
if (process.env.BMAD_VERBOSE_INSTALL === 'true') { if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(` Added new sidecar file: ${relativeToBmad}`); console.log(chalk.dim(` Added new sidecar file: ${relativeToBmad}`));
} }
} }
} else { } else {
// New installation // New installation
await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true); await this.copyFileWithPlaceholderReplacement(sourceFilePath, targetFilePath, true);
if (process.env.BMAD_VERBOSE_INSTALL === 'true') { if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(` Copied sidecar file: ${relativeToBmad}`); console.log(chalk.dim(` Copied sidecar file: ${relativeToBmad}`));
} }
} }
@ -287,7 +287,7 @@ class ModuleManager {
moduleInfo.dependencies = config.dependencies || []; moduleInfo.dependencies = config.dependencies || [];
moduleInfo.defaultSelected = config.default_selected === undefined ? false : config.default_selected; moduleInfo.defaultSelected = config.default_selected === undefined ? false : config.default_selected;
} catch (error) { } catch (error) {
await prompts.log.warn(`Failed to read config for ${defaultName}: ${error.message}`); console.warn(`Failed to read config for ${defaultName}:`, error.message);
} }
return moduleInfo; return moduleInfo;
@ -298,7 +298,7 @@ class ModuleManager {
* @param {string} moduleCode - Code of the module to find (from module.yaml) * @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(moduleCode, options = {}) { 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
@ -315,7 +315,7 @@ class ModuleManager {
} }
// Check external official modules // Check external official modules
const externalSource = await this.findExternalModuleSource(moduleCode, options); const externalSource = await this.findExternalModuleSource(moduleCode);
if (externalSource) { if (externalSource) {
return externalSource; return externalSource;
} }
@ -347,7 +347,7 @@ class ModuleManager {
* @param {string} moduleCode - Code of the external module * @param {string} moduleCode - Code of the external module
* @returns {string} Path to the cloned repository * @returns {string} Path to the cloned repository
*/ */
async cloneExternalModule(moduleCode, options = {}) { async cloneExternalModule(moduleCode) {
const { execSync } = require('node:child_process'); const { execSync } = require('node:child_process');
const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode); const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode);
@ -357,32 +357,10 @@ class ModuleManager {
const cacheDir = this.getExternalCacheDir(); const cacheDir = this.getExternalCacheDir();
const moduleCacheDir = path.join(cacheDir, moduleCode); const moduleCacheDir = path.join(cacheDir, moduleCode);
const silent = options.silent || false;
// Create cache directory if it doesn't exist // Create cache directory if it doesn't exist
await fs.ensureDir(cacheDir); await fs.ensureDir(cacheDir);
// Helper to create a spinner or a no-op when silent
const createSpinner = async () => {
if (silent) {
return {
start() {},
stop() {},
error() {},
message() {},
cancel() {},
clear() {},
get isSpinning() {
return false;
},
get isCancelled() {
return false;
},
};
}
return await prompts.spinner();
};
// Track if we need to install dependencies // Track if we need to install dependencies
let needsDependencyInstall = false; let needsDependencyInstall = false;
let wasNewClone = false; let wasNewClone = false;
@ -390,30 +368,21 @@ class ModuleManager {
// Check if already cloned // Check if already cloned
if (await fs.pathExists(moduleCacheDir)) { if (await fs.pathExists(moduleCacheDir)) {
// Try to update if it's a git repo // Try to update if it's a git repo
const fetchSpinner = await createSpinner(); const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start();
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
try { try {
const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim(); const currentRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
// Fetch and reset to remote - works better with shallow clones than pull // Fetch and reset to remote - works better with shallow clones than pull
execSync('git fetch origin --depth 1', { execSync('git fetch origin --depth 1', { cwd: moduleCacheDir, stdio: 'pipe' });
cwd: moduleCacheDir, execSync('git reset --hard origin/HEAD', { cwd: moduleCacheDir, stdio: 'pipe' });
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
execSync('git reset --hard origin/HEAD', {
cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'],
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
});
const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim(); const newRef = execSync('git rev-parse HEAD', { cwd: moduleCacheDir, stdio: 'pipe' }).toString().trim();
fetchSpinner.stop(`Fetched ${moduleInfo.name}`); fetchSpinner.succeed(`Fetched ${moduleInfo.name}`);
// Force dependency install if we got new code // Force dependency install if we got new code
if (currentRef !== newRef) { if (currentRef !== newRef) {
needsDependencyInstall = true; needsDependencyInstall = true;
} }
} catch { } catch {
fetchSpinner.error(`Fetch failed, re-downloading ${moduleInfo.name}`); fetchSpinner.warn(`Fetch failed, re-downloading ${moduleInfo.name}`);
// If update fails, remove and re-clone // If update fails, remove and re-clone
await fs.remove(moduleCacheDir); await fs.remove(moduleCacheDir);
wasNewClone = true; wasNewClone = true;
@ -424,16 +393,14 @@ class ModuleManager {
// Clone if not exists or was removed // Clone if not exists or was removed
if (wasNewClone) { if (wasNewClone) {
const fetchSpinner = await createSpinner(); const fetchSpinner = ora(`Fetching ${moduleInfo.name}...`).start();
fetchSpinner.start(`Fetching ${moduleInfo.name}...`);
try { try {
execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, { execSync(`git clone --depth 1 "${moduleInfo.url}" "${moduleCacheDir}"`, {
stdio: ['ignore', 'pipe', 'pipe'], stdio: 'pipe',
env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
}); });
fetchSpinner.stop(`Fetched ${moduleInfo.name}`); fetchSpinner.succeed(`Fetched ${moduleInfo.name}`);
} catch (error) { } catch (error) {
fetchSpinner.error(`Failed to fetch ${moduleInfo.name}`); fetchSpinner.fail(`Failed to fetch ${moduleInfo.name}`);
throw new Error(`Failed to clone external module '${moduleCode}': ${error.message}`); throw new Error(`Failed to clone external module '${moduleCode}': ${error.message}`);
} }
} }
@ -447,18 +414,17 @@ class ModuleManager {
// Force install if we updated or cloned new // Force install if we updated or cloned new
if (needsDependencyInstall || wasNewClone || nodeModulesMissing) { if (needsDependencyInstall || wasNewClone || nodeModulesMissing) {
const installSpinner = await createSpinner(); const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start();
installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`);
try { try {
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', { execSync('npm install --production --no-audit --no-fund --prefer-offline --no-progress', {
cwd: moduleCacheDir, cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'], stdio: 'pipe',
timeout: 120_000, // 2 minute timeout timeout: 120_000, // 2 minute timeout
}); });
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`); installSpinner.succeed(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) { } catch (error) {
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`); installSpinner.warn(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` Warning: ${error.message}`); console.warn(chalk.yellow(` Warning: ${error.message}`));
} }
} else { } else {
// Check if package.json is newer than node_modules // Check if package.json is newer than node_modules
@ -473,18 +439,17 @@ class ModuleManager {
} }
if (packageJsonNewer) { if (packageJsonNewer) {
const installSpinner = await createSpinner(); const installSpinner = ora(`Installing dependencies for ${moduleInfo.name}...`).start();
installSpinner.start(`Installing dependencies for ${moduleInfo.name}...`);
try { try {
execSync('npm install --omit=dev --no-audit --no-fund --no-progress --legacy-peer-deps', { execSync('npm install --production --no-audit --no-fund --prefer-offline --no-progress', {
cwd: moduleCacheDir, cwd: moduleCacheDir,
stdio: ['ignore', 'pipe', 'pipe'], stdio: 'pipe',
timeout: 120_000, // 2 minute timeout timeout: 120_000, // 2 minute timeout
}); });
installSpinner.stop(`Installed dependencies for ${moduleInfo.name}`); installSpinner.succeed(`Installed dependencies for ${moduleInfo.name}`);
} catch (error) { } catch (error) {
installSpinner.error(`Failed to install dependencies for ${moduleInfo.name}`); installSpinner.warn(`Failed to install dependencies for ${moduleInfo.name}`);
if (!silent) await prompts.log.warn(` Warning: ${error.message}`); console.warn(chalk.yellow(` Warning: ${error.message}`));
} }
} }
} }
@ -498,7 +463,7 @@ class ModuleManager {
* @param {string} moduleCode - Code of the external module * @param {string} moduleCode - Code of the external module
* @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 findExternalModuleSource(moduleCode, options = {}) { async findExternalModuleSource(moduleCode) {
const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode); const moduleInfo = await this.externalModuleManager.getModuleByCode(moduleCode);
if (!moduleInfo) { if (!moduleInfo) {
@ -506,7 +471,7 @@ class ModuleManager {
} }
// Clone the external module repo // Clone the external module repo
const cloneDir = await this.cloneExternalModule(moduleCode, options); const cloneDir = await this.cloneExternalModule(moduleCode);
// The module-definition specifies the path to module.yaml relative to repo root // The module-definition specifies the path to module.yaml relative to repo root
// We need to return the directory containing module.yaml // We need to return the directory containing module.yaml
@ -527,7 +492,7 @@ class ModuleManager {
* @param {Object} options.logger - Logger instance for output * @param {Object} options.logger - Logger instance for output
*/ */
async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) { async install(moduleName, bmadDir, fileTrackingCallback = null, options = {}) {
const sourcePath = await this.findModuleSource(moduleName, { silent: options.silent }); const sourcePath = await this.findModuleSource(moduleName);
const targetPath = path.join(bmadDir, moduleName); const targetPath = path.join(bmadDir, moduleName);
// Check if source module exists // Check if source module exists
@ -548,14 +513,14 @@ class ModuleManager {
const customContent = await fs.readFile(rootCustomConfigPath, 'utf8'); const customContent = await fs.readFile(rootCustomConfigPath, 'utf8');
customConfig = yaml.parse(customContent); customConfig = yaml.parse(customContent);
} catch (error) { } catch (error) {
await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`); console.warn(chalk.yellow(`Warning: Failed to read custom.yaml for ${moduleName}:`, error.message));
} }
} else if (await fs.pathExists(moduleInstallerCustomPath)) { } else if (await fs.pathExists(moduleInstallerCustomPath)) {
try { try {
const customContent = await fs.readFile(moduleInstallerCustomPath, 'utf8'); const customContent = await fs.readFile(moduleInstallerCustomPath, 'utf8');
customConfig = yaml.parse(customContent); customConfig = yaml.parse(customContent);
} catch (error) { } catch (error) {
await prompts.log.warn(`Warning: Failed to read custom.yaml for ${moduleName}: ${error.message}`); console.warn(chalk.yellow(`Warning: Failed to read custom.yaml for ${moduleName}:`, error.message));
} }
} }
@ -563,7 +528,7 @@ class ModuleManager {
if (customConfig) { if (customConfig) {
options.moduleConfig = { ...options.moduleConfig, ...customConfig }; options.moduleConfig = { ...options.moduleConfig, ...customConfig };
if (options.logger) { if (options.logger) {
options.logger.log(` Merged custom configuration for ${moduleName}`); options.logger.log(chalk.cyan(` Merged custom configuration for ${moduleName}`));
} }
} }
@ -616,7 +581,7 @@ class ModuleManager {
* @param {string} bmadDir - Target bmad directory * @param {string} bmadDir - Target bmad directory
* @param {boolean} force - Force update (overwrite modifications) * @param {boolean} force - Force update (overwrite modifications)
*/ */
async update(moduleName, bmadDir, force = false, options = {}) { async update(moduleName, bmadDir, force = false) {
const sourcePath = await this.findModuleSource(moduleName); const sourcePath = await this.findModuleSource(moduleName);
const targetPath = path.join(bmadDir, moduleName); const targetPath = path.join(bmadDir, moduleName);
@ -633,7 +598,7 @@ class ModuleManager {
if (force) { if (force) {
// Force update - remove and reinstall // Force update - remove and reinstall
await fs.remove(targetPath); await fs.remove(targetPath);
return await this.install(moduleName, bmadDir, null, { installer: options.installer }); return await this.install(moduleName, bmadDir);
} else { } else {
// Selective update - preserve user modifications // Selective update - preserve user modifications
await this.syncModule(sourcePath, targetPath); await this.syncModule(sourcePath, targetPath);
@ -707,7 +672,7 @@ class ModuleManager {
const config = yaml.parse(configContent); const config = yaml.parse(configContent);
Object.assign(moduleInfo, config); Object.assign(moduleInfo, config);
} catch (error) { } catch (error) {
await prompts.log.warn(`Failed to read installed module config: ${error.message}`); console.warn(`Failed to read installed module config:`, error.message);
} }
} }
@ -769,7 +734,7 @@ class ModuleManager {
// Check for localskip="true" in the agent tag // Check for localskip="true" in the agent tag
const agentMatch = content.match(/<agent[^>]*\slocalskip="true"[^>]*>/); const agentMatch = content.match(/<agent[^>]*\slocalskip="true"[^>]*>/);
if (agentMatch) { if (agentMatch) {
await prompts.log.message(` Skipping web-only agent: ${path.basename(file)}`); console.log(chalk.dim(` Skipping web-only agent: ${path.basename(file)}`));
continue; // Skip this agent continue; // Skip this agent
} }
} }
@ -801,6 +766,7 @@ class ModuleManager {
mdContent = mdContent.replaceAll('_bmad', this.bmadFolderName); mdContent = mdContent.replaceAll('_bmad', this.bmadFolderName);
mdContent = this.stripWebBundleFromFrontmatter(mdContent); mdContent = this.stripWebBundleFromFrontmatter(mdContent);
await fs.writeFile(targetFile, mdContent, 'utf8'); await fs.writeFile(targetFile, mdContent, 'utf8');
} }
@ -810,8 +776,6 @@ class ModuleManager {
return content; return content;
} }
const newline = frontmatterMatch[0].includes('\r\n') ? '\r\n' : '\n';
try { try {
const yaml = require('yaml'); const yaml = require('yaml');
const parsed = yaml.parse(frontmatterMatch[1]); const parsed = yaml.parse(frontmatterMatch[1]);
@ -829,8 +793,7 @@ class ModuleManager {
}) })
.trimEnd(); .trimEnd();
const normalized = newline === '\r\n' ? serialized.replaceAll('\n', '\r\n') : serialized; return content.replace(frontmatterMatch[0], `---\n${serialized}\n---`);
return content.replace(frontmatterMatch[0], `---${newline}${normalized}${newline}---`);
} catch (error) { } catch (error) {
console.warn(`Warning: Failed to parse workflow frontmatter for web_bundle removal: ${error.message}`); console.warn(`Warning: Failed to parse workflow frontmatter for web_bundle removal: ${error.message}`);
return content; return content;
@ -884,7 +847,7 @@ class ModuleManager {
await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath); await this.copyFileWithPlaceholderReplacement(genericTemplatePath, customizePath);
// Only show customize creation in verbose mode // Only show customize creation in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') { if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message(` Created customize: ${moduleName}-${agentName}.customize.yaml`); console.log(chalk.dim(` Created customize: ${moduleName}-${agentName}.customize.yaml`));
} }
// Store original hash for modification detection // Store original hash for modification detection
@ -984,10 +947,10 @@ class ModuleManager {
const copiedFiles = await this.copySidecarToMemory(sourceSidecarPath, agentName, bmadMemoryPath, isUpdate, bmadDir, installer); const copiedFiles = await this.copySidecarToMemory(sourceSidecarPath, agentName, bmadMemoryPath, isUpdate, bmadDir, installer);
if (process.env.BMAD_VERBOSE_INSTALL === 'true' && copiedFiles.length > 0) { if (process.env.BMAD_VERBOSE_INSTALL === 'true' && copiedFiles.length > 0) {
await prompts.log.message(` Sidecar files processed: ${copiedFiles.length} files`); console.log(chalk.dim(` Sidecar files processed: ${copiedFiles.length} files`));
} }
} else if (process.env.BMAD_VERBOSE_INSTALL === 'true') { } else if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.warn(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`); console.log(chalk.yellow(` Warning: Agent marked as having sidecar but ${sidecarDirName} directory not found`));
} }
} }
@ -1006,12 +969,14 @@ class ModuleManager {
// Only show compilation details in verbose mode // Only show compilation details in verbose mode
if (process.env.BMAD_VERBOSE_INSTALL === 'true') { if (process.env.BMAD_VERBOSE_INSTALL === 'true') {
await prompts.log.message( console.log(
chalk.dim(
` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`, ` Compiled agent: ${agentName} -> ${path.relative(targetPath, targetMdPath)}${hasSidecar ? ' (with sidecar)' : ''}`,
),
); );
} }
} catch (error) { } catch (error) {
await prompts.log.warn(` Failed to compile agent ${agentName}: ${error.message}`); console.warn(chalk.yellow(` Failed to compile agent ${agentName}:`, error.message));
} }
} }
} }
@ -1131,11 +1096,11 @@ class ModuleManager {
} }
if (!workflowsVendored) { if (!workflowsVendored) {
await prompts.log.info(`\n Vendoring cross-module workflows for ${moduleName}...`); console.log(chalk.cyan(`\n Vendoring cross-module workflows for ${moduleName}...`));
workflowsVendored = true; workflowsVendored = true;
} }
await prompts.log.message(` Processing: ${agentFile}`); console.log(chalk.dim(` Processing: ${agentFile}`));
for (const item of workflowInstallItems) { for (const item of workflowInstallItems) {
const sourceWorkflowPath = item.workflow; // Where to copy FROM const sourceWorkflowPath = item.workflow; // Where to copy FROM
@ -1147,7 +1112,7 @@ class ModuleManager {
// Or: {project-root}/bmad/bmm/workflows/4-implementation/create-story/workflow.md // Or: {project-root}/bmad/bmm/workflows/4-implementation/create-story/workflow.md
const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:_?bmad)\/([^/]+)\/workflows\/(.+)/); const sourceMatch = sourceWorkflowPath.match(/\{project-root\}\/(?:_?bmad)\/([^/]+)\/workflows\/(.+)/);
if (!sourceMatch) { if (!sourceMatch) {
await prompts.log.warn(` Could not parse workflow path: ${sourceWorkflowPath}`); console.warn(chalk.yellow(` Could not parse workflow path: ${sourceWorkflowPath}`));
continue; continue;
} }
@ -1159,7 +1124,7 @@ class ModuleManager {
// Or: {project-root}/bmad/bmgd/workflows/4-production/create-story/workflow.md // Or: {project-root}/bmad/bmgd/workflows/4-production/create-story/workflow.md
const installMatch = installWorkflowPath.match(/\{project-root\}\/(?:_?bmad)\/([^/]+)\/workflows\/(.+)/); const installMatch = installWorkflowPath.match(/\{project-root\}\/(?:_?bmad)\/([^/]+)\/workflows\/(.+)/);
if (!installMatch) { if (!installMatch) {
await prompts.log.warn(` Could not parse workflow-install path: ${installWorkflowPath}`); console.warn(chalk.yellow(` Could not parse workflow-install path: ${installWorkflowPath}`));
continue; continue;
} }
@ -1173,13 +1138,15 @@ class ModuleManager {
// Check if source workflow exists // Check if source workflow exists
if (!(await fs.pathExists(actualSourceWorkflowPath))) { if (!(await fs.pathExists(actualSourceWorkflowPath))) {
await prompts.log.warn(` Source workflow not found: ${actualSourceWorkflowPath}`); console.warn(chalk.yellow(` Source workflow not found: ${actualSourceWorkflowPath}`));
continue; continue;
} }
// Copy the entire workflow folder // Copy the entire workflow folder
await prompts.log.message( console.log(
chalk.dim(
` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(workflowSuffixPattern, '')}${installModule}/workflows/${installWorkflowSubPath.replace(workflowSuffixPattern, '')}`, ` Vendoring: ${sourceModule}/workflows/${sourceWorkflowSubPath.replace(workflowSuffixPattern, '')}${installModule}/workflows/${installWorkflowSubPath.replace(workflowSuffixPattern, '')}`,
),
); );
await fs.ensureDir(path.dirname(actualDestWorkflowPath)); await fs.ensureDir(path.dirname(actualDestWorkflowPath));
@ -1196,7 +1163,7 @@ class ModuleManager {
} }
if (workflowsVendored) { if (workflowsVendored) {
await prompts.log.success(` Workflow vendoring complete\n`); console.log(chalk.green(` ✓ Workflow vendoring complete\n`));
} }
} }
@ -1218,7 +1185,7 @@ class ModuleManager {
if (updatedContent !== fileContent) { if (updatedContent !== fileContent) {
await fs.writeFile(workflowPath, updatedContent, 'utf8'); await fs.writeFile(workflowPath, updatedContent, 'utf8');
await prompts.log.message(` Updated workflow config to: ${this.bmadFolderName}/${newModuleName}/config.yaml`); console.log(chalk.dim(` Updated workflow config to: ${this.bmadFolderName}/${newModuleName}/config.yaml`));
} }
} }
@ -1234,7 +1201,7 @@ class ModuleManager {
if (moduleName === 'core') { if (moduleName === 'core') {
sourcePath = getSourcePath('core'); sourcePath = getSourcePath('core');
} else { } else {
sourcePath = await this.findModuleSource(moduleName, { silent: options.silent }); sourcePath = await this.findModuleSource(moduleName);
if (!sourcePath) { if (!sourcePath) {
// No source found, skip module installer // No source found, skip module installer
return; return;
@ -1273,11 +1240,11 @@ class ModuleManager {
}); });
if (!result) { if (!result) {
await prompts.log.warn(`Module installer for ${moduleName} returned false`); console.warn(chalk.yellow(`Module installer for ${moduleName} returned false`));
} }
} }
} catch (error) { } catch (error) {
await prompts.log.error(`Error running module installer for ${moduleName}: ${error.message}`); console.error(chalk.red(`Error running module installer for ${moduleName}: ${error.message}`));
} }
} }
@ -1299,7 +1266,7 @@ class ModuleManager {
await fs.writeFile(configPath, configContent, 'utf8'); await fs.writeFile(configPath, configContent, 'utf8');
} catch (error) { } catch (error) {
await prompts.log.warn(`Failed to process module config: ${error.message}`); console.warn(`Failed to process module config:`, error.message);
} }
} }
} }

View File

@ -6,7 +6,7 @@
const fs = require('node:fs'); const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const yaml = require('yaml'); const yaml = require('yaml');
const prompts = require('../prompts'); const readline = require('node:readline');
const { compileAgent, compileAgentFile } = require('./compiler'); const { compileAgent, compileAgentFile } = require('./compiler');
const { extractInstallConfig, getDefaultValues } = require('./template-engine'); const { extractInstallConfig, getDefaultValues } = require('./template-engine');
@ -149,47 +149,83 @@ async function promptInstallQuestions(installConfig, defaults, presetAnswers = {
return { ...defaults, ...presetAnswers }; return { ...defaults, ...presetAnswers };
} }
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const question = (prompt) =>
new Promise((resolve) => {
rl.question(prompt, resolve);
});
const answers = { ...defaults, ...presetAnswers }; const answers = { ...defaults, ...presetAnswers };
await prompts.note(installConfig.description || '', 'Agent Configuration'); console.log('\n📝 Agent Configuration\n');
if (installConfig.description) {
console.log(` ${installConfig.description}\n`);
}
for (const q of installConfig.questions) { for (const q of installConfig.questions) {
// Skip questions for variables that are already set (e.g., custom_name set upfront) // Skip questions for variables that are already set (e.g., custom_name set upfront)
if (answers[q.var] !== undefined && answers[q.var] !== defaults[q.var]) { if (answers[q.var] !== undefined && answers[q.var] !== defaults[q.var]) {
await prompts.log.message(` ${q.var}: ${answers[q.var]} (already set)`); console.log(chalk.dim(` ${q.var}: ${answers[q.var]} (already set)`));
continue; continue;
} }
let response;
switch (q.type) { switch (q.type) {
case 'text': { case 'text': {
const response = await prompts.text({ const defaultHint = q.default ? ` (default: ${q.default})` : '';
message: q.prompt, response = await question(` ${q.prompt}${defaultHint}: `);
default: q.default ?? '', answers[q.var] = response || q.default || '';
});
answers[q.var] = response ?? q.default ?? '';
break; break;
} }
case 'boolean': { case 'boolean': {
const response = await prompts.confirm({ const defaultHint = q.default ? ' [Y/n]' : ' [y/N]';
message: q.prompt, response = await question(` ${q.prompt}${defaultHint}: `);
default: q.default, if (response === '') {
}); answers[q.var] = q.default;
answers[q.var] = response; } else {
answers[q.var] = response.toLowerCase().startsWith('y');
}
break; break;
} }
case 'choice': { case 'choice': {
const response = await prompts.select({ console.log(` ${q.prompt}`);
message: q.prompt, for (const [idx, opt] of q.options.entries()) {
options: q.options.map((o) => ({ value: o.value, label: o.label })), const marker = opt.value === q.default ? '* ' : ' ';
initialValue: q.default, console.log(` ${marker}${idx + 1}. ${opt.label}`);
}); }
answers[q.var] = response; const defaultIdx = q.options.findIndex((o) => o.value === q.default) + 1;
let validChoice = false;
let choiceIdx;
while (!validChoice) {
response = await question(` Choice (default: ${defaultIdx}): `);
if (response) {
choiceIdx = parseInt(response, 10) - 1;
if (isNaN(choiceIdx) || choiceIdx < 0 || choiceIdx >= q.options.length) {
console.log(` Invalid choice. Please enter 1-${q.options.length}`);
} else {
validChoice = true;
}
} else {
choiceIdx = defaultIdx - 1;
validChoice = true;
}
}
answers[q.var] = q.options[choiceIdx].value;
break; break;
} }
// No default // No default
} }
} }
rl.close();
return answers; return answers;
} }

View File

@ -1,6 +1,9 @@
const chalk = require('chalk');
const boxen = require('boxen');
const wrapAnsi = require('wrap-ansi');
const figlet = require('figlet');
const path = require('node:path'); const path = require('node:path');
const os = require('node:os'); const os = require('node:os');
const prompts = require('./prompts');
const CLIUtils = { const CLIUtils = {
/** /**
@ -16,32 +19,27 @@ const CLIUtils = {
}, },
/** /**
* Display BMAD logo using @clack intro + box * Display BMAD logo
* @param {boolean} _clearScreen - Deprecated, ignored (no longer clears screen) * @param {boolean} clearScreen - Whether to clear the screen first (default: true for initial display only)
*/ */
async displayLogo(_clearScreen = true) { displayLogo(clearScreen = true) {
if (clearScreen) {
console.clear();
}
const version = this.getVersion(); const version = this.getVersion();
const color = await prompts.getColor();
// ASCII art logo // ASCII art logo
const logo = [ const logo = `
' ██████╗ ███╗ ███╗ █████╗ ██████╗ ™',
' ██╔══██╗████╗ ████║██╔══██╗██╔══██╗',
' ██████╔╝██╔████╔██║███████║██║ ██║',
' ██╔══██╗██║╚██╔╝██║██╔══██║██║ ██║',
' ██████╔╝██║ ╚═╝ ██║██║ ██║██████╔╝',
' ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═════╝', `;
]
.map((line) => color.yellow(line))
.join('\n');
const tagline = ' Build More, Architect Dreams'; console.log(chalk.cyan(logo));
console.log(chalk.dim(` Build More, Architect Dreams`) + chalk.cyan.bold(` v${version}`) + '\n');
await prompts.box(`${logo}\n${tagline}`, `v${version}`, {
contentAlign: 'center',
rounded: true,
formatBorder: color.blue,
});
}, },
/** /**
@ -49,8 +47,13 @@ const CLIUtils = {
* @param {string} title - Section title * @param {string} title - Section title
* @param {string} subtitle - Optional subtitle * @param {string} subtitle - Optional subtitle
*/ */
async displaySection(title, subtitle = null) { displaySection(title, subtitle = null) {
await prompts.note(subtitle || '', title); console.log('\n' + chalk.cyan('═'.repeat(80)));
console.log(chalk.cyan.bold(` ${title}`));
if (subtitle) {
console.log(chalk.dim(` ${subtitle}`));
}
console.log(chalk.cyan('═'.repeat(80)) + '\n');
}, },
/** /**
@ -58,21 +61,25 @@ const CLIUtils = {
* @param {string|Array} content - Content to display * @param {string|Array} content - Content to display
* @param {Object} options - Box options * @param {Object} options - Box options
*/ */
async displayBox(content, options = {}) { displayBox(content, options = {}) {
const defaultOptions = {
padding: 1,
margin: 1,
borderStyle: 'round',
borderColor: 'cyan',
...options,
};
// Handle array content
let text = content; let text = content;
if (Array.isArray(content)) { if (Array.isArray(content)) {
text = content.join('\n\n'); text = content.join('\n\n');
} }
const color = await prompts.getColor(); // Wrap text to prevent overflow
const borderColor = options.borderColor || 'cyan'; const wrapped = wrapAnsi(text, 76, { hard: true, wordWrap: true });
const colorMap = { green: color.green, red: color.red, yellow: color.yellow, cyan: color.cyan, blue: color.blue };
const formatBorder = colorMap[borderColor] || color.cyan;
await prompts.box(text, options.title, { console.log(boxen(wrapped, defaultOptions));
rounded: options.borderStyle === 'round' || options.borderStyle === undefined,
formatBorder,
});
}, },
/** /**
@ -81,9 +88,14 @@ const CLIUtils = {
* @param {string} header - Custom header from module.yaml * @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml * @param {string} subheader - Custom subheader from module.yaml
*/ */
async displayModuleConfigHeader(moduleName, header = null, subheader = null) { displayModuleConfigHeader(moduleName, header = null, subheader = null) {
const title = header || `Configuring ${moduleName.toUpperCase()} Module`; // Simple blue banner with custom header/subheader if provided
await prompts.note(subheader || '', title); console.log('\n' + chalk.cyan('─'.repeat(80)));
console.log(chalk.cyan(header || `Configuring ${moduleName.toUpperCase()} Module`));
if (subheader) {
console.log(chalk.dim(`${subheader}`));
}
console.log(chalk.cyan('─'.repeat(80)) + '\n');
}, },
/** /**
@ -92,9 +104,14 @@ const CLIUtils = {
* @param {string} header - Custom header from module.yaml * @param {string} header - Custom header from module.yaml
* @param {string} subheader - Custom subheader from module.yaml * @param {string} subheader - Custom subheader from module.yaml
*/ */
async displayModuleNoConfig(moduleName, header = null, subheader = null) { displayModuleNoConfig(moduleName, header = null, subheader = null) {
const title = header || `${moduleName.toUpperCase()} Module - No Custom Configuration`; // Show full banner with header/subheader, just like modules with config
await prompts.note(subheader || '', title); console.log('\n' + chalk.cyan('─'.repeat(80)));
console.log(chalk.cyan(header || `${moduleName.toUpperCase()} Module - No Custom Configuration`));
if (subheader) {
console.log(chalk.dim(`${subheader}`));
}
console.log(chalk.cyan('─'.repeat(80)) + '\n');
}, },
/** /**
@ -103,33 +120,42 @@ const CLIUtils = {
* @param {number} total - Total steps * @param {number} total - Total steps
* @param {string} description - Step description * @param {string} description - Step description
*/ */
async displayStep(current, total, description) { displayStep(current, total, description) {
const progress = `[${current}/${total}]`; const progress = `[${current}/${total}]`;
await prompts.log.step(`${progress} ${description}`); console.log('\n' + chalk.cyan(progress) + ' ' + chalk.bold(description));
console.log(chalk.dim('─'.repeat(80 - progress.length - 1)) + '\n');
}, },
/** /**
* Display completion message * Display completion message
* @param {string} message - Completion message * @param {string} message - Completion message
*/ */
async displayComplete(message) { displayComplete(message) {
const color = await prompts.getColor(); console.log(
await prompts.box(`\u2728 ${message}`, 'Complete', { '\n' +
rounded: true, boxen(chalk.green('✨ ' + message), {
formatBorder: color.green, padding: 1,
}); margin: 1,
borderStyle: 'round',
borderColor: 'green',
}),
);
}, },
/** /**
* Display error message * Display error message
* @param {string} message - Error message * @param {string} message - Error message
*/ */
async displayError(message) { displayError(message) {
const color = await prompts.getColor(); console.log(
await prompts.box(`\u2717 ${message}`, 'Error', { '\n' +
rounded: true, boxen(chalk.red('✗ ' + message), {
formatBorder: color.red, padding: 1,
}); margin: 1,
borderStyle: 'round',
borderColor: 'red',
}),
);
}, },
/** /**
@ -137,7 +163,7 @@ const CLIUtils = {
* @param {Array} items - Items to display * @param {Array} items - Items to display
* @param {string} prefix - Item prefix * @param {string} prefix - Item prefix
*/ */
formatList(items, prefix = '\u2022') { formatList(items, prefix = '') {
return items.map((item) => ` ${prefix} ${item}`).join('\n'); return items.map((item) => ` ${prefix} ${item}`).join('\n');
}, },
@ -152,6 +178,25 @@ const CLIUtils = {
} }
}, },
/**
* Display table
* @param {Array} data - Table data
* @param {Object} options - Table options
*/
displayTable(data, options = {}) {
const Table = require('cli-table3');
const table = new Table({
style: {
head: ['cyan'],
border: ['dim'],
},
...options,
});
for (const row of data) table.push(row);
console.log(table.toString());
},
/** /**
* Display module completion message * Display module completion message
* @param {string} moduleName - Name of the completed module * @param {string} moduleName - Name of the completed module

View File

@ -89,51 +89,11 @@ async function note(message, title) {
/** /**
* Display a spinner for async operations * Display a spinner for async operations
* Wraps @clack/prompts spinner with isSpinning state tracking * @returns {Object} Spinner controller with start, stop, message methods
* @returns {Object} Spinner controller with start, stop, message, error, cancel, clear, isSpinning
*/ */
async function spinner() { async function spinner() {
const clack = await getClack(); const clack = await getClack();
const s = clack.spinner(); return clack.spinner();
let spinning = false;
return {
start: (msg) => {
if (spinning) {
s.message(msg);
} else {
spinning = true;
s.start(msg);
}
},
stop: (msg) => {
if (spinning) {
spinning = false;
s.stop(msg);
}
},
message: (msg) => {
if (spinning) s.message(msg);
},
error: (msg) => {
spinning = false;
s.error(msg);
},
cancel: (msg) => {
spinning = false;
s.cancel(msg);
},
clear: () => {
spinning = false;
s.clear();
},
get isSpinning() {
return spinning;
},
get isCancelled() {
return s.isCancelled;
},
};
} }
/** /**
@ -230,6 +190,31 @@ async function multiselect(options) {
return result; return result;
} }
/**
* Grouped multi-select prompt for categorized options
* @param {Object} options - Prompt options
* @param {string} options.message - The question to ask
* @param {Object} options.options - Object mapping group names to arrays of choices
* @param {Array} [options.initialValues] - Array of initially selected values
* @param {boolean} [options.required=false] - Whether at least one must be selected
* @param {boolean} [options.selectableGroups=false] - Whether groups can be selected as a whole
* @returns {Promise<Array>} Array of selected values
*/
async function groupMultiselect(options) {
const clack = await getClack();
const result = await clack.groupMultiselect({
message: options.message,
options: options.options,
initialValues: options.initialValues,
required: options.required || false,
selectableGroups: options.selectableGroups || false,
});
await handleCancel(result);
return result;
}
/** /**
* Default filter function for autocomplete - case-insensitive label matching * Default filter function for autocomplete - case-insensitive label matching
* @param {string} search - Search string * @param {string} search - Search string
@ -252,7 +237,6 @@ function defaultAutocompleteFilter(search, option) {
* @param {boolean} [options.required=false] - Whether at least one must be selected * @param {boolean} [options.required=false] - Whether at least one must be selected
* @param {number} [options.maxItems=5] - Maximum visible items in scrollable list * @param {number} [options.maxItems=5] - Maximum visible items in scrollable list
* @param {Function} [options.filter] - Custom filter function (search, option) => boolean * @param {Function} [options.filter] - Custom filter function (search, option) => boolean
* @param {Array} [options.lockedValues] - Values that are always selected and cannot be toggled off
* @returns {Promise<Array>} Array of selected values * @returns {Promise<Array>} Array of selected values
*/ */
async function autocompleteMultiselect(options) { async function autocompleteMultiselect(options) {
@ -261,7 +245,6 @@ async function autocompleteMultiselect(options) {
const color = await getPicocolors(); const color = await getPicocolors();
const filterFn = options.filter ?? defaultAutocompleteFilter; const filterFn = options.filter ?? defaultAutocompleteFilter;
const lockedSet = new Set(options.lockedValues || []);
const prompt = new core.AutocompletePrompt({ const prompt = new core.AutocompletePrompt({
options: options.options, options: options.options,
@ -272,7 +255,7 @@ async function autocompleteMultiselect(options) {
return 'Please select at least one item'; return 'Please select at least one item';
} }
}, },
initialValue: [...new Set([...(options.initialValues || []), ...(options.lockedValues || [])])], initialValue: options.initialValues,
render() { render() {
const barColor = this.state === 'error' ? color.yellow : color.cyan; const barColor = this.state === 'error' ? color.yellow : color.cyan;
const bar = barColor(clack.S_BAR); const bar = barColor(clack.S_BAR);
@ -297,17 +280,9 @@ async function autocompleteMultiselect(options) {
// Render option with checkbox // Render option with checkbox
const renderOption = (opt, isHighlighted) => { const renderOption = (opt, isHighlighted) => {
const isSelected = this.selectedValues.includes(opt.value); const isSelected = this.selectedValues.includes(opt.value);
const isLocked = lockedSet.has(opt.value);
const label = opt.label ?? String(opt.value ?? ''); const label = opt.label ?? String(opt.value ?? '');
const hintText = opt.hint && isHighlighted ? color.dim(` (${opt.hint})`) : ''; const hintText = opt.hint && opt.value === this.focusedValue ? color.dim(` (${opt.hint})`) : '';
const checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE);
let checkbox;
if (isLocked) {
checkbox = color.green(clack.S_CHECKBOX_SELECTED);
const lockHint = color.dim(' (always installed)');
return isHighlighted ? `${checkbox} ${label}${lockHint}` : `${checkbox} ${color.dim(label)}${lockHint}`;
}
checkbox = isSelected ? color.green(clack.S_CHECKBOX_SELECTED) : color.dim(clack.S_CHECKBOX_INACTIVE);
return isHighlighted ? `${checkbox} ${label}${hintText}` : `${checkbox} ${color.dim(label)}`; return isHighlighted ? `${checkbox} ${label}${hintText}` : `${checkbox} ${color.dim(label)}`;
}; };
@ -347,18 +322,6 @@ async function autocompleteMultiselect(options) {
}, },
}); });
// Prevent locked values from being toggled off
if (lockedSet.size > 0) {
const originalToggle = prompt.toggleSelected.bind(prompt);
prompt.toggleSelected = function (value) {
// If locked and already selected, skip the toggle (would deselect)
if (lockedSet.has(value) && this.selectedValues.includes(value)) {
return;
}
originalToggle(value);
};
}
// === FIX: Make SPACE always act as selection key (not search input) === // === FIX: Make SPACE always act as selection key (not search input) ===
// Override _isActionKey to treat SPACE like TAB - always an action key // Override _isActionKey to treat SPACE like TAB - always an action key
// This prevents SPACE from being added to the search input // This prevents SPACE from being added to the search input
@ -372,9 +335,8 @@ async function autocompleteMultiselect(options) {
// Handle SPACE toggle when NOT navigating (internal code only handles it when isNavigating=true) // Handle SPACE toggle when NOT navigating (internal code only handles it when isNavigating=true)
prompt.on('key', (char, key) => { prompt.on('key', (char, key) => {
if (key && key.name === 'space' && !prompt.isNavigating) { if (key && key.name === 'space' && !prompt.isNavigating && prompt.focusedValue !== undefined) {
const focused = prompt.filteredOptions[prompt.cursor]; prompt.toggleSelected(prompt.focusedValue);
if (focused) prompt.toggleSelected(focused.value);
} }
}); });
// === END FIX === // === END FIX ===
@ -558,131 +520,6 @@ const log = {
}, },
}; };
/**
* Display cancellation message
* @param {string} [message='Operation cancelled'] - The cancellation message
*/
async function cancel(message = 'Operation cancelled') {
const clack = await getClack();
clack.cancel(message);
}
/**
* Display content in a styled box
* @param {string} content - The box content
* @param {string} [title] - Optional title
* @param {Object} [options] - Box options (contentAlign, titleAlign, width, rounded, formatBorder, etc.)
*/
async function box(content, title, options) {
const clack = await getClack();
clack.box(content, title, options);
}
/**
* Create a progress bar for visualizing task completion
* @param {Object} [options] - Progress options (max, style, etc.)
* @returns {Promise<Object>} Progress controller with start, advance, stop methods
*/
async function progress(options) {
const clack = await getClack();
return clack.progress(options);
}
/**
* Create a task log for displaying scrolling subprocess output
* @param {Object} options - TaskLog options (title, limit, retainLog)
* @returns {Promise<Object>} TaskLog controller with message, success, error methods
*/
async function taskLog(options) {
const clack = await getClack();
return clack.taskLog(options);
}
/**
* File system path prompt with autocomplete
* @param {Object} options - Path options
* @param {string} options.message - The prompt message
* @param {string} [options.initialValue] - Initial path value
* @param {boolean} [options.directory=false] - Only allow directories
* @param {Function} [options.validate] - Validation function
* @returns {Promise<string>} Selected path
*/
async function pathPrompt(options) {
const clack = await getClack();
const result = await clack.path(options);
await handleCancel(result);
return result;
}
/**
* Autocomplete single-select prompt with type-ahead filtering
* @param {Object} options - Autocomplete options
* @param {string} options.message - The prompt message
* @param {Array} options.options - Array of choices [{value, label, hint?}]
* @param {string} [options.placeholder] - Placeholder text
* @param {number} [options.maxItems] - Maximum visible items
* @param {Function} [options.filter] - Custom filter function
* @returns {Promise<any>} Selected value
*/
async function autocomplete(options) {
const clack = await getClack();
const result = await clack.autocomplete(options);
await handleCancel(result);
return result;
}
/**
* Key-based instant selection prompt
* @param {Object} options - SelectKey options
* @param {string} options.message - The prompt message
* @param {Array} options.options - Array of choices [{value, label, hint?}]
* @returns {Promise<any>} Selected value
*/
async function selectKey(options) {
const clack = await getClack();
const result = await clack.selectKey(options);
await handleCancel(result);
return result;
}
/**
* Stream messages with dynamic content (for LLMs, generators, etc.)
*/
const stream = {
async info(generator) {
const clack = await getClack();
return clack.stream.info(generator);
},
async success(generator) {
const clack = await getClack();
return clack.stream.success(generator);
},
async step(generator) {
const clack = await getClack();
return clack.stream.step(generator);
},
async warn(generator) {
const clack = await getClack();
return clack.stream.warn(generator);
},
async error(generator) {
const clack = await getClack();
return clack.stream.error(generator);
},
async message(generator, options) {
const clack = await getClack();
return clack.stream.message(generator, options);
},
};
/**
* Get the color utility (picocolors instance from @clack/prompts)
* @returns {Promise<Object>} The color utility (picocolors)
*/
async function getColor() {
return await getPicocolors();
}
/** /**
* Execute an array of Inquirer-style questions using @clack/prompts * Execute an array of Inquirer-style questions using @clack/prompts
* This provides compatibility with dynamic question arrays * This provides compatibility with dynamic question arrays
@ -782,28 +619,20 @@ async function prompt(questions) {
module.exports = { module.exports = {
getClack, getClack,
getColor,
handleCancel, handleCancel,
intro, intro,
outro, outro,
cancel,
note, note,
box,
spinner, spinner,
progress,
taskLog,
select, select,
multiselect, multiselect,
groupMultiselect,
autocompleteMultiselect, autocompleteMultiselect,
autocomplete,
selectKey,
confirm, confirm,
text, text,
path: pathPrompt,
password, password,
group, group,
tasks, tasks,
log, log,
stream,
prompt, prompt,
}; };

View File

@ -1,3 +1,4 @@
const chalk = require('chalk');
const path = require('node:path'); const path = require('node:path');
const os = require('node:os'); const os = require('node:os');
const fs = require('fs-extra'); const fs = require('fs-extra');
@ -29,12 +30,12 @@ class UI {
* @returns {Object} Installation configuration * @returns {Object} Installation configuration
*/ */
async promptInstall(options = {}) { async promptInstall(options = {}) {
await CLIUtils.displayLogo(); CLIUtils.displayLogo();
// Display version-specific start message from install-messages.yaml // Display version-specific start message from install-messages.yaml
const { MessageLoader } = require('../installers/lib/message-loader'); const { MessageLoader } = require('../installers/lib/message-loader');
const messageLoader = new MessageLoader(); const messageLoader = new MessageLoader();
await messageLoader.displayStartMessage(); messageLoader.displayStartMessage();
// Get directory from options or prompt // Get directory from options or prompt
let confirmedDirectory; let confirmedDirectory;
@ -46,7 +47,7 @@ class UI {
throw new Error(`Invalid directory: ${validation}`); throw new Error(`Invalid directory: ${validation}`);
} }
confirmedDirectory = expandedDir; confirmedDirectory = expandedDir;
await prompts.log.info(`Using directory from command-line: ${confirmedDirectory}`); console.log(chalk.cyan('Using directory from command-line:'), chalk.bold(confirmedDirectory));
} else { } else {
confirmedDirectory = await this.getConfirmedDirectory(); confirmedDirectory = await this.getConfirmedDirectory();
} }
@ -74,7 +75,7 @@ class UI {
for (const entry of entries) { for (const entry of entries) {
if (entry.isDirectory() && (entry.name === '.bmad' || entry.name === 'bmad')) { if (entry.isDirectory() && (entry.name === '.bmad' || entry.name === 'bmad')) {
hasLegacyBmadFolder = true; hasLegacyBmadFolder = true;
legacyBmadPath = path.join(confirmedDirectory, entry.name); legacyBmadPath = path.join(confirmedDirectory, '.bmad');
bmadDir = legacyBmadPath; bmadDir = legacyBmadPath;
// Check if it has _cfg folder // Check if it has _cfg folder
@ -97,30 +98,38 @@ class UI {
// Handle legacy .bmad or _cfg folder - these are very old (v4 or alpha) // Handle legacy .bmad or _cfg folder - these are very old (v4 or alpha)
// Show version warning instead of offering conversion // Show version warning instead of offering conversion
if (hasLegacyBmadFolder || hasLegacyCfg) { if (hasLegacyBmadFolder || hasLegacyCfg) {
await prompts.log.warn('LEGACY INSTALLATION DETECTED'); console.log('');
await prompts.note( console.log(chalk.yellow.bold('⚠️ LEGACY INSTALLATION DETECTED'));
'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder -\n' + console.log(chalk.yellow('─'.repeat(80)));
'this is from an old BMAD version that is out of date for automatic upgrade,\n' + console.log(
'manual intervention required.\n\n' + chalk.yellow(
'You have a legacy version installed (v4 or alpha).\n' + 'Found a ".bmad"/"bmad" folder, or a legacy "_cfg" folder under the bmad folder - this is from a old BMAD version that is out of date for automatic upgrade, manual intervention required.',
'Legacy installations may have compatibility issues.\n\n' + ),
'For the best experience, we strongly recommend:\n' +
' 1. Delete your current BMAD installation folder (.bmad or bmad)\n' +
' 2. Run a fresh installation\n\n' +
'If you do not want to start fresh, you can attempt to proceed beyond this\n' +
'point IF you have ensured the bmad folder is named _bmad, and under it there\n' +
'is a _config folder. If you have a folder under your bmad folder named _cfg,\n' +
'you would need to rename it _config, and then restart the installer.\n\n' +
'Benefits of a fresh install:\n' +
' \u2022 Cleaner configuration without legacy artifacts\n' +
' \u2022 All new features properly configured\n' +
' \u2022 Fewer potential conflicts\n\n' +
'If you have already produced output from an earlier alpha version, you can\n' +
'still retain those artifacts. After installation, ensure you configured during\n' +
'install the proper file locations for artifacts depending on the module you\n' +
'are using, or move the files to the proper locations.',
'Legacy Installation Detected',
); );
console.log(chalk.yellow('You have a legacy version installed (v4 or alpha).'));
console.log('');
console.log(chalk.dim('Legacy installations may have compatibility issues.'));
console.log('');
console.log(chalk.dim('For the best experience, we strongly recommend:'));
console.log(chalk.dim(' 1. Delete your current BMAD installation folder (.bmad or bmad)'));
console.log(
chalk.dim(
' 2. Run a fresh installation\n\nIf you do not want to start fresh, you can attempt to proceed beyond this point IF you have ensured the bmad folder is named _bmad, and under it there is a _config folder. If you have a folder under your bmad folder named _cfg, you would need to rename it _config, and then restart the installer.',
),
);
console.log('');
console.log(chalk.dim('Benefits of a fresh install:'));
console.log(chalk.dim(' • Cleaner configuration without legacy artifacts'));
console.log(chalk.dim(' • All new features properly configured'));
console.log(chalk.dim(' • Fewer potential conflicts'));
console.log(chalk.dim(''));
console.log(
chalk.dim(
'If you have already produced output from an earlier alpha version, you can still retain those artifacts. After installation, ensure you configured during install the proper file locations for artifacts depending on the module you are using, or move the files to the proper locations.',
),
);
console.log(chalk.yellow('─'.repeat(80)));
console.log('');
const proceed = await prompts.select({ const proceed = await prompts.select({
message: 'How would you like to proceed?', message: 'How would you like to proceed?',
@ -138,33 +147,37 @@ class UI {
}); });
if (proceed === 'cancel') { if (proceed === 'cancel') {
await prompts.note('1. Delete the existing bmad folder in your project\n' + "2. Run 'bmad install' again", 'To do a fresh install'); console.log('');
console.log(chalk.cyan('To do a fresh install:'));
console.log(chalk.dim(' 1. Delete the existing bmad folder in your project'));
console.log(chalk.dim(" 2. Run 'bmad install' again"));
console.log('');
process.exit(0); process.exit(0);
return; return;
} }
const s = await prompts.spinner(); const ora = require('ora');
s.start('Updating folder structure...'); const spinner = ora('Updating folder structure...').start();
try { try {
// Handle .bmad folder // Handle .bmad folder
if (hasLegacyBmadFolder) { if (hasLegacyBmadFolder) {
const newBmadPath = path.join(confirmedDirectory, '_bmad'); const newBmadPath = path.join(confirmedDirectory, '_bmad');
await fs.move(legacyBmadPath, newBmadPath); await fs.move(legacyBmadPath, newBmadPath);
bmadDir = newBmadPath; bmadDir = newBmadPath;
s.stop(`Renamed "${path.basename(legacyBmadPath)}" to "_bmad"`); spinner.succeed('Renamed ".bmad" to "_bmad"');
} }
// Handle _cfg folder (either from .bmad or standalone) // Handle _cfg folder (either from .bmad or standalone)
const cfgPath = path.join(bmadDir, '_cfg'); const cfgPath = path.join(bmadDir, '_cfg');
if (await fs.pathExists(cfgPath)) { if (await fs.pathExists(cfgPath)) {
s.start('Renaming configuration folder...'); spinner.start('Renaming configuration folder...');
const newCfgPath = path.join(bmadDir, '_config'); const newCfgPath = path.join(bmadDir, '_config');
await fs.move(cfgPath, newCfgPath); await fs.move(cfgPath, newCfgPath);
s.stop('Renamed "_cfg" to "_config"'); spinner.succeed('Renamed "_cfg" to "_config"');
} }
} catch (error) { } catch (error) {
s.stop('Failed to update folder structure'); spinner.fail('Failed to update folder structure');
await prompts.log.error(`Error: ${error.message}`); console.error(chalk.red(`Error: ${error.message}`));
process.exit(1); process.exit(1);
} }
} }
@ -226,7 +239,7 @@ class UI {
throw new Error(`Invalid action: ${options.action}. Valid actions: ${validActions.join(', ')}`); throw new Error(`Invalid action: ${options.action}. Valid actions: ${validActions.join(', ')}`);
} }
actionType = options.action; actionType = options.action;
await prompts.log.info(`Using action from command-line: ${actionType}`); console.log(chalk.cyan('Using action from command-line:'), chalk.bold(actionType));
} else { } else {
actionType = await prompts.select({ actionType = await prompts.select({
message: 'How would you like to proceed?', message: 'How would you like to proceed?',
@ -261,7 +274,7 @@ class UI {
// Get existing installation info // Get existing installation info
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory); const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
await prompts.log.message(`Found existing modules: ${[...installedModuleIds].join(', ')}`); console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
// Unified module selection - all modules in one grouped multiselect // Unified module selection - all modules in one grouped multiselect
let selectedModules; let selectedModules;
@ -271,13 +284,13 @@ class UI {
.split(',') .split(',')
.map((m) => m.trim()) .map((m) => m.trim())
.filter(Boolean); .filter(Boolean);
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`); console.log(chalk.cyan('Using modules from command-line:'), chalk.bold(selectedModules.join(', ')));
} else { } else {
selectedModules = await this.selectAllModules(installedModuleIds); selectedModules = await this.selectAllModules(installedModuleIds);
selectedModules = selectedModules.filter((m) => m !== 'core');
} }
// After module selection, ask about custom modules // After module selection, ask about custom modules
console.log('');
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } }; let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
if (options.customContent) { if (options.customContent) {
@ -286,7 +299,7 @@ class UI {
.split(',') .split(',')
.map((p) => p.trim()) .map((p) => p.trim())
.filter(Boolean); .filter(Boolean);
await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`); console.log(chalk.cyan('Using custom content from command-line:'), chalk.bold(paths.join(', ')));
// Build custom content config similar to promptCustomContentSource // Build custom content config similar to promptCustomContentSource
const customPaths = []; const customPaths = [];
@ -296,7 +309,7 @@ class UI {
const expandedPath = this.expandUserPath(customPath); const expandedPath = this.expandUserPath(customPath);
const validation = this.validateCustomContentPathSync(expandedPath); const validation = this.validateCustomContentPathSync(expandedPath);
if (validation) { if (validation) {
await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`); console.log(chalk.yellow(`⚠️ Skipping invalid custom content path: ${customPath} - ${validation}`));
continue; continue;
} }
@ -308,12 +321,12 @@ class UI {
const yaml = require('yaml'); const yaml = require('yaml');
moduleMeta = yaml.parse(moduleYaml); moduleMeta = yaml.parse(moduleYaml);
} catch (error) { } catch (error) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`); console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`));
continue; continue;
} }
if (!moduleMeta.code) { if (!moduleMeta.code) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`); console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - module.yaml missing 'code' field`));
continue; continue;
} }
@ -391,11 +404,11 @@ class UI {
.split(',') .split(',')
.map((m) => m.trim()) .map((m) => m.trim())
.filter(Boolean); .filter(Boolean);
await prompts.log.info(`Using modules from command-line: ${selectedModules.join(', ')}`); console.log(chalk.cyan('Using modules from command-line:'), chalk.bold(selectedModules.join(', ')));
} else if (options.yes) { } else if (options.yes) {
// Use default modules when --yes flag is set // Use default modules when --yes flag is set
selectedModules = await this.getDefaultModules(installedModuleIds); selectedModules = await this.getDefaultModules(installedModuleIds);
await prompts.log.info(`Using default modules (--yes flag): ${selectedModules.join(', ')}`); console.log(chalk.cyan('Using default modules (--yes flag):'), chalk.bold(selectedModules.join(', ')));
} else { } else {
selectedModules = await this.selectAllModules(installedModuleIds); selectedModules = await this.selectAllModules(installedModuleIds);
} }
@ -407,7 +420,7 @@ class UI {
.split(',') .split(',')
.map((p) => p.trim()) .map((p) => p.trim())
.filter(Boolean); .filter(Boolean);
await prompts.log.info(`Using custom content from command-line: ${paths.join(', ')}`); console.log(chalk.cyan('Using custom content from command-line:'), chalk.bold(paths.join(', ')));
// Build custom content config similar to promptCustomContentSource // Build custom content config similar to promptCustomContentSource
const customPaths = []; const customPaths = [];
@ -417,7 +430,7 @@ class UI {
const expandedPath = this.expandUserPath(customPath); const expandedPath = this.expandUserPath(customPath);
const validation = this.validateCustomContentPathSync(expandedPath); const validation = this.validateCustomContentPathSync(expandedPath);
if (validation) { if (validation) {
await prompts.log.warn(`Skipping invalid custom content path: ${customPath} - ${validation}`); console.log(chalk.yellow(`⚠️ Skipping invalid custom content path: ${customPath} - ${validation}`));
continue; continue;
} }
@ -429,12 +442,12 @@ class UI {
const yaml = require('yaml'); const yaml = require('yaml');
moduleMeta = yaml.parse(moduleYaml); moduleMeta = yaml.parse(moduleYaml);
} catch (error) { } catch (error) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`); console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - failed to read module.yaml: ${error.message}`));
continue; continue;
} }
if (!moduleMeta.code) { if (!moduleMeta.code) {
await prompts.log.warn(`Skipping custom content path: ${customPath} - module.yaml missing 'code' field`); console.log(chalk.yellow(`⚠️ Skipping custom content path: ${customPath} - module.yaml missing 'code' field`));
continue; continue;
} }
@ -518,7 +531,7 @@ class UI {
const allKnownValues = new Set([...preferredIdes, ...otherIdes].map((ide) => ide.value)); const allKnownValues = new Set([...preferredIdes, ...otherIdes].map((ide) => ide.value));
const unknownTools = configuredIdes.filter((id) => id && typeof id === 'string' && !allKnownValues.has(id)); const unknownTools = configuredIdes.filter((id) => id && typeof id === 'string' && !allKnownValues.has(id));
if (unknownTools.length > 0) { if (unknownTools.length > 0) {
await prompts.log.warn(`Previously configured tools are no longer available: ${unknownTools.join(', ')}`); console.log(chalk.yellow(`⚠️ Previously configured tools are no longer available: ${unknownTools.join(', ')}`));
} }
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
@ -556,20 +569,21 @@ class UI {
const selectedIdes = upgradeSelected || []; const selectedIdes = upgradeSelected || [];
if (selectedIdes.length === 0) { if (selectedIdes.length === 0) {
console.log('');
const confirmNoTools = await prompts.confirm({ const confirmNoTools = await prompts.confirm({
message: 'No tools selected. Continue without installing any tools?', message: 'No tools selected. Continue without installing any tools?',
default: false, default: false,
}); });
if (!confirmNoTools) { if (!confirmNoTools) {
return this.promptToolSelection(projectDir, options); return this.promptToolSelection(projectDir);
} }
return { ides: [], skipIde: true }; return { ides: [], skipIde: true };
} }
// Display selected tools // Display selected tools
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { ides: selectedIdes, skipIde: false }; return { ides: selectedIdes, skipIde: false };
} }
@ -595,25 +609,25 @@ class UI {
if (options.tools) { if (options.tools) {
// Check for explicit "none" value to skip tool installation // Check for explicit "none" value to skip tool installation
if (options.tools.toLowerCase() === 'none') { if (options.tools.toLowerCase() === 'none') {
await prompts.log.info('Skipping tool configuration (--tools none)'); console.log(chalk.cyan('Skipping tool configuration (--tools none)'));
return { ides: [], skipIde: true }; return { ides: [], skipIde: true };
} else { } else {
selectedIdes = options.tools selectedIdes = options.tools
.split(',') .split(',')
.map((t) => t.trim()) .map((t) => t.trim())
.filter(Boolean); .filter(Boolean);
await prompts.log.info(`Using tools from command-line: ${selectedIdes.join(', ')}`); console.log(chalk.cyan('Using tools from command-line:'), chalk.bold(selectedIdes.join(', ')));
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { ides: selectedIdes, skipIde: false }; return { ides: selectedIdes, skipIde: false };
} }
} else if (options.yes) { } else if (options.yes) {
// If --yes flag is set, skip tool prompt and use previously configured tools or empty // If --yes flag is set, skip tool prompt and use previously configured tools or empty
if (configuredIdes.length > 0) { if (configuredIdes.length > 0) {
await prompts.log.info(`Using previously configured tools (--yes flag): ${configuredIdes.join(', ')}`); console.log(chalk.cyan('Using previously configured tools (--yes flag):'), chalk.bold(configuredIdes.join(', ')));
await this.displaySelectedTools(configuredIdes, preferredIdes, allTools); this.displaySelectedTools(configuredIdes, preferredIdes, allTools);
return { ides: configuredIdes, skipIde: false }; return { ides: configuredIdes, skipIde: false };
} else { } else {
await prompts.log.info('Skipping tool configuration (--yes flag, no previous tools)'); console.log(chalk.cyan('Skipping tool configuration (--yes flag, no previous tools)'));
return { ides: [], skipIde: true }; return { ides: [], skipIde: true };
} }
} }
@ -633,6 +647,7 @@ class UI {
// STEP 3: Confirm if no tools selected // STEP 3: Confirm if no tools selected
// ───────────────────────────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────────────────────────
if (selectedIdes.length === 0) { if (selectedIdes.length === 0) {
console.log('');
const confirmNoTools = await prompts.confirm({ const confirmNoTools = await prompts.confirm({
message: 'No tools selected. Continue without installing any tools?', message: 'No tools selected. Continue without installing any tools?',
default: false, default: false,
@ -640,7 +655,7 @@ class UI {
if (!confirmNoTools) { if (!confirmNoTools) {
// User wants to select tools - recurse // User wants to select tools - recurse
return this.promptToolSelection(projectDir, options); return this.promptToolSelection(projectDir);
} }
return { return {
@ -650,7 +665,7 @@ class UI {
} }
// Display selected tools // Display selected tools
await this.displaySelectedTools(selectedIdes, preferredIdes, allTools); this.displaySelectedTools(selectedIdes, preferredIdes, allTools);
return { return {
ides: selectedIdes, ides: selectedIdes,
@ -693,12 +708,15 @@ class UI {
* Display installation summary * Display installation summary
* @param {Object} result - Installation result * @param {Object} result - Installation result
*/ */
async showInstallSummary(result) { showInstallSummary(result) {
let summary = `Installed to: ${result.path}`; // Clean, simple completion message
console.log('\n' + chalk.green.bold('✨ BMAD is ready to use!'));
// Show installation summary in a simple format
console.log(chalk.dim(`Installed to: ${result.path}`));
if (result.modules && result.modules.length > 0) { if (result.modules && result.modules.length > 0) {
summary += `\nModules: ${result.modules.join(', ')}`; console.log(chalk.dim(`Modules: ${result.modules.join(', ')}`));
} }
await prompts.note(summary, 'BMAD is ready to use!');
} }
/** /**
@ -751,19 +769,19 @@ class UI {
const coreConfig = {}; const coreConfig = {};
if (options.userName) { if (options.userName) {
coreConfig.user_name = options.userName; coreConfig.user_name = options.userName;
await prompts.log.info(`Using user name from command-line: ${options.userName}`); console.log(chalk.cyan('Using user name from command-line:'), chalk.bold(options.userName));
} }
if (options.communicationLanguage) { if (options.communicationLanguage) {
coreConfig.communication_language = options.communicationLanguage; coreConfig.communication_language = options.communicationLanguage;
await prompts.log.info(`Using communication language from command-line: ${options.communicationLanguage}`); console.log(chalk.cyan('Using communication language from command-line:'), chalk.bold(options.communicationLanguage));
} }
if (options.documentOutputLanguage) { if (options.documentOutputLanguage) {
coreConfig.document_output_language = options.documentOutputLanguage; coreConfig.document_output_language = options.documentOutputLanguage;
await prompts.log.info(`Using document output language from command-line: ${options.documentOutputLanguage}`); console.log(chalk.cyan('Using document output language from command-line:'), chalk.bold(options.documentOutputLanguage));
} }
if (options.outputFolder) { if (options.outputFolder) {
coreConfig.output_folder = options.outputFolder; coreConfig.output_folder = options.outputFolder;
await prompts.log.info(`Using output folder from command-line: ${options.outputFolder}`); console.log(chalk.cyan('Using output folder from command-line:'), chalk.bold(options.outputFolder));
} }
// Load existing config to merge with provided options // Load existing config to merge with provided options
@ -800,7 +818,7 @@ class UI {
document_output_language: 'English', document_output_language: 'English',
output_folder: '_bmad-output', output_folder: '_bmad-output',
}; };
await prompts.log.info('Using default configuration (--yes flag)'); console.log(chalk.cyan('Using default configuration (--yes flag)'));
} }
} else { } else {
// Load existing configs first if they exist // Load existing configs first if they exist
@ -821,11 +839,11 @@ class UI {
* @returns {Array} Module choices for prompt * @returns {Array} Module choices for prompt
*/ */
async getModuleChoices(installedModuleIds, customContentConfig = null) { async getModuleChoices(installedModuleIds, customContentConfig = null) {
const color = await prompts.getColor();
const moduleChoices = []; const moduleChoices = [];
const isNewInstallation = installedModuleIds.size === 0; const isNewInstallation = installedModuleIds.size === 0;
const customContentItems = []; const customContentItems = [];
const hasCustomContentItems = false;
// Add custom content items // Add custom content items
if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) { if (customContentConfig && customContentConfig.hasCustomContent && customContentConfig.customPath) {
@ -837,7 +855,7 @@ class UI {
const customInfo = await customHandler.getCustomInfo(customFile); const customInfo = await customHandler.getCustomInfo(customFile);
if (customInfo) { if (customInfo) {
customContentItems.push({ customContentItems.push({
name: `${color.cyan('\u2713')} ${customInfo.name} ${color.dim(`(${customInfo.relativePath})`)}`, name: `${chalk.cyan('✓')} ${customInfo.name} ${chalk.gray(`(${customInfo.relativePath})`)}`,
value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content value: `__CUSTOM_CONTENT__${customFile}`, // Unique value for each custom content
checked: true, // Default to selected since user chose to provide custom content checked: true, // Default to selected since user chose to provide custom content
path: customInfo.path, // Track path to avoid duplicates path: customInfo.path, // Track path to avoid duplicates
@ -865,7 +883,7 @@ class UI {
if (!isDuplicate) { if (!isDuplicate) {
allCustomModules.push({ allCustomModules.push({
name: `${color.cyan('\u2713')} ${mod.name} ${color.dim('(cached)')}`, 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),
hint: mod.description || undefined, hint: mod.description || undefined,
@ -916,20 +934,22 @@ class UI {
...choicesWithDefaults, ...choicesWithDefaults,
{ {
value: '__NONE__', value: '__NONE__',
label: '\u26A0 None / I changed my mind - skip module installation', label: ' None / I changed my mind - skip module installation',
checked: false, checked: false,
}, },
]; ];
const selected = await prompts.multiselect({ const selected = await prompts.multiselect({
message: 'Select modules to install (use arrow keys, space to toggle):', message: `Select modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
choices: choicesWithSkipOption, choices: choicesWithSkipOption,
required: true, required: true,
}); });
// If user selected both "__NONE__" and other items, honor the "None" choice // If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) { if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no modules will be installed.'); console.log();
console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no modules will be installed.'));
console.log();
return []; return [];
} }
@ -962,7 +982,8 @@ class UI {
*/ */
async selectExternalModules(externalModuleChoices, defaultSelections = []) { async selectExternalModules(externalModuleChoices, defaultSelections = []) {
// Build a message showing available modules // Build a message showing available modules
const message = 'Select official BMad modules to install (use arrow keys, space to toggle):'; const availableNames = externalModuleChoices.map((c) => c.name).join(', ');
const message = `Select official BMad modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`;
// Mark choices as checked based on defaultSelections // Mark choices as checked based on defaultSelections
const choicesWithDefaults = externalModuleChoices.map((choice) => ({ const choicesWithDefaults = externalModuleChoices.map((choice) => ({
@ -988,7 +1009,9 @@ class UI {
// If user selected both "__NONE__" and other items, honor the "None" choice // If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) { if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no external modules will be installed.'); console.log();
console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no external modules will be installed.'));
console.log();
return []; return [];
} }
@ -1010,98 +1033,100 @@ class UI {
const externalManager = new ExternalModuleManager(); const externalManager = new ExternalModuleManager();
const externalModules = await externalManager.listAvailable(); const externalModules = await externalManager.listAvailable();
// Build flat options list with group hints for autocompleteMultiselect // Build grouped options
const allOptions = []; const groupedOptions = {};
const initialValues = []; const initialValues = [];
const lockedValues = ['core'];
// Core module is always installed — show it locked at the top
allOptions.push({ label: 'BMad Core Module', value: 'core', hint: 'Core configuration and shared resources' });
initialValues.push('core');
// Helper to build module entry with proper sorting and selection // Helper to build module entry with proper sorting and selection
const buildModuleEntry = (mod, value, group) => { const buildModuleEntry = (mod, value) => {
const isInstalled = installedModuleIds.has(value); const isInstalled = installedModuleIds.has(value);
const isDefault = mod.defaultSelected === true;
return { return {
label: mod.name, label: mod.description ? `${mod.name}${mod.description}` : mod.name,
value, value,
hint: mod.description || group, // For sorting: defaultSelected=0, others=1
// Pre-select only if already installed (not on fresh install) sortKey: isDefault ? 0 : 1,
selected: isInstalled, // Pre-select if default selected OR already installed
selected: isDefault || isInstalled,
}; };
}; };
// Local modules (BMM, BMB, etc.) // Group 1: BMad Core (BMM, BMB)
const localEntries = []; const coreModules = [];
for (const mod of localModules) { for (const mod of localModules) {
if (!mod.isCustom && mod.id !== 'core') { if (!mod.isCustom && (mod.id === 'bmm' || mod.id === 'bmb')) {
const entry = buildModuleEntry(mod, mod.id, 'Local'); const entry = buildModuleEntry(mod, mod.id);
localEntries.push(entry); coreModules.push(entry);
if (entry.selected) { if (entry.selected) {
initialValues.push(mod.id); initialValues.push(mod.id);
} }
} }
} }
allOptions.push(...localEntries.map(({ label, value, hint }) => ({ label, value, hint }))); // Sort: defaultSelected first, then others
coreModules.sort((a, b) => a.sortKey - b.sortKey);
// Remove sortKey from final entries
if (coreModules.length > 0) {
groupedOptions['BMad Core'] = coreModules.map(({ label, value }) => ({ label, value }));
}
// Group 2: BMad Official Modules (type: bmad-org) // Group 2: BMad Official Modules (type: bmad-org)
const officialModules = []; const officialModules = [];
for (const mod of externalModules) { for (const mod of externalModules) {
if (mod.type === 'bmad-org') { if (mod.type === 'bmad-org') {
const entry = buildModuleEntry(mod, mod.code, 'Official'); const entry = buildModuleEntry(mod, mod.code);
officialModules.push(entry); officialModules.push(entry);
if (entry.selected) { if (entry.selected) {
initialValues.push(mod.code); initialValues.push(mod.code);
} }
} }
} }
allOptions.push(...officialModules.map(({ label, value, hint }) => ({ label, value, hint }))); officialModules.sort((a, b) => a.sortKey - b.sortKey);
if (officialModules.length > 0) {
groupedOptions['BMad Official Modules'] = officialModules.map(({ label, value }) => ({ label, value }));
}
// Group 3: Community Modules (type: community) // Group 3: Community Modules (type: community)
const communityModules = []; const communityModules = [];
for (const mod of externalModules) { for (const mod of externalModules) {
if (mod.type === 'community') { if (mod.type === 'community') {
const entry = buildModuleEntry(mod, mod.code, 'Community'); const entry = buildModuleEntry(mod, mod.code);
communityModules.push(entry); communityModules.push(entry);
if (entry.selected) { if (entry.selected) {
initialValues.push(mod.code); initialValues.push(mod.code);
} }
} }
} }
allOptions.push(...communityModules.map(({ label, value, hint }) => ({ label, value, hint })), { communityModules.sort((a, b) => a.sortKey - b.sortKey);
// "None" option at the end if (communityModules.length > 0) {
label: '\u26A0 None - Skip module installation', groupedOptions['Community Modules'] = communityModules.map(({ label, value }) => ({ label, value }));
value: '__NONE__', }
});
const selected = await prompts.autocompleteMultiselect({ // Add "None" option at the end
message: 'Select modules to install:', groupedOptions[' '] = [
options: allOptions, {
label: '⚠ None - Skip module installation',
value: '__NONE__',
},
];
const selected = await prompts.groupMultiselect({
message: `Select modules to install ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
options: groupedOptions,
initialValues: initialValues.length > 0 ? initialValues : undefined, initialValues: initialValues.length > 0 ? initialValues : undefined,
lockedValues,
required: true, required: true,
maxItems: allOptions.length, selectableGroups: false,
}); });
// If user selected both "__NONE__" and other items, honor the "None" choice // If user selected both "__NONE__" and other items, honor the "None" choice
if (selected && selected.includes('__NONE__') && selected.length > 1) { if (selected && selected.includes('__NONE__') && selected.length > 1) {
await prompts.log.warn('"None" was selected, so no modules will be installed.'); console.log();
console.log(chalk.yellow('⚠️ "None" was selected, so no modules will be installed.'));
console.log();
return []; return [];
} }
// Filter out the special '__NONE__' value // Filter out the special '__NONE__' value
const result = selected ? selected.filter((m) => m !== '__NONE__') : []; return selected ? selected.filter((m) => m !== '__NONE__') : [];
// Display selected modules as bulleted list
if (result.length > 0) {
const moduleLines = result.map((moduleId) => {
const opt = allOptions.find((o) => o.value === moduleId);
return ` \u2022 ${opt?.label || moduleId}`;
});
await prompts.log.message('Selected modules:\n' + moduleLines.join('\n'));
}
return result;
} }
/** /**
@ -1160,7 +1185,7 @@ class UI {
* @param {string} directory - The directory path * @param {string} directory - The directory path
*/ */
async displayDirectoryInfo(directory) { async displayDirectoryInfo(directory) {
await prompts.log.info(`Resolved installation path: ${directory}`); console.log(chalk.cyan('\nResolved installation path:'), chalk.bold(directory));
const dirExists = await fs.pathExists(directory); const dirExists = await fs.pathExists(directory);
if (dirExists) { if (dirExists) {
@ -1176,10 +1201,12 @@ class UI {
const hasBmadInstall = const hasBmadInstall =
(await fs.pathExists(bmadResult.bmadDir)) && (await fs.pathExists(path.join(bmadResult.bmadDir, '_config', 'manifest.yaml'))); (await fs.pathExists(bmadResult.bmadDir)) && (await fs.pathExists(path.join(bmadResult.bmadDir, '_config', 'manifest.yaml')));
const bmadNote = hasBmadInstall ? ` including existing BMAD installation (${path.basename(bmadResult.bmadDir)})` : ''; console.log(
await prompts.log.message(`Directory exists and contains ${files.length} item(s)${bmadNote}`); chalk.gray(`Directory exists and contains ${files.length} item(s)`) +
(hasBmadInstall ? chalk.yellow(` including existing BMAD installation (${path.basename(bmadResult.bmadDir)})`) : ''),
);
} else { } else {
await prompts.log.message('Directory exists and is empty'); console.log(chalk.gray('Directory exists and is empty'));
} }
} }
} }
@ -1200,7 +1227,7 @@ class UI {
}); });
if (!proceed) { if (!proceed) {
await prompts.log.warn("Let's try again with a different path."); console.log(chalk.yellow("\nLet's try again with a different path.\n"));
} }
return proceed; return proceed;
@ -1212,7 +1239,7 @@ class UI {
}); });
if (!create) { if (!create) {
await prompts.log.warn("Let's try again with a different path."); console.log(chalk.yellow("\nLet's try again with a different path.\n"));
} }
return create; return create;
@ -1432,7 +1459,7 @@ class UI {
return configs; return configs;
} catch { } catch {
// If loading fails, return empty configs // If loading fails, return empty configs
await prompts.log.warn('Could not load existing configurations'); console.warn('Warning: Could not load existing configurations');
return configs; return configs;
} }
} }
@ -1563,7 +1590,7 @@ class UI {
name: moduleData.name || moduleData.code, name: moduleData.name || moduleData.code,
}); });
await prompts.log.success(`Confirmed local custom module: ${moduleData.name || moduleData.code}`); console.log(chalk.green(`Confirmed local custom module: ${moduleData.name || moduleData.code}`));
} }
// Ask if user wants to add these to the installation // Ask if user wants to add these to the installation
@ -1629,11 +1656,11 @@ class UI {
}; };
// Ask user about custom modules // Ask user about custom modules
await prompts.log.info('Custom Modules'); console.log(chalk.cyan('\n⚙ Custom Modules'));
if (cachedCustomModules.length > 0) { if (cachedCustomModules.length > 0) {
await prompts.log.message('Found custom modules in your installation:'); console.log(chalk.dim('Found custom modules in your installation:'));
} else { } else {
await prompts.log.message('No custom modules currently installed.'); console.log(chalk.dim('No custom modules currently installed.'));
} }
// Build choices dynamically based on whether we have existing modules // Build choices dynamically based on whether we have existing modules
@ -1659,14 +1686,14 @@ class UI {
case 'keep': { case 'keep': {
// Keep all existing custom modules // Keep all existing custom modules
result.selectedCustomModules = cachedCustomModules.map((m) => m.id); result.selectedCustomModules = cachedCustomModules.map((m) => m.id);
await prompts.log.message(`Keeping ${result.selectedCustomModules.length} custom module(s)`); console.log(chalk.dim(`Keeping ${result.selectedCustomModules.length} custom module(s)`));
break; break;
} }
case 'select': { case 'select': {
// Let user choose which to keep // Let user choose which to keep
const selectChoices = cachedCustomModules.map((m) => ({ const selectChoices = cachedCustomModules.map((m) => ({
name: `${m.name} (${m.id})`, name: `${m.name} ${chalk.gray(`(${m.id})`)}`,
value: m.id, value: m.id,
checked: m.checked, checked: m.checked,
})); }));
@ -1682,14 +1709,16 @@ class UI {
]; ];
const keepModules = await prompts.multiselect({ const keepModules = await prompts.multiselect({
message: 'Select custom modules to keep (use arrow keys, space to toggle):', message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
choices: choicesWithSkip, choices: choicesWithSkip,
required: true, required: true,
}); });
// If user selected both "__NONE__" and other modules, honor the "None" choice // If user selected both "__NONE__" and other modules, honor the "None" choice
if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) { if (keepModules && keepModules.includes('__NONE__') && keepModules.length > 1) {
await prompts.log.warn('"None / I changed my mind" was selected, so no custom modules will be kept.'); console.log();
console.log(chalk.yellow('⚠️ "None / I changed my mind" was selected, so no custom modules will be kept.'));
console.log();
result.selectedCustomModules = []; result.selectedCustomModules = [];
} else { } else {
// Filter out the special '__NONE__' value // Filter out the special '__NONE__' value
@ -1714,13 +1743,13 @@ class UI {
case 'remove': { case 'remove': {
// Remove all custom modules // Remove all custom modules
await prompts.log.warn('All custom modules will be removed from the installation'); console.log(chalk.yellow('All custom modules will be removed from the installation'));
break; break;
} }
case 'cancel': { case 'cancel': {
// User cancelled - no custom modules // User cancelled - no custom modules
await prompts.log.message('No custom modules will be added'); console.log(chalk.dim('No custom modules will be added'));
break; break;
} }
} }
@ -1753,26 +1782,30 @@ class UI {
return true; // Not legacy, proceed return true; // Not legacy, proceed
} }
let warningContent; console.log('');
console.log(chalk.yellow.bold('⚠️ VERSION WARNING'));
console.log(chalk.yellow('─'.repeat(80)));
if (installedVersion === 'unknown') { if (installedVersion === 'unknown') {
warningContent = 'Unable to detect your installed BMAD version.\n' + 'This appears to be a legacy or unsupported installation.'; console.log(chalk.yellow('Unable to detect your installed BMAD version.'));
console.log(chalk.yellow('This appears to be a legacy or unsupported installation.'));
} else { } else {
warningContent = console.log(chalk.yellow(`You are updating from ${installedVersion} to ${currentVersion}.`));
`You are updating from ${installedVersion} to ${currentVersion}.\n` + 'You have a legacy version installed (v4 or alpha).'; console.log(chalk.yellow('You have a legacy version installed (v4 or alpha).'));
} }
warningContent += console.log('');
'\n\nFor the best experience, we recommend:\n' + console.log(chalk.dim('For the best experience, we recommend:'));
' 1. Delete your current BMAD installation folder\n' + console.log(chalk.dim(' 1. Delete your current BMAD installation folder'));
` (the "${bmadFolderName}/" folder in your project)\n` + console.log(chalk.dim(` (the "${bmadFolderName}/" folder in your project)`));
' 2. Run a fresh installation\n\n' + console.log(chalk.dim(' 2. Run a fresh installation'));
'Benefits of a fresh install:\n' + console.log('');
' \u2022 Cleaner configuration without legacy artifacts\n' + console.log(chalk.dim('Benefits of a fresh install:'));
' \u2022 All new features properly configured\n' + console.log(chalk.dim(' • Cleaner configuration without legacy artifacts'));
' \u2022 Fewer potential conflicts'; console.log(chalk.dim(' • All new features properly configured'));
console.log(chalk.dim(' • Fewer potential conflicts'));
await prompts.log.warn('VERSION WARNING'); console.log(chalk.yellow('─'.repeat(80)));
await prompts.note(warningContent, 'Version Warning'); console.log('');
const proceed = await prompts.select({ const proceed = await prompts.select({
message: 'How would you like to proceed?', message: 'How would you like to proceed?',
@ -1790,10 +1823,11 @@ class UI {
}); });
if (proceed === 'cancel') { if (proceed === 'cancel') {
await prompts.note( console.log('');
`1. Delete the "${bmadFolderName}/" folder in your project\n` + "2. Run 'bmad install' again", console.log(chalk.cyan('To do a fresh install:'));
'To do a fresh install', console.log(chalk.dim(` 1. Delete the "${bmadFolderName}/" folder in your project`));
); console.log(chalk.dim(" 2. Run 'bmad install' again"));
console.log('');
} }
return proceed === 'proceed'; return proceed === 'proceed';
@ -1804,34 +1838,41 @@ class UI {
* @param {Array} modules - Array of module info objects with version info * @param {Array} modules - Array of module info objects with version info
* @param {Array} availableUpdates - Array of available updates * @param {Array} availableUpdates - Array of available updates
*/ */
async displayModuleVersions(modules, availableUpdates = []) { displayModuleVersions(modules, availableUpdates = []) {
console.log('');
console.log(chalk.cyan.bold('📦 Module Versions'));
console.log(chalk.gray('─'.repeat(80)));
// Group modules by source // Group modules by source
const builtIn = modules.filter((m) => m.source === 'built-in'); const builtIn = modules.filter((m) => m.source === 'built-in');
const external = modules.filter((m) => m.source === 'external'); const external = modules.filter((m) => m.source === 'external');
const custom = modules.filter((m) => m.source === 'custom'); const custom = modules.filter((m) => m.source === 'custom');
const unknown = modules.filter((m) => m.source === 'unknown'); const unknown = modules.filter((m) => m.source === 'unknown');
const lines = []; const displayGroup = (group, title) => {
const formatGroup = (group, title) => {
if (group.length === 0) return; if (group.length === 0) return;
lines.push(title);
for (const mod of group) { console.log(chalk.yellow(`\n${title}`));
const updateInfo = availableUpdates.find((u) => u.name === mod.name); for (const module of group) {
const versionDisplay = mod.version || 'unknown'; const updateInfo = availableUpdates.find((u) => u.name === module.name);
const versionDisplay = module.version || chalk.gray('unknown');
if (updateInfo) { if (updateInfo) {
lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2192 ${updateInfo.latestVersion} \u2191`); console.log(
` ${chalk.cyan(module.name.padEnd(20))} ${versionDisplay}${chalk.green(updateInfo.latestVersion)} ${chalk.green('↑')}`,
);
} else { } else {
lines.push(` ${mod.name.padEnd(20)} ${versionDisplay} \u2713`); console.log(` ${chalk.cyan(module.name.padEnd(20))} ${versionDisplay} ${chalk.gray('✓')}`);
} }
} }
}; };
formatGroup(builtIn, 'Built-in Modules'); displayGroup(builtIn, 'Built-in Modules');
formatGroup(external, 'External Modules (Official)'); displayGroup(external, 'External Modules (Official)');
formatGroup(custom, 'Custom Modules'); displayGroup(custom, 'Custom Modules');
formatGroup(unknown, 'Other Modules'); displayGroup(unknown, 'Other Modules');
await prompts.note(lines.join('\n'), 'Module Versions'); console.log('');
} }
/** /**
@ -1844,10 +1885,12 @@ class UI {
return []; return [];
} }
await prompts.log.info('Available Updates'); console.log('');
console.log(chalk.cyan.bold('🔄 Available Updates'));
console.log(chalk.gray('─'.repeat(80)));
const choices = availableUpdates.map((update) => ({ const choices = availableUpdates.map((update) => ({
name: `${update.name} (v${update.installedVersion} \u2192 v${update.latestVersion})`, name: `${update.name} ${chalk.dim(`(v${update.installedVersion} v${update.latestVersion})`)}`,
value: update.name, value: update.name,
checked: true, // Default to selecting all updates checked: true, // Default to selecting all updates
})); }));
@ -1873,7 +1916,7 @@ class UI {
// Allow specific selection // Allow specific selection
const selected = await prompts.multiselect({ const selected = await prompts.multiselect({
message: 'Select modules to update (use arrow keys, space to toggle):', message: `Select modules to update ${chalk.dim('(↑/↓ navigates, SPACE toggles, ENTER to confirm)')}:`,
choices: choices, choices: choices,
required: true, required: true,
}); });
@ -1885,29 +1928,34 @@ class UI {
* Display status of all installed modules * Display status of all installed modules
* @param {Object} statusData - Status data with modules, installation info, and available updates * @param {Object} statusData - Status data with modules, installation info, and available updates
*/ */
async displayStatus(statusData) { displayStatus(statusData) {
const { installation, modules, availableUpdates, bmadDir } = statusData; const { installation, modules, availableUpdates, bmadDir } = statusData;
// Installation info console.log('');
const infoLines = [ console.log(chalk.cyan.bold('📋 BMAD Status'));
`Version: ${installation.version || 'unknown'}`, console.log(chalk.gray('─'.repeat(80)));
`Location: ${bmadDir}`,
`Installed: ${new Date(installation.installDate).toLocaleDateString()}`,
`Last Updated: ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : 'unknown'}`,
];
await prompts.note(infoLines.join('\n'), 'BMAD Status'); // Installation info
console.log(chalk.yellow('\nInstallation'));
console.log(` ${chalk.gray('Version:'.padEnd(20))} ${installation.version || chalk.gray('unknown')}`);
console.log(` ${chalk.gray('Location:'.padEnd(20))} ${bmadDir}`);
console.log(` ${chalk.gray('Installed:'.padEnd(20))} ${new Date(installation.installDate).toLocaleDateString()}`);
console.log(
` ${chalk.gray('Last Updated:'.padEnd(20))} ${installation.lastUpdated ? new Date(installation.lastUpdated).toLocaleDateString() : chalk.gray('unknown')}`,
);
// Module versions // Module versions
await this.displayModuleVersions(modules, availableUpdates); this.displayModuleVersions(modules, availableUpdates);
// Update summary // Update summary
if (availableUpdates.length > 0) { if (availableUpdates.length > 0) {
await prompts.log.warn(`${availableUpdates.length} update(s) available`); console.log(chalk.yellow.bold(`\n⚠️ ${availableUpdates.length} update(s) available`));
await prompts.log.message('Run \'bmad install\' and select "Quick Update" to update'); console.log(chalk.dim(` Run 'bmad install' and select "Quick Update" to update`));
} else { } else {
await prompts.log.success('All modules are up to date'); console.log(chalk.green.bold('\n✓ All modules are up to date'));
} }
console.log('');
} }
/** /**
@ -1916,17 +1964,19 @@ class UI {
* @param {Array} preferredIdes - Array of preferred IDE objects * @param {Array} preferredIdes - Array of preferred IDE objects
* @param {Array} allTools - Array of all tool objects * @param {Array} allTools - Array of all tool objects
*/ */
async displaySelectedTools(selectedIdes, preferredIdes, allTools) { displaySelectedTools(selectedIdes, preferredIdes, allTools) {
if (selectedIdes.length === 0) return; if (selectedIdes.length === 0) return;
const preferredValues = new Set(preferredIdes.map((ide) => ide.value)); const preferredValues = new Set(preferredIdes.map((ide) => ide.value));
const toolLines = selectedIdes.map((ideValue) => {
console.log('');
console.log(chalk.dim(' Selected tools:'));
for (const ideValue of selectedIdes) {
const tool = allTools.find((t) => t.value === ideValue); const tool = allTools.find((t) => t.value === ideValue);
const name = tool?.name || ideValue; const name = tool?.name || ideValue;
const marker = preferredValues.has(ideValue) ? ' \u2B50' : ''; const marker = preferredValues.has(ideValue) ? ' ⭐' : '';
return ` \u2022 ${name}${marker}`; console.log(chalk.dim(`${name}${marker}`));
}); }
await prompts.log.message('Selected tools:\n' + toolLines.join('\n'));
} }
} }

View File

@ -20,8 +20,11 @@ const path = require('node:path');
const DOCS_ROOT = path.resolve(__dirname, '../docs'); const DOCS_ROOT = path.resolve(__dirname, '../docs');
const DRY_RUN = !process.argv.includes('--write'); const DRY_RUN = !process.argv.includes('--write');
// Match all markdown links; filtering (external, anchors, assets) happens in convertToRepoRelative. // Regex to match markdown links:
// This intentionally matches broadly so the handler can make context-aware decisions. // - [text](path.md) or [text](path.md#anchor) - existing .md links
// - [text](/path/to/page/) or [text](/path/to/page/#anchor) - site-relative links to convert
const MARKDOWN_LINK_REGEX = /\[([^\]]*)\]\(([^)]+(?:\.md|\/))(?:#[^)]*)?(?:\?[^)]*)?\)/g;
// Simpler approach: match all markdown links and filter in the handler
const ALL_MARKDOWN_LINKS_REGEX = /\[([^\]]*)\]\(([^)]+)\)/g; const ALL_MARKDOWN_LINKS_REGEX = /\[([^\]]*)\]\(([^)]+)\)/g;
/** /**
@ -61,8 +64,8 @@ function getMarkdownFiles(dir) {
* @returns {string|null} - Repo-relative path (e.g., "/docs/path/to/file.md"), or null if shouldn't be converted * @returns {string|null} - Repo-relative path (e.g., "/docs/path/to/file.md"), or null if shouldn't be converted
*/ */
function convertToRepoRelative(href, currentFilePath) { function convertToRepoRelative(href, currentFilePath) {
// Skip external links (including protocol-relative URLs like //cdn.example.com) // Skip external links
if (href.includes('://') || href.startsWith('//') || href.startsWith('mailto:') || href.startsWith('tel:')) { if (href.includes('://') || href.startsWith('mailto:') || href.startsWith('tel:')) {
return null; return null;
} }

View File

@ -67,11 +67,11 @@ platforms:
category: ide category: ide
description: "Atlassian's Rovo development environment" description: "Atlassian's Rovo development environment"
kiro: kiro-cli:
name: "Kiro" name: "Kiro CLI"
preferred: false preferred: false
category: ide category: cli
description: "Amazon's AI-powered IDE" description: "Kiro command-line interface"
github-copilot: github-copilot:
name: "GitHub Copilot" name: "GitHub Copilot"

View File

@ -21,8 +21,8 @@ const path = require('node:path');
const DOCS_ROOT = path.resolve(__dirname, '../docs'); const DOCS_ROOT = path.resolve(__dirname, '../docs');
const DRY_RUN = !process.argv.includes('--write'); const DRY_RUN = !process.argv.includes('--write');
// Regex to match markdown links with site-relative paths or bare .md references // Regex to match markdown links with site-relative paths
const LINK_REGEX = /\[([^\]]*)\]\(((?:\.{1,2}\/|\/)[^)]+|[\w][^)\s]*\.md(?:[?#][^)]*)?)\)/g; const LINK_REGEX = /\[([^\]]*)\]\((\/[^)]+)\)/g;
// File extensions that are static assets, not markdown docs // File extensions that are static assets, not markdown docs
const STATIC_ASSET_EXTENSIONS = ['.zip', '.txt', '.pdf', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico']; const STATIC_ASSET_EXTENSIONS = ['.zip', '.txt', '.pdf', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.webp', '.ico'];
@ -108,27 +108,11 @@ function extractAnchors(content) {
* /docs/how-to/installation/install-bmad.md -> docs/how-to/installation/install-bmad.md * /docs/how-to/installation/install-bmad.md -> docs/how-to/installation/install-bmad.md
* /how-to/installation/install-bmad/ -> docs/how-to/installation/install-bmad.md or .../index.md * /how-to/installation/install-bmad/ -> docs/how-to/installation/install-bmad.md or .../index.md
*/ */
function resolveLink(siteRelativePath, sourceFile) { function resolveLink(siteRelativePath) {
// Strip anchor and query // Strip anchor and query
let checkPath = siteRelativePath.split('#')[0].split('?')[0]; let checkPath = siteRelativePath.split('#')[0].split('?')[0];
// Handle relative paths (including bare .md): resolve from source file's directory // Strip /docs/ prefix if present (repo-relative links)
if (checkPath.startsWith('./') || checkPath.startsWith('../') || (!checkPath.startsWith('/') && checkPath.endsWith('.md'))) {
const sourceDir = path.dirname(sourceFile);
const resolved = path.resolve(sourceDir, checkPath);
// Ensure the resolved path stays within DOCS_ROOT
if (!resolved.startsWith(DOCS_ROOT + path.sep) && resolved !== DOCS_ROOT) return null;
if (fs.existsSync(resolved) && fs.statSync(resolved).isFile()) return resolved;
if (fs.existsSync(resolved + '.md')) return resolved + '.md';
// Directory: check for index.md
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
const indexFile = path.join(resolved, 'index.md');
if (fs.existsSync(indexFile)) return indexFile;
}
return null;
}
// Strip /docs/ prefix if present (legacy absolute links)
if (checkPath.startsWith('/docs/')) { if (checkPath.startsWith('/docs/')) {
checkPath = checkPath.slice(5); // Remove '/docs' but keep leading '/' checkPath = checkPath.slice(5); // Remove '/docs' but keep leading '/'
} }
@ -145,18 +129,12 @@ function resolveLink(siteRelativePath, sourceFile) {
// Direct path (e.g., /path/file.md) // Direct path (e.g., /path/file.md)
const direct = path.join(DOCS_ROOT, checkPath); const direct = path.join(DOCS_ROOT, checkPath);
if (fs.existsSync(direct) && fs.statSync(direct).isFile()) return direct; if (fs.existsSync(direct)) return direct;
// Try with .md extension // Try with .md extension
const withMd = direct + '.md'; const withMd = direct + '.md';
if (fs.existsSync(withMd)) return withMd; if (fs.existsSync(withMd)) return withMd;
// Directory without trailing slash: check for index.md
if (fs.existsSync(direct) && fs.statSync(direct).isDirectory()) {
const indexFile = path.join(direct, 'index.md');
if (fs.existsSync(indexFile)) return indexFile;
}
return null; return null;
} }
@ -166,7 +144,7 @@ function resolveLink(siteRelativePath, sourceFile) {
function findFileWithContext(brokenPath) { function findFileWithContext(brokenPath) {
// Extract filename and parent directory from the broken path // Extract filename and parent directory from the broken path
// e.g., /tutorials/getting-started/foo/ -> parent: getting-started, file: foo.md // e.g., /tutorials/getting-started/foo/ -> parent: getting-started, file: foo.md
const cleanPath = brokenPath.replace(/\/$/, '').replace(/^(\.\.\/|\.\/|\/)+/, ''); const cleanPath = brokenPath.replace(/\/$/, '').replace(/^\//, '');
const parts = cleanPath.split('/'); const parts = cleanPath.split('/');
const fileName = parts.at(-1) + '.md'; const fileName = parts.at(-1) + '.md';
const parentDir = parts.length > 1 ? parts.at(-2) : null; const parentDir = parts.length > 1 ? parts.at(-2) : null;
@ -241,7 +219,7 @@ function processFile(filePath) {
} }
// Validate the link target exists // Validate the link target exists
const targetFile = resolveLink(linkPath, filePath); const targetFile = resolveLink(linkPath);
if (!targetFile) { if (!targetFile) {
// Link is broken - try to find the file // Link is broken - try to find the file

View File

@ -29,7 +29,6 @@
const fs = require('node:fs'); const fs = require('node:fs');
const path = require('node:path'); const path = require('node:path');
const yaml = require('yaml'); const yaml = require('yaml');
const { parse: parseCsv } = require('csv-parse/sync');
const PROJECT_ROOT = path.resolve(__dirname, '..'); const PROJECT_ROOT = path.resolve(__dirname, '..');
const SRC_DIR = path.join(PROJECT_ROOT, 'src'); const SRC_DIR = path.join(PROJECT_ROOT, 'src');
@ -39,7 +38,7 @@ const STRICT = process.argv.includes('--strict');
// --- Constants --- // --- Constants ---
// File extensions to scan // File extensions to scan
const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml', '.csv']); const SCAN_EXTENSIONS = new Set(['.yaml', '.yml', '.md', '.xml']);
// Skip directories // Skip directories
const SKIP_DIRS = new Set(['node_modules', '_module-installer', '.git']); const SKIP_DIRS = new Set(['node_modules', '_module-installer', '.git']);
@ -293,46 +292,6 @@ function extractMarkdownRefs(filePath, content) {
return refs; return refs;
} }
function extractCsvRefs(filePath, content) {
const refs = [];
let records;
try {
records = parseCsv(content, {
columns: true,
skip_empty_lines: true,
relax_column_count: true,
});
} catch (error) {
// No CSV schema validator exists yet (planned as Layer 2c) — surface parse errors visibly.
// YAML equivalent (line ~198) defers to validate-agent-schema.js; CSV has no such fallback.
const rel = path.relative(PROJECT_ROOT, filePath);
console.error(` [CSV-PARSE-ERROR] ${rel}: ${error.message}`);
if (process.env.GITHUB_ACTIONS) {
console.log(`::warning file=${rel},line=1::${escapeAnnotation(`CSV parse error: ${error.message}`)}`);
}
return refs;
}
// Only process if workflow-file column exists
const firstRecord = records[0];
if (!firstRecord || !('workflow-file' in firstRecord)) {
return refs;
}
for (const [i, record] of records.entries()) {
const raw = record['workflow-file'];
if (!raw || raw.trim() === '') continue;
if (!isResolvable(raw)) continue;
// Line = header (1) + data row index (0-based) + 1
const line = i + 2;
refs.push({ file: filePath, raw, type: 'project-root', line });
}
return refs;
}
// --- Reference Resolution --- // --- Reference Resolution ---
function resolveRef(ref) { function resolveRef(ref) {
@ -392,25 +351,21 @@ function checkAbsolutePathLeaks(filePath, content) {
return leaks; return leaks;
} }
// --- Exports (for testing) ---
module.exports = { extractCsvRefs };
// --- Main --- // --- Main ---
if (require.main === module) { console.log(`\nValidating file references in: ${SRC_DIR}`);
console.log(`\nValidating file references in: ${SRC_DIR}`); console.log(`Mode: ${STRICT ? 'STRICT (exit 1 on issues)' : 'WARNING (exit 0)'}${VERBOSE ? ' + VERBOSE' : ''}\n`);
console.log(`Mode: ${STRICT ? 'STRICT (exit 1 on issues)' : 'WARNING (exit 0)'}${VERBOSE ? ' + VERBOSE' : ''}\n`);
const files = getSourceFiles(SRC_DIR); const files = getSourceFiles(SRC_DIR);
console.log(`Found ${files.length} source files\n`); console.log(`Found ${files.length} source files\n`);
let totalRefs = 0; let totalRefs = 0;
let brokenRefs = 0; let brokenRefs = 0;
let totalLeaks = 0; let totalLeaks = 0;
let filesWithIssues = 0; let filesWithIssues = 0;
const allIssues = []; // Collect for $GITHUB_STEP_SUMMARY const allIssues = []; // Collect for $GITHUB_STEP_SUMMARY
for (const filePath of files) { for (const filePath of files) {
const relativePath = path.relative(PROJECT_ROOT, filePath); const relativePath = path.relative(PROJECT_ROOT, filePath);
const content = fs.readFileSync(filePath, 'utf-8'); const content = fs.readFileSync(filePath, 'utf-8');
const ext = path.extname(filePath); const ext = path.extname(filePath);
@ -419,45 +374,35 @@ if (require.main === module) {
let refs; let refs;
if (ext === '.yaml' || ext === '.yml') { if (ext === '.yaml' || ext === '.yml') {
refs = extractYamlRefs(filePath, content); refs = extractYamlRefs(filePath, content);
} else if (ext === '.csv') {
refs = extractCsvRefs(filePath, content);
} else { } else {
refs = extractMarkdownRefs(filePath, content); refs = extractMarkdownRefs(filePath, content);
} }
// Resolve and classify all refs before printing anything. // Resolve and check
// This avoids the confusing pattern of printing headers at two different
// times depending on verbosity — collect first, then print once.
const broken = []; const broken = [];
const ok = [];
if (VERBOSE && refs.length > 0) {
console.log(`\n${relativePath}`);
}
for (const ref of refs) { for (const ref of refs) {
totalRefs++; totalRefs++;
const resolved = resolveRef(ref); const resolved = resolveRef(ref);
if (resolved && !fs.existsSync(resolved)) { if (resolved && !fs.existsSync(resolved)) {
// Extensionless paths may be directory references or partial templates. // For paths without extensions, also check if it's a directory
// If the path has no extension, check whether it exists as a directory.
// Flag it if nothing exists at all — likely a real broken reference.
const hasExt = path.extname(resolved) !== ''; const hasExt = path.extname(resolved) !== '';
if (!hasExt) { if (!hasExt) {
if (fs.existsSync(resolved)) { // Could be a directory reference — skip if not clearly a file
ok.push({ ref, tag: 'OK-DIR' });
} else {
// No extension and nothing exists — not a file, not a directory.
// Flag as UNRESOLVED (distinct from BROKEN which means "file with extension not found").
broken.push({ ref, resolved: path.relative(PROJECT_ROOT, resolved), kind: 'unresolved' });
brokenRefs++;
}
continue; continue;
} }
broken.push({ ref, resolved: path.relative(PROJECT_ROOT, resolved), kind: 'broken' }); broken.push({ ref, resolved: path.relative(PROJECT_ROOT, resolved) });
brokenRefs++; brokenRefs++;
continue; continue;
} }
if (resolved) { if (VERBOSE && resolved) {
ok.push({ ref, tag: 'OK' }); console.log(` [OK] ${ref.raw}`);
} }
} }
@ -465,33 +410,21 @@ if (require.main === module) {
const leaks = checkAbsolutePathLeaks(filePath, content); const leaks = checkAbsolutePathLeaks(filePath, content);
totalLeaks += leaks.length; totalLeaks += leaks.length;
// Print results — file header appears once, in one place // Report issues for this file
const hasFileIssues = broken.length > 0 || leaks.length > 0; if (broken.length > 0 || leaks.length > 0) {
if (hasFileIssues) {
filesWithIssues++; filesWithIssues++;
if (!VERBOSE) {
console.log(`\n${relativePath}`); console.log(`\n${relativePath}`);
if (VERBOSE) {
for (const { ref, tag, note } of ok) {
const suffix = note ? ` (${note})` : '';
console.log(` [${tag}] ${ref.raw}${suffix}`);
}
} }
for (const { ref, resolved, kind } of broken) { for (const { ref, resolved } of broken) {
const location = ref.line ? `line ${ref.line}` : ref.key ? `key: ${ref.key}` : ''; const location = ref.line ? `line ${ref.line}` : ref.key ? `key: ${ref.key}` : '';
const tag = kind === 'unresolved' ? 'UNRESOLVED' : 'BROKEN'; console.log(` [BROKEN] ${ref.raw}${location ? ` (${location})` : ''}`);
const detail = kind === 'unresolved' ? 'Not found as file or directory' : 'Target not found'; console.log(` Target not found: ${resolved}`);
const issueType = kind === 'unresolved' ? 'unresolved path' : 'broken ref'; allIssues.push({ file: relativePath, line: ref.line || 1, ref: ref.raw, issue: 'broken ref' });
console.log(` [${tag}] ${ref.raw}${location ? ` (${location})` : ''}`);
console.log(` ${detail}: ${resolved}`);
allIssues.push({ file: relativePath, line: ref.line || 1, ref: ref.raw, issue: issueType });
if (process.env.GITHUB_ACTIONS) { if (process.env.GITHUB_ACTIONS) {
const line = ref.line || 1; const line = ref.line || 1;
console.log( console.log(`::warning file=${relativePath},line=${line}::${escapeAnnotation(`Broken reference: ${ref.raw}${resolved}`)}`);
`::warning file=${relativePath},line=${line}::${escapeAnnotation(`${tag === 'UNRESOLVED' ? 'Unresolved path' : 'Broken reference'}: ${ref.raw}${resolved}`)}`,
);
} }
} }
@ -502,26 +435,20 @@ if (require.main === module) {
console.log(`::warning file=${relativePath},line=${leak.line}::${escapeAnnotation(`Absolute path leak: ${leak.content}`)}`); console.log(`::warning file=${relativePath},line=${leak.line}::${escapeAnnotation(`Absolute path leak: ${leak.content}`)}`);
} }
} }
} else if (VERBOSE && refs.length > 0) {
console.log(`\n${relativePath}`);
for (const { ref, tag, note } of ok) {
const suffix = note ? ` (${note})` : '';
console.log(` [${tag}] ${ref.raw}${suffix}`);
}
}
} }
}
// Summary // Summary
console.log(`\n${'─'.repeat(60)}`); console.log(`\n${'─'.repeat(60)}`);
console.log(`\nSummary:`); console.log(`\nSummary:`);
console.log(` Files scanned: ${files.length}`); console.log(` Files scanned: ${files.length}`);
console.log(` References checked: ${totalRefs}`); console.log(` References checked: ${totalRefs}`);
console.log(` Broken references: ${brokenRefs}`); console.log(` Broken references: ${brokenRefs}`);
console.log(` Absolute path leaks: ${totalLeaks}`); console.log(` Absolute path leaks: ${totalLeaks}`);
const hasIssues = brokenRefs > 0 || totalLeaks > 0; const hasIssues = brokenRefs > 0 || totalLeaks > 0;
if (hasIssues) { if (hasIssues) {
console.log(`\n ${filesWithIssues} file(s) with issues`); console.log(`\n ${filesWithIssues} file(s) with issues`);
if (STRICT) { if (STRICT) {
@ -529,14 +456,14 @@ if (require.main === module) {
} else { } else {
console.log(`\n Run with --strict to treat warnings as errors.`); console.log(`\n Run with --strict to treat warnings as errors.`);
} }
} else { } else {
console.log(`\n All file references valid!`); console.log(`\n All file references valid!`);
} }
console.log(''); console.log('');
// Write GitHub Actions step summary // Write GitHub Actions step summary
if (process.env.GITHUB_STEP_SUMMARY) { if (process.env.GITHUB_STEP_SUMMARY) {
let summary = '## File Reference Validation\n\n'; let summary = '## File Reference Validation\n\n';
if (allIssues.length > 0) { if (allIssues.length > 0) {
summary += '| File | Line | Reference | Issue |\n'; summary += '| File | Line | Reference | Issue |\n';
@ -548,7 +475,6 @@ if (require.main === module) {
} }
summary += `**${files.length} files scanned, ${totalRefs} references checked, ${brokenRefs + totalLeaks} issues found**\n`; summary += `**${files.length} files scanned, ${totalRefs} references checked, ${brokenRefs + totalLeaks} issues found**\n`;
fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary); fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary);
}
process.exit(hasIssues && STRICT ? 1 : 0);
} }
process.exit(hasIssues && STRICT ? 1 : 0);

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" width="100" height="100">
<rect width="100" height="100" rx="20" fill="#0d9488"/>
<text x="50" y="65" font-family="Arial, sans-serif" font-size="40" font-weight="bold" fill="white" text-anchor="middle">B</text>
</svg>

After

Width:  |  Height:  |  Size: 284 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

Some files were not shown because too many files have changed in this diff Show More