Compare commits
9 Commits
e75f57e73d
...
954c6010dd
| Author | SHA1 | Date |
|---|---|---|
|
|
954c6010dd | |
|
|
993d02b8b3 | |
|
|
5cb5606ba3 | |
|
|
49d284179a | |
|
|
594d9854eb | |
|
|
9565bef286 | |
|
|
54ab3f13d3 | |
|
|
c3cf0c1fc6 | |
|
|
5247468d98 |
|
|
@ -79,3 +79,6 @@ bmad-custom-src/
|
||||||
website/.astro/
|
website/.astro/
|
||||||
website/dist/
|
website/dist/
|
||||||
build/
|
build/
|
||||||
|
|
||||||
|
# Fork-specific documentation (not committed)
|
||||||
|
BUG-TRACKING.md
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
# Security Policy
|
||||||
|
|
||||||
|
## Supported Versions
|
||||||
|
|
||||||
|
We release security patches for the following versions:
|
||||||
|
|
||||||
|
| Version | Supported |
|
||||||
|
| ------- | ------------------ |
|
||||||
|
| Latest | :white_check_mark: |
|
||||||
|
| < Latest | :x: |
|
||||||
|
|
||||||
|
We recommend always using the latest version of BMad Method to ensure you have the most recent security updates.
|
||||||
|
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
We take security vulnerabilities seriously. If you discover a security issue, please report it responsibly.
|
||||||
|
|
||||||
|
### How to Report
|
||||||
|
|
||||||
|
**Do NOT report security vulnerabilities through public GitHub issues.**
|
||||||
|
|
||||||
|
Instead, please report them via one of these methods:
|
||||||
|
|
||||||
|
1. **GitHub Security Advisories** (Preferred): Use [GitHub's private vulnerability reporting](https://github.com/bmad-code-org/BMAD-METHOD/security/advisories/new) to submit a confidential report.
|
||||||
|
|
||||||
|
2. **Discord**: Contact a maintainer directly via DM on our [Discord server](https://discord.gg/gk8jAdXWmj).
|
||||||
|
|
||||||
|
### What to Include
|
||||||
|
|
||||||
|
Please include as much of the following information as possible:
|
||||||
|
|
||||||
|
- Type of vulnerability (e.g., prompt injection, path traversal, etc.)
|
||||||
|
- Full paths of source file(s) related to the vulnerability
|
||||||
|
- Step-by-step instructions to reproduce the issue
|
||||||
|
- Proof-of-concept or exploit code (if available)
|
||||||
|
- Impact assessment of the vulnerability
|
||||||
|
|
||||||
|
### Response Timeline
|
||||||
|
|
||||||
|
- **Initial Response**: Within 48 hours of receiving your report
|
||||||
|
- **Status Update**: Within 7 days with our assessment
|
||||||
|
- **Resolution Target**: Critical issues within 30 days; other issues within 90 days
|
||||||
|
|
||||||
|
### What to Expect
|
||||||
|
|
||||||
|
1. We will acknowledge receipt of your report
|
||||||
|
2. We will investigate and validate the vulnerability
|
||||||
|
3. We will work on a fix and coordinate disclosure timing with you
|
||||||
|
4. We will credit you in the security advisory (unless you prefer to remain anonymous)
|
||||||
|
|
||||||
|
## Security Scope
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
|
||||||
|
- Vulnerabilities in BMad Method core framework code
|
||||||
|
- Security issues in agent definitions or workflows that could lead to unintended behavior
|
||||||
|
- Path traversal or file system access issues
|
||||||
|
- Prompt injection vulnerabilities that bypass intended agent behavior
|
||||||
|
- Supply chain vulnerabilities in dependencies
|
||||||
|
|
||||||
|
### Out of Scope
|
||||||
|
|
||||||
|
- Security issues in user-created custom agents or modules
|
||||||
|
- Vulnerabilities in third-party AI providers (Claude, GPT, etc.)
|
||||||
|
- Issues that require physical access to a user's machine
|
||||||
|
- Social engineering attacks
|
||||||
|
- Denial of service attacks that don't exploit a specific vulnerability
|
||||||
|
|
||||||
|
## Security Best Practices for Users
|
||||||
|
|
||||||
|
When using BMad Method:
|
||||||
|
|
||||||
|
1. **Review Agent Outputs**: Always review AI-generated code before executing it
|
||||||
|
2. **Limit File Access**: Configure your AI IDE to limit file system access where possible
|
||||||
|
3. **Keep Updated**: Regularly update to the latest version
|
||||||
|
4. **Validate Dependencies**: Review any dependencies added by generated code
|
||||||
|
5. **Environment Isolation**: Consider running AI-assisted development in isolated environments
|
||||||
|
|
||||||
|
## Acknowledgments
|
||||||
|
|
||||||
|
We appreciate the security research community's efforts in helping keep BMad Method secure. Contributors who report valid security issues will be acknowledged in our security advisories.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Thank you for helping keep BMad Method and our community safe.
|
||||||
|
|
@ -0,0 +1,536 @@
|
||||||
|
# Bug Tracking Workflow Wireframe
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
```
|
||||||
|
COMMANDS:
|
||||||
|
/triage - Triage new bugs from bugs.md
|
||||||
|
/implement bug-NNN - Implement a bug fix
|
||||||
|
/implement feature-N - Implement a feature
|
||||||
|
/verify - List pending verification
|
||||||
|
/verify bug-NNN - Verify and close specific bug
|
||||||
|
/verify all - Batch verify all
|
||||||
|
|
||||||
|
FILES:
|
||||||
|
docs/bugs.md - Human-readable bug tracking
|
||||||
|
docs/bugs.yaml - Agent-readable metadata
|
||||||
|
|
||||||
|
SEVERITY → COMPLEXITY → WORKFLOW ROUTING:
|
||||||
|
┌──────────┬─────────┬─────────┬─────────┬─────────┐
|
||||||
|
│ │ TRIVIAL │ SMALL │ MEDIUM │ COMPLEX │
|
||||||
|
├──────────┼─────────┼─────────┼─────────┼─────────┤
|
||||||
|
│ CRITICAL │ correct-course (any complexity) │
|
||||||
|
├──────────┼─────────┼─────────┼─────────┼─────────┤
|
||||||
|
│ HIGH │direct-fx│tech-spec│corr-crs │corr-crs │
|
||||||
|
├──────────┼─────────┼─────────┼─────────┼─────────┤
|
||||||
|
│ MEDIUM │direct-fx│tech-spec│corr-crs │corr-crs │
|
||||||
|
├──────────┼─────────┼─────────┼─────────┼─────────┤
|
||||||
|
│ LOW │direct-fx│ backlog │ backlog │ backlog │
|
||||||
|
└──────────┴─────────┴─────────┴─────────┴─────────┘
|
||||||
|
|
||||||
|
SEVERITY:
|
||||||
|
critical - Core broken, crashes, data loss
|
||||||
|
high - Major feature blocked, workaround exists
|
||||||
|
medium - Partial breakage, minor impact
|
||||||
|
low - Cosmetic, edge case
|
||||||
|
|
||||||
|
COMPLEXITY:
|
||||||
|
trivial - One-liner, minimal change
|
||||||
|
small - Single file change
|
||||||
|
medium - Multi-file change
|
||||||
|
complex - Architectural change
|
||||||
|
|
||||||
|
STATUS FLOW:
|
||||||
|
reported → triaged → [routed] → in-progress → fixed/implemented → verified → closed
|
||||||
|
|
||||||
|
STATUS VALUES:
|
||||||
|
triaged - Analyzed, routed, awaiting implementation
|
||||||
|
routed - Sent to tech-spec or correct-course workflow
|
||||||
|
in-progress - Developer actively working
|
||||||
|
fixed - Code complete, awaiting verification (bugs)
|
||||||
|
implemented - Code complete, awaiting verification (features)
|
||||||
|
closed - Verified and closed
|
||||||
|
backlog - Deferred to future sprint
|
||||||
|
blocked - Cannot proceed until issue resolved
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 1: System Architecture
|
||||||
|
|
||||||
|
### System Overview
|
||||||
|
|
||||||
|
```
|
||||||
|
INPUT SOURCES
|
||||||
|
+-------------------+ +-------------------+ +-------------------+
|
||||||
|
| IN-APP MODAL | | MANUAL ENTRY | | EXTERNAL ISSUE |
|
||||||
|
| (Optional API) | | (bugs.md) | | TRACKER IMPORT |
|
||||||
|
+--------+----------+ +--------+----------+ +--------+----------+
|
||||||
|
| | |
|
||||||
|
+------------+------------+-------------------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+============================+
|
||||||
|
| /triage (WORKFLOW) |
|
||||||
|
+============================+
|
||||||
|
|
|
||||||
|
+---------------+---------------+---------------+
|
||||||
|
| | | |
|
||||||
|
v v v v
|
||||||
|
direct-fix tech-spec correct-course backlog
|
||||||
|
| | | |
|
||||||
|
v v v v
|
||||||
|
/implement /tech-spec /correct-course (deferred)
|
||||||
|
| | |
|
||||||
|
+---------------+---------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
/verify → CLOSED
|
||||||
|
```
|
||||||
|
|
||||||
|
### File Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
{project-root}/
|
||||||
|
docs/
|
||||||
|
bugs.md <-- User-facing: informal bug reports & tracking
|
||||||
|
bugs.yaml <-- Agent-facing: structured metadata database
|
||||||
|
epics.md <-- Context: for mapping bugs to stories
|
||||||
|
_bmad/bmm/
|
||||||
|
config.yaml <-- Project configuration
|
||||||
|
workflows/
|
||||||
|
bug-tracking/ <-- Triage workflow files
|
||||||
|
implement/ <-- Implementation workflow
|
||||||
|
verify/ <-- Verification workflow
|
||||||
|
```
|
||||||
|
|
||||||
|
### bugs.md Structure
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# Bug Tracking - {project_name}
|
||||||
|
|
||||||
|
# manual input
|
||||||
|
## Bug: Login fails on iOS Safari
|
||||||
|
Description of the bug...
|
||||||
|
Reported by: User Name
|
||||||
|
Date: 2025-01-15
|
||||||
|
|
||||||
|
- **Crash on startup (Android)**: App crashes immediately. CRITICAL.
|
||||||
|
|
||||||
|
1. Form validation missing - No validation on email field
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tracked Bugs
|
||||||
|
### bug-001: Login fails on iOS Safari
|
||||||
|
Brief description...
|
||||||
|
- **Severity:** high
|
||||||
|
- **Complexity:** small
|
||||||
|
- **Workflow:** tech-spec
|
||||||
|
- **Related:** story-2-3
|
||||||
|
**Notes:** Triage reasoning...
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tracked Feature Requests
|
||||||
|
### feature-001: Dark mode toggle
|
||||||
|
Brief description...
|
||||||
|
- **Priority:** medium
|
||||||
|
- **Complexity:** medium
|
||||||
|
- **Workflow:** tech-spec
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Fixed Bugs
|
||||||
|
[IMPLEMENTED] bug-003: Header alignment [Sev: low, Fixed: 2025-01-18, Verified: pending]
|
||||||
|
- Fix: Adjusted flexbox styling
|
||||||
|
- File(s): src/components/Header.tsx
|
||||||
|
|
||||||
|
bug-002: Form submission error [Sev: medium, Fixed: 2025-01-15, Verified: 2025-01-16, CLOSED]
|
||||||
|
- Fix: Added error boundary
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Implemented Features
|
||||||
|
[IMPLEMENTED] feature-002: Export to CSV [Impl: 2025-01-20, Verified: pending]
|
||||||
|
- Files: src/export.ts, src/utils/csv.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 2: Workflow Operations
|
||||||
|
|
||||||
|
### Slash Command Reference
|
||||||
|
|
||||||
|
| Command | Description | When to Use |
|
||||||
|
|---------|-------------|-------------|
|
||||||
|
| `/triage` | Main workflow - triage user-reported bugs | When new bugs are in bugs.md |
|
||||||
|
| `/implement bug-NNN` | Implement a specific bug fix | After triage, routed for direct-fix |
|
||||||
|
| `/implement feature-NNN` | Implement a feature request | After feature is triaged |
|
||||||
|
| `/verify` | List all pending verification | After implementation, before closing |
|
||||||
|
| `/verify bug-NNN` | Verify and close specific bug | After testing confirms fix works |
|
||||||
|
| `/verify all` | Batch verify all pending items | Bulk close multiple fixes |
|
||||||
|
|
||||||
|
### /triage Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
USER INVOKES: /triage
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+---------------------------+
|
||||||
|
| STEP 1: INITIALIZATION |
|
||||||
|
+---------------------------+
|
||||||
|
| - Load config.yaml |
|
||||||
|
| - Check for bugs.yaml |
|
||||||
|
| - Detect existing session |
|
||||||
|
+------------+--------------+
|
||||||
|
|
|
||||||
|
+--------+--------+
|
||||||
|
| |
|
||||||
|
v v
|
||||||
|
+-------------+ +-------------+
|
||||||
|
| Has pending | | Fresh start |
|
||||||
|
| triaged work| +------+------+
|
||||||
|
+------+------+ |
|
||||||
|
| v
|
||||||
|
v +-------------+
|
||||||
|
+-------------+ | Scan manual |
|
||||||
|
| Show status | | input section|
|
||||||
|
| [T/I/V/L/Q] | +------+------+
|
||||||
|
+-------------+ |
|
||||||
|
v
|
||||||
|
+-------------+
|
||||||
|
| [S/C/Q] |
|
||||||
|
| Sync/Cont/Q |
|
||||||
|
+------+------+
|
||||||
|
|
|
||||||
|
+---------------+---------------+
|
||||||
|
v v v
|
||||||
|
[S] API Sync [C] Continue [Q] Quit
|
||||||
|
|
||||||
|
+---------------------------+
|
||||||
|
| STEP 2: API SYNC | (Optional - if [S] selected)
|
||||||
|
+---------------------------+
|
||||||
|
| GET /api/bug-reports/pending
|
||||||
|
| - Fetch, format, insert to bugs.md
|
||||||
|
| - POST /mark-synced
|
||||||
|
+---------------------------+
|
||||||
|
|
||||||
|
+---------------------------+
|
||||||
|
| STEP 3: PARSE |
|
||||||
|
+---------------------------+
|
||||||
|
| Read "# manual input" only
|
||||||
|
| - Parse headers, bullets, numbered lists
|
||||||
|
| - Extract: title, desc, reporter, platform
|
||||||
|
| - Compare with bugs.yaml (NEW vs EXISTING)
|
||||||
|
+------------+--------------+
|
||||||
|
|
|
||||||
|
+--------+--------+
|
||||||
|
v v
|
||||||
|
No new bugs {N} new bugs
|
||||||
|
[HALT] [C] Continue
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+---------------------------+
|
||||||
|
| STEP 4: TRIAGE (per bug) |
|
||||||
|
+---------------------------+
|
||||||
|
| FOR EACH NEW BUG:
|
||||||
|
| 1. Generate bug-NNN ID
|
||||||
|
| 2. Assess SEVERITY (critical|high|med|low)
|
||||||
|
| 3. Assess COMPLEXITY (trivial|small|med|complex)
|
||||||
|
| 4. Apply ROUTING MATRIX → workflow
|
||||||
|
| 5. Map to story/epic if applicable
|
||||||
|
| 6. Assess DOC IMPACT (prd|architecture|ux)
|
||||||
|
| 7. Add triage notes
|
||||||
|
| 8. Present: [A]ccept/[M]odify/[S]kip/[N]ext
|
||||||
|
+---------------------------+
|
||||||
|
|
|
||||||
|
v (after all bugs)
|
||||||
|
+---------------------------+
|
||||||
|
| STEP 5: UPDATE FILES |
|
||||||
|
+---------------------------+
|
||||||
|
| bugs.yaml: Add entries, update stats
|
||||||
|
| bugs.md: Remove from manual input,
|
||||||
|
| Add to Tracked Bugs/Features
|
||||||
|
+---------------------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+---------------------------+
|
||||||
|
| STEP 6: COMPLETE |
|
||||||
|
+---------------------------+
|
||||||
|
| Show summary + next steps:
|
||||||
|
| /implement bug-NNN
|
||||||
|
| /verify bug-NNN
|
||||||
|
+---------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### /implement Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
USER INVOKES: /implement bug-NNN
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-------------------------------+
|
||||||
|
| STEP 1-2: Load Context |
|
||||||
|
+-------------------------------+
|
||||||
|
| - Parse ID (bug-NNN/feature-NNN)
|
||||||
|
| - Load from bugs.yaml
|
||||||
|
| - Check status (halt if backlog/blocked/deferred)
|
||||||
|
+---------------+---------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-------------------------------+
|
||||||
|
| STEP 3: Check Workflow Route |
|
||||||
|
+-------------------------------+
|
||||||
|
|
|
||||||
|
+-----------+-----------+-----------+
|
||||||
|
v v v v
|
||||||
|
correct- tech-spec direct-fix ambiguous
|
||||||
|
course |
|
||||||
|
| | | Apply Matrix
|
||||||
|
v v |
|
||||||
|
[ROUTES TO [ROUTES TO |
|
||||||
|
/correct- /tech-spec |
|
||||||
|
course] workflow] |
|
||||||
|
| | |
|
||||||
|
v v v
|
||||||
|
Creates Creates +--------+
|
||||||
|
story spec | STEP 4:|
|
||||||
|
| Confirm|
|
||||||
|
+---+----+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+---------------+
|
||||||
|
| STEP 5: |
|
||||||
|
| IMPLEMENT |
|
||||||
|
+---------------+
|
||||||
|
| Dev Agent: |
|
||||||
|
| - Read files |
|
||||||
|
| - Make changes|
|
||||||
|
| - Minimal fix |
|
||||||
|
+-------+-------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+---------------+
|
||||||
|
| STEP 6: Check |
|
||||||
|
| npm run check |
|
||||||
|
+-------+-------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+---------------+
|
||||||
|
| STEP 7-8: |
|
||||||
|
| Update Files |
|
||||||
|
+---------------+
|
||||||
|
| bugs.yaml: |
|
||||||
|
| status: fixed|
|
||||||
|
| bugs.md: |
|
||||||
|
| [IMPLEMENTED]|
|
||||||
|
+-------+-------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+---------------+
|
||||||
|
| STEP 9: |
|
||||||
|
| "Run /verify" |
|
||||||
|
+---------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### /verify Workflow
|
||||||
|
|
||||||
|
```
|
||||||
|
USER INVOKES: /verify [bug-NNN]
|
||||||
|
|
|
||||||
|
+-----------+-----------+
|
||||||
|
v v
|
||||||
|
+---------------+ +---------------+
|
||||||
|
| No ID given | | ID provided |
|
||||||
|
+-------+-------+ +-------+-------+
|
||||||
|
| |
|
||||||
|
v |
|
||||||
|
+---------------+ |
|
||||||
|
| List pending | |
|
||||||
|
| [IMPLEMENTED] | |
|
||||||
|
| items | |
|
||||||
|
+-------+-------+ |
|
||||||
|
| |
|
||||||
|
+-------+---------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-------------------------------+
|
||||||
|
| STEP 2: Load & Validate |
|
||||||
|
+-------------------------------+
|
||||||
|
| - Verify status: fixed/implemented
|
||||||
|
| - Check file sync
|
||||||
|
+---------------+---------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-------------------------------+
|
||||||
|
| STEP 3: Confirm Verification |
|
||||||
|
+-------------------------------+
|
||||||
|
| Show: Title, type, date, files
|
||||||
|
| "Has this been tested?"
|
||||||
|
| [yes | no | skip]
|
||||||
|
+---------------+---------------+
|
||||||
|
|
|
||||||
|
+-----------+-----------+
|
||||||
|
v v v
|
||||||
|
+-------+ +-------+ +-------+
|
||||||
|
| YES | | NO | | SKIP |
|
||||||
|
+---+---+ +---+---+ +---+---+
|
||||||
|
| | |
|
||||||
|
v v v
|
||||||
|
Step 4 Add note Next item
|
||||||
|
"rework"
|
||||||
|
|
||||||
|
+-------------------------------+
|
||||||
|
| STEP 4-5: Update Files |
|
||||||
|
+-------------------------------+
|
||||||
|
| bugs.yaml: status: closed,
|
||||||
|
| verified_date
|
||||||
|
| bugs.md: Remove [IMPLEMENTED],
|
||||||
|
| Add CLOSED tag
|
||||||
|
+-------------------------------+
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+-------------------------------+
|
||||||
|
| STEP 6: Summary |
|
||||||
|
| "bug-NNN VERIFIED and CLOSED" |
|
||||||
|
+-------------------------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 3: Routing & Agent Delegation
|
||||||
|
|
||||||
|
### Workflow Routing by Type
|
||||||
|
|
||||||
|
| Workflow | Trigger Conditions | Pre-Implement Phase | Implementation Phase |
|
||||||
|
|----------|-------------------|---------------------|---------------------|
|
||||||
|
| **direct-fix** | high/med + trivial | None | Dev Agent in /implement Step 5 |
|
||||||
|
| **tech-spec** | high/med + small | Architect creates spec | /dev-story per spec |
|
||||||
|
| **correct-course** | critical (any) OR med/complex+ OR doc_impact | PM→Architect→SM create story | /dev-story per story |
|
||||||
|
| **backlog** | low + small+ | None (deferred) | Awaits sprint promotion |
|
||||||
|
|
||||||
|
### Agent Responsibilities
|
||||||
|
|
||||||
|
```
|
||||||
|
/triage
|
||||||
|
|
|
||||||
|
v
|
||||||
|
+------------------------+
|
||||||
|
| SM AGENT (Scrum |
|
||||||
|
| Master Facilitator) |
|
||||||
|
+------------------------+
|
||||||
|
| - Runs triage workflow |
|
||||||
|
| - Assesses severity |
|
||||||
|
| - Routes to workflows |
|
||||||
|
+-----------+------------+
|
||||||
|
|
|
||||||
|
+-------------------+-------------------+
|
||||||
|
v v v
|
||||||
|
+------------+ +------------+ +------------+
|
||||||
|
| Direct-Fix | | Tech-Spec | | Correct- |
|
||||||
|
+-----+------+ +-----+------+ | Course |
|
||||||
|
| | +-----+------+
|
||||||
|
v v |
|
||||||
|
+------------+ +------------+ v
|
||||||
|
| DEV AGENT | | ARCHITECT | +------------+
|
||||||
|
| /implement | | /tech-spec | | PM AGENT |
|
||||||
|
| Step 5 | +-----+------+ | + ARCHITECT|
|
||||||
|
+------------+ | | + SM |
|
||||||
|
v +-----+------+
|
||||||
|
+------------+ |
|
||||||
|
| DEV AGENT | v
|
||||||
|
| /dev-story | +------------+
|
||||||
|
+------------+ | DEV AGENT |
|
||||||
|
| /dev-story |
|
||||||
|
+------------+
|
||||||
|
```
|
||||||
|
|
||||||
|
### Doc Impact Routing
|
||||||
|
|
||||||
|
When `doc_impact` flags are detected during /implement:
|
||||||
|
|
||||||
|
| Flag | Agent | Action |
|
||||||
|
|------|-------|--------|
|
||||||
|
| PRD | PM Agent | Update PRD.md |
|
||||||
|
| Architecture | Architect Agent | Update architecture.md |
|
||||||
|
| UX | UX Designer Agent | Update UX specs |
|
||||||
|
|
||||||
|
User is prompted: `[update-docs-first | proceed-anyway | cancel]`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Part 4: State & Lifecycle
|
||||||
|
|
||||||
|
### File State Transitions
|
||||||
|
|
||||||
|
```
|
||||||
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
DIRECT-FIX TECH-SPEC CORRECT-COURSE BACKLOG
|
||||||
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
ENTRY # manual input # manual input # manual input # manual input
|
||||||
|
(informal text) (informal text) (informal text) (informal text)
|
||||||
|
│ │ │ │
|
||||||
|
▼ ▼ ▼ ▼
|
||||||
|
─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
TRIAGE # Tracked Bugs # Tracked Bugs # Tracked Bugs # Tracked Bugs
|
||||||
|
bug-NNN bug-NNN bug-NNN bug-NNN
|
||||||
|
wf: direct-fix wf: tech-spec wf: correct-crs wf: backlog
|
||||||
|
│ │ │ │
|
||||||
|
▼ ▼ ▼ │
|
||||||
|
─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
ROUTE (skip) /tech-spec /correct-course (waiting)
|
||||||
|
creates spec creates story │
|
||||||
|
│ │ │ │
|
||||||
|
▼ ▼ ▼ │
|
||||||
|
─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
CODE /implement /dev-story /dev-story (waiting)
|
||||||
|
Step 5 per spec per story │
|
||||||
|
│ │ │ │
|
||||||
|
▼ ▼ ▼ │
|
||||||
|
─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
IMPL # Fixed Bugs # Fixed Bugs # Fixed Bugs (unchanged)
|
||||||
|
[IMPLEMENTED] [IMPLEMENTED] [IMPLEMENTED] │
|
||||||
|
bug-NNN bug-NNN bug-NNN │
|
||||||
|
│ │ │ │
|
||||||
|
▼ ▼ ▼ │
|
||||||
|
─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
VERIFY /verify /verify /verify (waiting)
|
||||||
|
bug-NNN bug-NNN bug-NNN │
|
||||||
|
│ │ │ │
|
||||||
|
▼ ▼ ▼ ▼
|
||||||
|
─────────────────────────────────────────────────────────────────────────────────
|
||||||
|
DONE CLOSED ✓ CLOSED ✓ CLOSED ✓ WAITING ◷
|
||||||
|
═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
FILE STATE SUMMARY:
|
||||||
|
┌──────────┬─────────────────────────────┬──────────────────────────────────┐
|
||||||
|
│ STAGE │ bugs.md │ bugs.yaml │
|
||||||
|
├──────────┼─────────────────────────────┼──────────────────────────────────┤
|
||||||
|
│ Entry │ # manual input │ (no entry) │
|
||||||
|
├──────────┼─────────────────────────────┼──────────────────────────────────┤
|
||||||
|
│ Triage │ → # Tracked Bugs/Features │ status: triaged + metadata │
|
||||||
|
├──────────┼─────────────────────────────┼──────────────────────────────────┤
|
||||||
|
│ Implement│ → # Fixed [IMPLEMENTED] │ status: fixed/implemented │
|
||||||
|
├──────────┼─────────────────────────────┼──────────────────────────────────┤
|
||||||
|
│ Verify │ [IMPLEMENTED] → CLOSED │ status: closed + verified_date │
|
||||||
|
└──────────┴─────────────────────────────┴──────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appendix: Optional Extensions
|
||||||
|
|
||||||
|
### In-App Bug Reporting API
|
||||||
|
|
||||||
|
Optional integration for apps with built-in bug reporting UI:
|
||||||
|
|
||||||
|
1. **User submits** via in-app modal → `POST /api/bug-reports`
|
||||||
|
2. **Database stores** with `status: 'new'`
|
||||||
|
3. **During /triage Step 2** (if [S]ync selected):
|
||||||
|
- `GET /api/bug-reports/pending` fetches new reports
|
||||||
|
- Formats as markdown, inserts to `# manual input`
|
||||||
|
- `POST /api/bug-reports/mark-synced` prevents re-fetch
|
||||||
|
|
||||||
|
This is optional - manual entry to bugs.md works without any API.
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
"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",
|
||||||
"inquirer": "^9.3.8",
|
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"ora": "^5.4.1",
|
"ora": "^5.4.1",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
|
|
@ -34,6 +33,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@astrojs/sitemap": "^3.6.0",
|
"@astrojs/sitemap": "^3.6.0",
|
||||||
"@astrojs/starlight": "^0.37.0",
|
"@astrojs/starlight": "^0.37.0",
|
||||||
|
"@clack/prompts": "^0.11.0",
|
||||||
"@eslint/js": "^9.33.0",
|
"@eslint/js": "^9.33.0",
|
||||||
"archiver": "^7.0.1",
|
"archiver": "^7.0.1",
|
||||||
"astro": "^5.16.0",
|
"astro": "^5.16.0",
|
||||||
|
|
@ -244,7 +244,6 @@
|
||||||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
|
|
@ -756,6 +755,29 @@
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@clack/core": {
|
||||||
|
"version": "0.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@clack/core/-/core-0.5.0.tgz",
|
||||||
|
"integrity": "sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"sisteransi": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@clack/prompts": {
|
||||||
|
"version": "0.11.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-0.11.0.tgz",
|
||||||
|
"integrity": "sha512-pMN5FcrEw9hUkZA4f+zLlzivQSeQf5dRGJjSUbvVYDLvpKCdQx5OaknvKzgbtXOizhP+SJJJjqEbOe55uKKfAw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@clack/core": "0.5.0",
|
||||||
|
"picocolors": "^1.0.0",
|
||||||
|
"sisteransi": "^1.0.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@colors/colors": {
|
"node_modules/@colors/colors": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz",
|
||||||
|
|
@ -1998,36 +2020,6 @@
|
||||||
"url": "https://opencollective.com/libvips"
|
"url": "https://opencollective.com/libvips"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@inquirer/external-editor": {
|
|
||||||
"version": "1.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
|
|
||||||
"integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chardet": "^2.1.1",
|
|
||||||
"iconv-lite": "^0.7.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@types/node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@types/node": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@inquirer/figures": {
|
|
||||||
"version": "1.0.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.15.tgz",
|
|
||||||
"integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@isaacs/balanced-match": {
|
"node_modules/@isaacs/balanced-match": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
|
||||||
|
|
@ -3641,9 +3633,8 @@
|
||||||
"version": "25.0.3",
|
"version": "25.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.3.tgz",
|
||||||
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
"integrity": "sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
|
|
@ -3983,7 +3974,6 @@
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
|
|
@ -4031,6 +4021,7 @@
|
||||||
"version": "4.3.2",
|
"version": "4.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
|
||||||
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
|
"integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"type-fest": "^0.21.3"
|
"type-fest": "^0.21.3"
|
||||||
|
|
@ -4046,6 +4037,7 @@
|
||||||
"version": "0.21.3",
|
"version": "0.21.3",
|
||||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
||||||
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
|
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
|
||||||
|
"dev": true,
|
||||||
"license": "(MIT OR CC0-1.0)",
|
"license": "(MIT OR CC0-1.0)",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
|
|
@ -4290,7 +4282,6 @@
|
||||||
"integrity": "sha512-6mF/YrvwwRxLTu+aMEa5pwzKUNl5ZetWbTyZCs9Um0F12HUmxUiF5UHiZPy4rifzU3gtpM3xP2DfdmkNX9eZRg==",
|
"integrity": "sha512-6mF/YrvwwRxLTu+aMEa5pwzKUNl5ZetWbTyZCs9Um0F12HUmxUiF5UHiZPy4rifzU3gtpM3xP2DfdmkNX9eZRg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/compiler": "^2.13.0",
|
"@astrojs/compiler": "^2.13.0",
|
||||||
"@astrojs/internal-helpers": "0.7.5",
|
"@astrojs/internal-helpers": "0.7.5",
|
||||||
|
|
@ -5358,7 +5349,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -5601,12 +5591,6 @@
|
||||||
"url": "https://github.com/sponsors/wooorm"
|
"url": "https://github.com/sponsors/wooorm"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chardet": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/chokidar": {
|
"node_modules/chokidar": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
||||||
|
|
@ -5787,15 +5771,6 @@
|
||||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/cli-width": {
|
|
||||||
"version": "4.1.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
|
|
||||||
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cliui": {
|
"node_modules/cliui": {
|
||||||
"version": "8.0.1",
|
"version": "8.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||||
|
|
@ -6689,7 +6664,6 @@
|
||||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
|
|
@ -8269,22 +8243,6 @@
|
||||||
"@babel/runtime": "^7.23.2"
|
"@babel/runtime": "^7.23.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/iconv-lite": {
|
|
||||||
"version": "0.7.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
|
|
||||||
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
|
@ -8420,43 +8378,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/inquirer": {
|
|
||||||
"version": "9.3.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.8.tgz",
|
|
||||||
"integrity": "sha512-pFGGdaHrmRKMh4WoDDSowddgjT1Vkl90atobmTeSmcPGdYiwikch/m/Ef5wRaiamHejtw0cUUMMerzDUXCci2w==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@inquirer/external-editor": "^1.0.2",
|
|
||||||
"@inquirer/figures": "^1.0.3",
|
|
||||||
"ansi-escapes": "^4.3.2",
|
|
||||||
"cli-width": "^4.1.0",
|
|
||||||
"mute-stream": "1.0.0",
|
|
||||||
"ora": "^5.4.1",
|
|
||||||
"run-async": "^3.0.0",
|
|
||||||
"rxjs": "^7.8.1",
|
|
||||||
"string-width": "^4.2.3",
|
|
||||||
"strip-ansi": "^6.0.1",
|
|
||||||
"wrap-ansi": "^6.2.0",
|
|
||||||
"yoctocolors-cjs": "^2.1.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/inquirer/node_modules/wrap-ansi": {
|
|
||||||
"version": "6.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
|
||||||
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": "^4.0.0",
|
|
||||||
"string-width": "^4.1.0",
|
|
||||||
"strip-ansi": "^6.0.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/iron-webcrypto": {
|
"node_modules/iron-webcrypto": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz",
|
||||||
|
|
@ -10304,7 +10225,6 @@
|
||||||
"integrity": "sha512-p3JTemJJbkiMjXEMiFwgm0v6ym5g8K+b2oDny+6xdl300tUKySxvilJQLSea48C6OaYNmO30kH9KxpiAg5bWJw==",
|
"integrity": "sha512-p3JTemJJbkiMjXEMiFwgm0v6ym5g8K+b2oDny+6xdl300tUKySxvilJQLSea48C6OaYNmO30kH9KxpiAg5bWJw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"globby": "15.0.0",
|
"globby": "15.0.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
|
|
@ -11576,15 +11496,6 @@
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mute-stream": {
|
|
||||||
"version": "1.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz",
|
|
||||||
"integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==",
|
|
||||||
"license": "ISC",
|
|
||||||
"engines": {
|
|
||||||
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/nano-spawn": {
|
"node_modules/nano-spawn": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/nano-spawn/-/nano-spawn-2.0.0.tgz",
|
||||||
|
|
@ -12378,7 +12289,6 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
|
@ -12444,7 +12354,6 @@
|
||||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
|
|
@ -13273,7 +13182,6 @@
|
||||||
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
|
|
@ -13310,15 +13218,6 @@
|
||||||
"fsevents": "~2.3.2"
|
"fsevents": "~2.3.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/run-async": {
|
|
||||||
"version": "3.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
|
|
||||||
"integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.12.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/run-parallel": {
|
"node_modules/run-parallel": {
|
||||||
"version": "1.2.0",
|
"version": "1.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
|
||||||
|
|
@ -13343,15 +13242,6 @@
|
||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/rxjs": {
|
|
||||||
"version": "7.8.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
|
||||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
|
|
@ -13372,12 +13262,6 @@
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/safer-buffer": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/sax": {
|
"node_modules/sax": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/sax/-/sax-1.4.3.tgz",
|
||||||
|
|
@ -14251,6 +14135,7 @@
|
||||||
"version": "2.8.1",
|
"version": "2.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||||
|
"dev": true,
|
||||||
"license": "0BSD"
|
"license": "0BSD"
|
||||||
},
|
},
|
||||||
"node_modules/type-check": {
|
"node_modules/type-check": {
|
||||||
|
|
@ -14335,7 +14220,7 @@
|
||||||
"version": "7.16.0",
|
"version": "7.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
||||||
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
||||||
"devOptional": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/unicode-properties": {
|
"node_modules/unicode-properties": {
|
||||||
|
|
@ -14837,7 +14722,6 @@
|
||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
|
|
@ -15111,7 +14995,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"yaml": "bin.mjs"
|
"yaml": "bin.mjs"
|
||||||
},
|
},
|
||||||
|
|
@ -15270,18 +15153,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yoctocolors-cjs": {
|
|
||||||
"version": "2.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
|
|
||||||
"integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/zip-stream": {
|
"node_modules/zip-stream": {
|
||||||
"version": "6.0.1",
|
"version": "6.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz",
|
||||||
|
|
@ -15303,7 +15174,6 @@
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@clack/prompts": "^0.11.0",
|
||||||
"@kayvan/markdown-tree-parser": "^1.6.1",
|
"@kayvan/markdown-tree-parser": "^1.6.1",
|
||||||
"boxen": "^5.1.2",
|
"boxen": "^5.1.2",
|
||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
|
|
@ -77,7 +78,6 @@
|
||||||
"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",
|
||||||
"inquirer": "^9.3.8",
|
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"ora": "^5.4.1",
|
"ora": "^5.4.1",
|
||||||
"semver": "^7.6.3",
|
"semver": "^7.6.3",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
<task id="{bmad_folder}/core/tasks/sync-bug-tracking.xml" name="Sync Bug Tracking">
|
||||||
|
<objective>Sync bugs.yaml and bugs.md when a story is marked done, updating related bugs to "fixed" and features to "implemented"</objective>
|
||||||
|
|
||||||
|
<description>
|
||||||
|
This task is invoked by workflows (story-done, code-review) after a story is marked done.
|
||||||
|
It searches bugs.yaml for bugs/features linked to the completed story and updates their status.
|
||||||
|
For multi-story features, it only marks "implemented" when ALL linked stories are done.
|
||||||
|
</description>
|
||||||
|
|
||||||
|
<inputs>
|
||||||
|
<input name="story_key" required="true">The story key (e.g., "3-7-checkout-from-club-detail-page")</input>
|
||||||
|
<input name="story_id" required="false">The story ID (e.g., "3.7") - used for related_story matching</input>
|
||||||
|
<input name="bugs_yaml" required="true">Path to bugs.yaml file</input>
|
||||||
|
<input name="bugs_md" required="true">Path to bugs.md file</input>
|
||||||
|
<input name="sprint_status" required="true">Path to sprint-status.yaml file</input>
|
||||||
|
<input name="date" required="true">Current date for timestamps</input>
|
||||||
|
</inputs>
|
||||||
|
|
||||||
|
<outputs>
|
||||||
|
<output name="bugs_updated">List of bug IDs marked as fixed</output>
|
||||||
|
<output name="features_updated">List of feature IDs marked as implemented</output>
|
||||||
|
<output name="features_pending">List of feature IDs with incomplete stories</output>
|
||||||
|
</outputs>
|
||||||
|
|
||||||
|
<flow>
|
||||||
|
<step n="1" goal="Load bugs.yaml and check for existence">
|
||||||
|
<action>Load {bugs_yaml} if it exists</action>
|
||||||
|
<check if="bugs.yaml does not exist">
|
||||||
|
<action>Set bugs_updated = [], features_updated = [], features_pending = []</action>
|
||||||
|
<action>Return early - no bug tracking to sync</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="2" goal="Find matching bugs and features using multiple methods">
|
||||||
|
<action>Initialize: bugs_updated = [], features_updated = [], features_pending = []</action>
|
||||||
|
|
||||||
|
<action>Search for entries matching this story using ALL THREE methods:</action>
|
||||||
|
<action>1. Check sprint-status.yaml for comment "# Source: bugs.yaml/feature-XXX" or "# Source: bugs.yaml/bug-XXX" on the {story_key} line - this is the MOST RELIABLE method</action>
|
||||||
|
<action>2. Check related_story field in bugs.yaml matching {story_id} or {story_key}</action>
|
||||||
|
<action>3. Check sprint_stories arrays in feature_requests for entries containing {story_key}</action>
|
||||||
|
|
||||||
|
<critical>PRIORITY: Use sprint-status comment source if present - it's explicit and unambiguous</critical>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="3" goal="Update matching bugs">
|
||||||
|
<check if="matching bugs found in bugs section">
|
||||||
|
<action>For each matching bug:</action>
|
||||||
|
<action>- Update status: "triaged" or "routed" or "in-progress" → "fixed"</action>
|
||||||
|
<action>- Set fixed_date: {date}</action>
|
||||||
|
<action>- Set assigned_to: "dev-agent" (if not already set)</action>
|
||||||
|
<action>- Append to notes: "Auto-closed via sync-bug-tracking. Story {story_key} marked done on {date}."</action>
|
||||||
|
<action>- Add bug ID to bugs_updated list</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="4" goal="Update matching features (with multi-story check)">
|
||||||
|
<check if="matching features found in feature_requests section">
|
||||||
|
<action>For each matching feature (via related_story OR sprint_stories):</action>
|
||||||
|
|
||||||
|
<critical>MULTI-STORY FEATURE CHECK: If feature has sprint_stories array with multiple entries:</critical>
|
||||||
|
<action>1. Extract all story keys from sprint_stories (format: "story-key: status")</action>
|
||||||
|
<action>2. Load sprint-status.yaml and check development_status for EACH story</action>
|
||||||
|
<action>3. Only proceed if ALL stories in sprint_stories have status "done" in sprint-status.yaml</action>
|
||||||
|
<action>4. If any story is NOT done, add feature to features_pending and log: "Feature {feature_id} has incomplete stories: {incomplete_list}"</action>
|
||||||
|
|
||||||
|
<check if="ALL sprint_stories are done (or feature has single story that matches)">
|
||||||
|
<action>- Update status: "backlog" or "triaged" or "routed" or "in-progress" → "implemented"</action>
|
||||||
|
<action>- Set implemented_date: {date}</action>
|
||||||
|
<action>- Update sprint_stories entries to reflect done status</action>
|
||||||
|
<action>- Append to notes: "Auto-closed via sync-bug-tracking. Story {story_key} marked done on {date}."</action>
|
||||||
|
<action>- Add feature ID to features_updated list</action>
|
||||||
|
</check>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="5" goal="Save bugs.yaml updates">
|
||||||
|
<check if="bugs_updated is not empty OR features_updated is not empty">
|
||||||
|
<action>Save updated bugs.yaml, preserving all structure and comments</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="6" goal="Update bugs.md to match">
|
||||||
|
<check if="bugs_updated is not empty OR features_updated is not empty">
|
||||||
|
<action>Load {bugs_md}</action>
|
||||||
|
|
||||||
|
<check if="bugs_updated is not empty">
|
||||||
|
<action>For each bug in bugs_updated:</action>
|
||||||
|
<action>- Find the bug entry in "# Tracked Bugs" section</action>
|
||||||
|
<action>- Move it to "# Fixed Bugs" section</action>
|
||||||
|
<action>- Add [IMPLEMENTED] tag prefix with date: "[IMPLEMENTED] bug-XXX: Title [Fixed: {date}, Verified: pending]"</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="features_updated is not empty">
|
||||||
|
<action>For each feature in features_updated:</action>
|
||||||
|
<action>- Find the feature entry in "# Tracked Feature Requests" section</action>
|
||||||
|
<action>- Move it to "# Implemented Features" section</action>
|
||||||
|
<action>- Add [IMPLEMENTED] tag prefix with date: "[IMPLEMENTED] feature-XXX: Title [Implemented: {date}, Verified: pending]"</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>Update statistics section if present</action>
|
||||||
|
<action>Save updated bugs.md</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="7" goal="Return results">
|
||||||
|
<output>
|
||||||
|
Bug/Feature Sync Results:
|
||||||
|
{{#if bugs_updated}}
|
||||||
|
- Bugs marked fixed: {{bugs_updated}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if features_updated}}
|
||||||
|
- Features marked implemented: {{features_updated}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if features_pending}}
|
||||||
|
- Features with incomplete stories (not yet implemented): {{features_pending}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if no_matches}}
|
||||||
|
- No related bugs/features found for story {story_key}
|
||||||
|
{{/if}}
|
||||||
|
</output>
|
||||||
|
</step>
|
||||||
|
</flow>
|
||||||
|
</task>
|
||||||
|
|
@ -214,11 +214,24 @@
|
||||||
<output>ℹ️ Story status updated (no sprint tracking configured)</output>
|
<output>ℹ️ Story status updated (no sprint tracking configured)</output>
|
||||||
</check>
|
</check>
|
||||||
|
|
||||||
|
<!-- Sync bug tracking when story is marked done -->
|
||||||
|
<check if="{{new_status}} == 'done'">
|
||||||
|
<invoke-task path="{project-root}/.bmad/core/tasks/sync-bug-tracking.xml">
|
||||||
|
<param name="story_key">{{story_key}}</param>
|
||||||
|
<param name="story_id">{{story_id}}</param>
|
||||||
|
<param name="bugs_yaml">{output_folder}/bugs.yaml</param>
|
||||||
|
<param name="bugs_md">{output_folder}/bugs.md</param>
|
||||||
|
<param name="sprint_status">{sprint_status}</param>
|
||||||
|
<param name="date">{date}</param>
|
||||||
|
</invoke-task>
|
||||||
|
</check>
|
||||||
|
|
||||||
<output>**✅ Review Complete!**
|
<output>**✅ Review Complete!**
|
||||||
|
|
||||||
**Story Status:** {{new_status}}
|
**Story Status:** {{new_status}}
|
||||||
**Issues Fixed:** {{fixed_count}}
|
**Issues Fixed:** {{fixed_count}}
|
||||||
**Action Items Created:** {{action_count}}
|
**Action Items Created:** {{action_count}}
|
||||||
|
{{#if new_status == "done"}}**Bug/Feature Tracking:** Synced automatically{{/if}}
|
||||||
|
|
||||||
{{#if new_status == "done"}}Code review complete!{{else}}Address the action items and continue development.{{/if}}
|
{{#if new_status == "done"}}Code review complete!{{else}}Address the action items and continue development.{{/if}}
|
||||||
</output>
|
</output>
|
||||||
|
|
|
||||||
|
|
@ -1300,7 +1300,67 @@ Bob (Scrum Master): "See you all when prep work is done. Meeting adjourned!"
|
||||||
|
|
||||||
</step>
|
</step>
|
||||||
|
|
||||||
<step n="11" goal="Save Retrospective and Update Sprint Status">
|
<step n="11" goal="Sync Epic-Linked Bugs/Features to Closed Status">
|
||||||
|
<critical>Check bugs.yaml for bugs/features linked to this epic and close them</critical>
|
||||||
|
|
||||||
|
<action>Load {bugs_yaml} if it exists</action>
|
||||||
|
|
||||||
|
<check if="bugs.yaml exists">
|
||||||
|
<action>Search for entries with related_epic matching {{epic_number}}</action>
|
||||||
|
|
||||||
|
<action>For bugs section - find bugs with related_epic == {{epic_number}} AND status in ["fixed", "triaged", "routed"]:</action>
|
||||||
|
<check if="matching bugs found">
|
||||||
|
<action>For each matching bug:</action>
|
||||||
|
<action>- Move entry from "bugs" section to "closed_bugs" section</action>
|
||||||
|
<action>- Update status: → "closed"</action>
|
||||||
|
<action>- Set verified_by: "retrospective-workflow"</action>
|
||||||
|
<action>- Set verified_date: {date}</action>
|
||||||
|
<action>- Append to notes: "Auto-closed via epic retrospective. Epic {{epic_number}} completed on {date}."</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>For feature_requests section - find features with related_epic == {{epic_number}} AND status in ["implemented", "backlog", "in-progress"]:</action>
|
||||||
|
<check if="matching features found">
|
||||||
|
<action>For each matching feature:</action>
|
||||||
|
<action>- Move entry from "feature_requests" section to "implemented_features" section</action>
|
||||||
|
<action>- Update status: → "complete"</action>
|
||||||
|
<action>- Set completed_by: "retrospective-workflow"</action>
|
||||||
|
<action>- Set completed_date: {date}</action>
|
||||||
|
<action>- Append to notes: "Auto-closed via epic retrospective. Epic {{epic_number}} completed on {date}."</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>Update statistics section with new counts</action>
|
||||||
|
<action>Save updated bugs.yaml</action>
|
||||||
|
|
||||||
|
<check if="bugs/features were moved">
|
||||||
|
<action>Also update bugs.md:</action>
|
||||||
|
<action>- Remove [IMPLEMENTED] tag from closed items</action>
|
||||||
|
<action>- Move bug entries to "# Fixed Bugs" section if not already there</action>
|
||||||
|
<action>- Move feature entries to "# Implemented Features" section if not already there</action>
|
||||||
|
<action>- Add [CLOSED] or [COMPLETE] tag to indicate final status</action>
|
||||||
|
<action>Save updated bugs.md</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
Bug/Feature Closure:
|
||||||
|
{{#if bugs_closed}}
|
||||||
|
- Bugs closed for Epic {{epic_number}}: {{bugs_closed_list}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if features_completed}}
|
||||||
|
- Features completed for Epic {{epic_number}}: {{features_completed_list}}
|
||||||
|
{{/if}}
|
||||||
|
{{#if no_matches}}
|
||||||
|
- No outstanding bugs/features linked to Epic {{epic_number}}
|
||||||
|
{{/if}}
|
||||||
|
</output>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="bugs.yaml does not exist">
|
||||||
|
<action>Skip bug tracking sync - no bugs.yaml file present</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="12" goal="Save Retrospective and Update Sprint Status">
|
||||||
|
|
||||||
<action>Ensure retrospectives folder exists: {retrospectives_folder}</action>
|
<action>Ensure retrospectives folder exists: {retrospectives_folder}</action>
|
||||||
<action>Create folder if it doesn't exist</action>
|
<action>Create folder if it doesn't exist</action>
|
||||||
|
|
@ -1356,7 +1416,7 @@ Retrospective document was saved successfully, but {sprint_status_file} may need
|
||||||
|
|
||||||
</step>
|
</step>
|
||||||
|
|
||||||
<step n="12" goal="Final Summary and Handoff">
|
<step n="13" goal="Final Summary and Handoff">
|
||||||
|
|
||||||
<output>
|
<output>
|
||||||
**✅ Retrospective Complete, {user_name}!**
|
**✅ Retrospective Complete, {user_name}!**
|
||||||
|
|
|
||||||
|
|
@ -54,5 +54,9 @@ sprint_status_file: "{implementation_artifacts}/sprint-status.yaml"
|
||||||
story_directory: "{implementation_artifacts}"
|
story_directory: "{implementation_artifacts}"
|
||||||
retrospectives_folder: "{implementation_artifacts}"
|
retrospectives_folder: "{implementation_artifacts}"
|
||||||
|
|
||||||
|
# Bug tracking integration (optional)
|
||||||
|
bugs_yaml: "{planning_artifacts}/bugs.yaml"
|
||||||
|
bugs_md: "{planning_artifacts}/bugs.md"
|
||||||
|
|
||||||
standalone: true
|
standalone: true
|
||||||
web_bundle: false
|
web_bundle: false
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,26 @@
|
||||||
<note>After discovery, these content variables are available: {epics_content} (all epics loaded - uses FULL_LOAD strategy)</note>
|
<note>After discovery, these content variables are available: {epics_content} (all epics loaded - uses FULL_LOAD strategy)</note>
|
||||||
</step>
|
</step>
|
||||||
|
|
||||||
|
<step n="1.5" goal="Load bugs.yaml for bug/feature tracking (optional)">
|
||||||
|
<action>Check if {bugs_yaml} exists in {planning_artifacts}</action>
|
||||||
|
<check if="bugs_yaml exists">
|
||||||
|
<action>Read bugs.yaml using grep to find all bug-NNN and feature-NNN entries</action>
|
||||||
|
<action>For each bug/feature, extract:
|
||||||
|
- ID (e.g., bug-001, feature-003)
|
||||||
|
- Title
|
||||||
|
- Status (triaged, routed, in-progress, fixed/implemented, verified, closed)
|
||||||
|
- Recommended workflow (direct-fix, tech-spec, correct-course, backlog)
|
||||||
|
- Related stories (sprint_stories field for features)
|
||||||
|
</action>
|
||||||
|
<action>Build bug/feature inventory for inclusion in sprint status</action>
|
||||||
|
<action>Track feature-to-story mappings (feature-001 → stories 7-1, 7-2, etc.)</action>
|
||||||
|
</check>
|
||||||
|
<check if="bugs_yaml does not exist">
|
||||||
|
<output>Note: No bugs.yaml found - bug tracking not enabled for this project.</output>
|
||||||
|
<action>Continue without bug integration</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
<step n="2" goal="Build sprint status structure">
|
<step n="2" goal="Build sprint status structure">
|
||||||
<action>For each epic found, create entries in this order:</action>
|
<action>For each epic found, create entries in this order:</action>
|
||||||
|
|
||||||
|
|
@ -65,6 +85,17 @@ development_status:
|
||||||
epic-1-retrospective: optional
|
epic-1-retrospective: optional
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<action>If bugs.yaml was loaded, add bug/feature sources header comment:</action>
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# STORY SOURCES:
|
||||||
|
# ==============
|
||||||
|
# - epics.md: Primary source ({story_count} stories)
|
||||||
|
# - bugs.yaml: Feature-driven stories ({feature_story_count} stories from sprint_stories)
|
||||||
|
# - feature-001: 7-1, 7-2, 7-3 (from sprint_stories field)
|
||||||
|
# - feature-002: 3-7
|
||||||
|
```
|
||||||
|
|
||||||
</step>
|
</step>
|
||||||
|
|
||||||
<step n="3" goal="Apply intelligent status detection">
|
<step n="3" goal="Apply intelligent status detection">
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ variables:
|
||||||
epics_location: "{planning_artifacts}" # Directory containing epic*.md files
|
epics_location: "{planning_artifacts}" # Directory containing epic*.md files
|
||||||
epics_pattern: "epic*.md" # Pattern to find epic files
|
epics_pattern: "epic*.md" # Pattern to find epic files
|
||||||
|
|
||||||
|
# Bug tracking integration (optional)
|
||||||
|
bugs_yaml: "{planning_artifacts}/bugs.yaml" # Structured bug/feature metadata
|
||||||
|
bugs_md: "{planning_artifacts}/bugs.md" # Human-readable bug tracking
|
||||||
|
|
||||||
# Output configuration
|
# Output configuration
|
||||||
status_file: "{implementation_artifacts}/sprint-status.yaml"
|
status_file: "{implementation_artifacts}/sprint-status.yaml"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,15 +88,31 @@ Enter corrections (e.g., "1=in-progress, 2=backlog") or "skip" to continue witho
|
||||||
- IF any epic has status in-progress but has no associated stories: warn "in-progress epic has no stories"
|
- IF any epic has status in-progress but has no associated stories: warn "in-progress epic has no stories"
|
||||||
</step>
|
</step>
|
||||||
|
|
||||||
|
<step n="2.5" goal="Load bug/feature tracking status (optional)">
|
||||||
|
<action>Check if {bugs_yaml} exists</action>
|
||||||
|
<check if="bugs_yaml exists">
|
||||||
|
<action>Grep for bug-NNN and feature-NNN entries with status field</action>
|
||||||
|
<action>Count items by status: triaged, fixed/implemented (pending verify), verified, closed</action>
|
||||||
|
<action>Identify items needing action:
|
||||||
|
- Items with [IMPLEMENTED] tag → need verification
|
||||||
|
- Items with status "triaged" + workflow "direct-fix" → ready for implementation
|
||||||
|
</action>
|
||||||
|
<action>Store: bugs_pending_verify, bugs_triaged, features_pending_verify, features_triaged</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
<step n="3" goal="Select next action recommendation">
|
<step n="3" goal="Select next action recommendation">
|
||||||
<action>Pick the next recommended workflow using priority:</action>
|
<action>Pick the next recommended workflow using priority:</action>
|
||||||
<note>When selecting "first" story: sort by epic number, then story number (e.g., 1-1 before 1-2 before 2-1)</note>
|
<note>When selecting "first" story: sort by epic number, then story number (e.g., 1-1 before 1-2 before 2-1)</note>
|
||||||
1. If any story status == in-progress → recommend `dev-story` for the first in-progress story
|
<note>Bug verification takes priority over new story work to close the feedback loop</note>
|
||||||
2. Else if any story status == review → recommend `code-review` for the first review story
|
1. If any bug/feature has [IMPLEMENTED] tag (pending verify) → recommend `verify` for first pending item
|
||||||
3. Else if any story status == ready-for-dev → recommend `dev-story`
|
2. If any story status == in-progress → recommend `dev-story` for the first in-progress story
|
||||||
4. Else if any story status == backlog → recommend `create-story`
|
3. Else if any story status == review → recommend `code-review` for the first review story
|
||||||
5. Else if any retrospective status == optional → recommend `retrospective`
|
4. Else if any story status == ready-for-dev → recommend `dev-story`
|
||||||
6. Else → All implementation items done; suggest `workflow-status` to plan next phase
|
5. Else if any bug status == triaged with workflow == direct-fix → recommend `implement` for first triaged bug
|
||||||
|
6. Else if any story status == backlog → recommend `create-story`
|
||||||
|
7. Else if any retrospective status == optional → recommend `retrospective`
|
||||||
|
8. Else → All implementation items done; suggest `workflow-status` to plan next phase
|
||||||
<action>Store selected recommendation as: next_story_id, next_workflow_id, next_agent (SM/DEV as appropriate)</action>
|
<action>Store selected recommendation as: next_story_id, next_workflow_id, next_agent (SM/DEV as appropriate)</action>
|
||||||
</step>
|
</step>
|
||||||
|
|
||||||
|
|
@ -112,6 +128,11 @@ Enter corrections (e.g., "1=in-progress, 2=backlog") or "skip" to continue witho
|
||||||
|
|
||||||
**Epics:** backlog {{epic_backlog}}, in-progress {{epic_in_progress}}, done {{epic_done}}
|
**Epics:** backlog {{epic_backlog}}, in-progress {{epic_in_progress}}, done {{epic_done}}
|
||||||
|
|
||||||
|
{{#if bugs_yaml_exists}}
|
||||||
|
**Bugs:** triaged {{bugs_triaged}}, pending-verify {{bugs_pending_verify}}, closed {{bugs_closed}}
|
||||||
|
**Features:** triaged {{features_triaged}}, pending-verify {{features_pending_verify}}, complete {{features_complete}}
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
**Next Recommendation:** /bmad:bmm:workflows:{{next_workflow_id}} ({{next_story_id}})
|
**Next Recommendation:** /bmad:bmm:workflows:{{next_workflow_id}} ({{next_story_id}})
|
||||||
|
|
||||||
{{#if risks}}
|
{{#if risks}}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,9 @@ instructions: "{installed_path}/instructions.md"
|
||||||
variables:
|
variables:
|
||||||
sprint_status_file: "{implementation_artifacts}/sprint-status.yaml"
|
sprint_status_file: "{implementation_artifacts}/sprint-status.yaml"
|
||||||
tracking_system: "file-system"
|
tracking_system: "file-system"
|
||||||
|
# Bug tracking integration (optional)
|
||||||
|
bugs_yaml: "{planning_artifacts}/bugs.yaml"
|
||||||
|
bugs_md: "{planning_artifacts}/bugs.md"
|
||||||
|
|
||||||
# Smart input file references
|
# Smart input file references
|
||||||
input_file_patterns:
|
input_file_patterns:
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
# Story Approved Workflow Instructions (DEV Agent)
|
||||||
|
|
||||||
|
<critical>The workflow execution engine is governed by: {project-root}/.bmad/core/tasks/workflow.xml</critical>
|
||||||
|
<critical>You MUST have already loaded and processed: {installed_path}/workflow.yaml</critical>
|
||||||
|
<critical>Communicate all responses in {communication_language}</critical>
|
||||||
|
|
||||||
|
<workflow>
|
||||||
|
|
||||||
|
<critical>This workflow is run by DEV agent AFTER user confirms a story is approved (Definition of Done is complete)</critical>
|
||||||
|
<critical>Workflow: Update story file status to Done</critical>
|
||||||
|
|
||||||
|
<step n="1" goal="Find reviewed story to mark done" tag="sprint-status">
|
||||||
|
|
||||||
|
<check if="{story_path} is provided">
|
||||||
|
<action>Use {story_path} directly</action>
|
||||||
|
<action>Read COMPLETE story file and parse sections</action>
|
||||||
|
<action>Extract story_key from filename or story metadata</action>
|
||||||
|
<action>Verify Status is "review" - if not, HALT with message: "Story status must be 'review' to mark as done"</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="{story_path} is NOT provided">
|
||||||
|
<critical>MUST read COMPLETE sprint-status.yaml file from start to end to preserve order</critical>
|
||||||
|
<action>Load the FULL file: {output_folder}/sprint-status.yaml</action>
|
||||||
|
<action>Read ALL lines from beginning to end - do not skip any content</action>
|
||||||
|
<action>Parse the development_status section completely</action>
|
||||||
|
|
||||||
|
<action>Find FIRST story (reading in order from top to bottom) where: - Key matches pattern: number-number-name (e.g., "1-2-user-auth") - NOT an epic key (epic-X) or retrospective (epic-X-retrospective) - Status value equals "review"
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<check if="no story with status 'review' found">
|
||||||
|
<output>No stories with status "review" found
|
||||||
|
|
||||||
|
All stories are either still in development or already done.
|
||||||
|
|
||||||
|
**Next Steps:**
|
||||||
|
|
||||||
|
1. Run `dev-story` to implement stories
|
||||||
|
2. Run `code-review` if stories need review first
|
||||||
|
3. Check sprint-status.yaml for current story states
|
||||||
|
</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>Use the first reviewed story found</action>
|
||||||
|
<action>Find matching story file in {story_dir} using story_key pattern</action>
|
||||||
|
<action>Read the COMPLETE story file</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>Extract story_id and story_title from the story file</action>
|
||||||
|
|
||||||
|
<action>Find the "Status:" line (usually at the top)</action>
|
||||||
|
<action>Update story file: Change Status to "done"</action>
|
||||||
|
|
||||||
|
<action>Add completion notes to Dev Agent Record section:</action>
|
||||||
|
<action>Find "## Dev Agent Record" section and add:
|
||||||
|
|
||||||
|
```
|
||||||
|
### Completion Notes
|
||||||
|
**Completed:** {date}
|
||||||
|
**Definition of Done:** All acceptance criteria met, code reviewed, tests passing
|
||||||
|
```
|
||||||
|
|
||||||
|
</action>
|
||||||
|
|
||||||
|
<action>Save the story file</action>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="2" goal="Update sprint status to done" tag="sprint-status">
|
||||||
|
<action>Load the FULL file: {output_folder}/sprint-status.yaml</action>
|
||||||
|
<action>Find development_status key matching {story_key}</action>
|
||||||
|
<action>Verify current status is "review" (expected previous state)</action>
|
||||||
|
<action>Update development_status[{story_key}] = "done"</action>
|
||||||
|
<action>Save file, preserving ALL comments and structure including STATUS DEFINITIONS</action>
|
||||||
|
|
||||||
|
<check if="story key not found in file">
|
||||||
|
<output>Story file updated, but could not update sprint-status: {story_key} not found
|
||||||
|
|
||||||
|
Story is marked Done in file, but sprint-status.yaml may be out of sync.
|
||||||
|
</output>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="3" goal="Sync related bugs/features in bug tracking">
|
||||||
|
<critical>Invoke shared task to sync bugs.yaml and bugs.md for this completed story</critical>
|
||||||
|
|
||||||
|
<invoke-task path="{project-root}/.bmad/core/tasks/sync-bug-tracking.xml">
|
||||||
|
<param name="story_key">{story_key}</param>
|
||||||
|
<param name="story_id">{story_id}</param>
|
||||||
|
<param name="bugs_yaml">{bugs_yaml}</param>
|
||||||
|
<param name="bugs_md">{bugs_md}</param>
|
||||||
|
<param name="sprint_status">{sprint_status}</param>
|
||||||
|
<param name="date">{date}</param>
|
||||||
|
</invoke-task>
|
||||||
|
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="4" goal="Confirm completion to user">
|
||||||
|
|
||||||
|
<output>**Story Approved and Marked Done, {user_name}!**
|
||||||
|
|
||||||
|
Story file updated - Status: done
|
||||||
|
Sprint status updated: review → done
|
||||||
|
|
||||||
|
**Completed Story:**
|
||||||
|
|
||||||
|
- **ID:** {story_id}
|
||||||
|
- **Key:** {story_key}
|
||||||
|
- **Title:** {story_title}
|
||||||
|
- **Completed:** {date}
|
||||||
|
|
||||||
|
**Next Steps:**
|
||||||
|
|
||||||
|
1. Continue with next story in your backlog
|
||||||
|
- Run `create-story` for next backlog story
|
||||||
|
- Or run `dev-story` if ready stories exist
|
||||||
|
2. Check epic completion status
|
||||||
|
- Run `retrospective` workflow to check if epic is complete
|
||||||
|
- Epic retrospective will verify all stories are done
|
||||||
|
</output>
|
||||||
|
|
||||||
|
</step>
|
||||||
|
|
||||||
|
</workflow>
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
# Story Done Workflow (DEV Agent)
|
||||||
|
name: story-done
|
||||||
|
description: 'Marks a story as done (DoD complete), updates sprint-status → DONE, and syncs related bugs/features in bugs.yaml/bugs.md to [IMPLEMENTED] status.'
|
||||||
|
author: 'BMad'
|
||||||
|
|
||||||
|
# Critical variables from config
|
||||||
|
config_source: '{project-root}/.bmad/bmm/config.yaml'
|
||||||
|
output_folder: '{config_source}:output_folder'
|
||||||
|
user_name: '{config_source}:user_name'
|
||||||
|
communication_language: '{config_source}:communication_language'
|
||||||
|
date: system-generated
|
||||||
|
sprint_status: '{output_folder}/sprint-status.yaml'
|
||||||
|
|
||||||
|
# Workflow components
|
||||||
|
installed_path: '{project-root}/.bmad/bmm/workflows/4-implementation/story-done'
|
||||||
|
instructions: '{installed_path}/instructions.md'
|
||||||
|
|
||||||
|
# Variables and inputs
|
||||||
|
variables:
|
||||||
|
story_dir: '{config_source}:dev_ephemeral_location/stories' # Directory where stories are stored
|
||||||
|
bugs_yaml: '{output_folder}/bugs.yaml' # Bug/feature tracking structured data
|
||||||
|
bugs_md: '{output_folder}/bugs.md' # Bug/feature tracking human-readable log
|
||||||
|
|
||||||
|
# Output configuration - no output file, just status updates
|
||||||
|
default_output_file: ''
|
||||||
|
|
||||||
|
standalone: true
|
||||||
|
|
@ -0,0 +1,542 @@
|
||||||
|
# In-App Bug Reporting - Reference Implementation
|
||||||
|
|
||||||
|
This document provides a reference implementation for adding **in-app bug reporting** to your project. The BMAD bug-tracking workflow works without this feature (using manual `bugs.md` input), but in-app reporting provides a better user experience.
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The in-app bug reporting feature allows users to submit bug reports directly from your application. Reports are stored in your database and then synced to `bugs.md` by the triage workflow.
|
||||||
|
|
||||||
|
```
|
||||||
|
User -> UI Modal -> API -> Database -> Triage Workflow -> bugs.md/bugs.yaml
|
||||||
|
```
|
||||||
|
|
||||||
|
## Components Required
|
||||||
|
|
||||||
|
| Component | Purpose | Stack-Specific |
|
||||||
|
|-----------|---------|----------------|
|
||||||
|
| Database table | Store pending bug reports | Yes |
|
||||||
|
| API: Create report | Accept user submissions | Yes |
|
||||||
|
| API: Get pending | Fetch unsynced reports | Yes |
|
||||||
|
| API: Mark synced | Update status after sync | Yes |
|
||||||
|
| UI Modal | Bug report form | Yes |
|
||||||
|
| Validation schemas | Input validation | Partially |
|
||||||
|
|
||||||
|
## 1. Database Schema
|
||||||
|
|
||||||
|
### Drizzle ORM (PostgreSQL)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/server/db/schema.ts
|
||||||
|
|
||||||
|
import { pgTable, uuid, text, timestamp, index } from 'drizzle-orm/pg-core';
|
||||||
|
|
||||||
|
export const bugReports = pgTable(
|
||||||
|
'bug_reports',
|
||||||
|
{
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
organizationId: uuid('organization_id').notNull(), // For multi-tenant apps
|
||||||
|
reporterType: text('reporter_type').notNull(), // 'staff' | 'member' | 'user'
|
||||||
|
reporterId: uuid('reporter_id').notNull(),
|
||||||
|
title: text('title').notNull(),
|
||||||
|
description: text('description').notNull(),
|
||||||
|
userAgent: text('user_agent'),
|
||||||
|
pageUrl: text('page_url'),
|
||||||
|
platform: text('platform'), // 'Windows', 'macOS', 'iOS', etc.
|
||||||
|
browser: text('browser'), // 'Chrome', 'Safari', 'Firefox'
|
||||||
|
screenshotUrl: text('screenshot_url'), // Optional: cloud storage URL
|
||||||
|
status: text('status').notNull().default('new'), // 'new' | 'synced' | 'dismissed'
|
||||||
|
createdAt: timestamp('created_at', { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
syncedAt: timestamp('synced_at', { withTimezone: true })
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index('bug_reports_organization_id_idx').on(table.organizationId),
|
||||||
|
index('bug_reports_status_idx').on(table.status),
|
||||||
|
index('bug_reports_created_at_idx').on(table.createdAt)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const BUG_REPORT_STATUS = {
|
||||||
|
NEW: 'new',
|
||||||
|
SYNCED: 'synced',
|
||||||
|
DISMISSED: 'dismissed'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const REPORTER_TYPE = {
|
||||||
|
STAFF: 'staff',
|
||||||
|
MEMBER: 'member',
|
||||||
|
USER: 'user'
|
||||||
|
} as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Prisma Schema
|
||||||
|
|
||||||
|
```prisma
|
||||||
|
model BugReport {
|
||||||
|
id String @id @default(uuid())
|
||||||
|
organizationId String @map("organization_id")
|
||||||
|
reporterType String @map("reporter_type")
|
||||||
|
reporterId String @map("reporter_id")
|
||||||
|
title String
|
||||||
|
description String
|
||||||
|
userAgent String? @map("user_agent")
|
||||||
|
pageUrl String? @map("page_url")
|
||||||
|
platform String?
|
||||||
|
browser String?
|
||||||
|
screenshotUrl String? @map("screenshot_url")
|
||||||
|
status String @default("new")
|
||||||
|
createdAt DateTime @default(now()) @map("created_at")
|
||||||
|
syncedAt DateTime? @map("synced_at")
|
||||||
|
|
||||||
|
@@index([organizationId])
|
||||||
|
@@index([status])
|
||||||
|
@@index([createdAt])
|
||||||
|
@@map("bug_reports")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Validation Schemas
|
||||||
|
|
||||||
|
### Zod (TypeScript)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// src/lib/schemas/bug-report.ts
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const createBugReportSchema = z.object({
|
||||||
|
title: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(5, 'Title must be at least 5 characters')
|
||||||
|
.max(200, 'Title must be 200 characters or less'),
|
||||||
|
description: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(10, 'Description must be at least 10 characters')
|
||||||
|
.max(5000, 'Description must be 5000 characters or less'),
|
||||||
|
pageUrl: z.string().url().optional(),
|
||||||
|
userAgent: z.string().max(1000).optional(),
|
||||||
|
platform: z.string().max(50).optional(),
|
||||||
|
browser: z.string().max(50).optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export const markSyncedSchema = z.object({
|
||||||
|
ids: z.array(z.string().uuid()).min(1, 'At least one ID is required')
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SCREENSHOT_CONFIG = {
|
||||||
|
maxSizeBytes: 5 * 1024 * 1024, // 5MB
|
||||||
|
maxSizeMB: 5,
|
||||||
|
allowedTypes: ['image/jpeg', 'image/png', 'image/webp'] as const
|
||||||
|
} as const;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. API Endpoints
|
||||||
|
|
||||||
|
### POST /api/bug-reports - Create Report
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SvelteKit: src/routes/api/bug-reports/+server.ts
|
||||||
|
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { bugReports } from '$lib/server/db/schema';
|
||||||
|
import { createBugReportSchema } from '$lib/schemas/bug-report';
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
|
// Determine reporter from auth context
|
||||||
|
if (!locals.user) {
|
||||||
|
return json({ error: { code: 'UNAUTHORIZED' } }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.json();
|
||||||
|
const result = createBugReportSchema.safeParse(body);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return json({
|
||||||
|
error: { code: 'VALIDATION_ERROR', message: result.error.issues[0]?.message }
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, pageUrl, userAgent, platform, browser } = result.data;
|
||||||
|
|
||||||
|
const [newReport] = await db
|
||||||
|
.insert(bugReports)
|
||||||
|
.values({
|
||||||
|
organizationId: locals.user.organizationId,
|
||||||
|
reporterType: 'staff',
|
||||||
|
reporterId: locals.user.id,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
pageUrl,
|
||||||
|
userAgent,
|
||||||
|
platform,
|
||||||
|
browser
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return json({
|
||||||
|
data: {
|
||||||
|
bugReport: {
|
||||||
|
id: newReport.id,
|
||||||
|
title: newReport.title,
|
||||||
|
createdAt: newReport.createdAt.toISOString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, { status: 201 });
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### GET /api/bug-reports/pending - Fetch for Triage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SvelteKit: src/routes/api/bug-reports/pending/+server.ts
|
||||||
|
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { bugReports, BUG_REPORT_STATUS } from '$lib/server/db/schema';
|
||||||
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
|
export const GET: RequestHandler = async () => {
|
||||||
|
const reports = await db
|
||||||
|
.select()
|
||||||
|
.from(bugReports)
|
||||||
|
.where(eq(bugReports.status, BUG_REPORT_STATUS.NEW))
|
||||||
|
.orderBy(bugReports.createdAt);
|
||||||
|
|
||||||
|
// Map to workflow-expected format
|
||||||
|
const formatted = reports.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
title: r.title,
|
||||||
|
description: r.description,
|
||||||
|
reporterType: r.reporterType,
|
||||||
|
reporterName: 'Unknown', // Join with users table for real name
|
||||||
|
platform: r.platform,
|
||||||
|
browser: r.browser,
|
||||||
|
pageUrl: r.pageUrl,
|
||||||
|
screenshotUrl: r.screenshotUrl,
|
||||||
|
createdAt: r.createdAt.toISOString()
|
||||||
|
}));
|
||||||
|
|
||||||
|
return json({
|
||||||
|
data: {
|
||||||
|
reports: formatted,
|
||||||
|
count: formatted.length
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/bug-reports/mark-synced - Update After Sync
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// SvelteKit: src/routes/api/bug-reports/mark-synced/+server.ts
|
||||||
|
|
||||||
|
import { json } from '@sveltejs/kit';
|
||||||
|
import type { RequestHandler } from './$types';
|
||||||
|
import { db } from '$lib/server/db';
|
||||||
|
import { bugReports, BUG_REPORT_STATUS } from '$lib/server/db/schema';
|
||||||
|
import { inArray } from 'drizzle-orm';
|
||||||
|
import { markSyncedSchema } from '$lib/schemas/bug-report';
|
||||||
|
|
||||||
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
|
const body = await request.json();
|
||||||
|
const result = markSyncedSchema.safeParse(body);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return json({
|
||||||
|
error: { code: 'VALIDATION_ERROR', message: result.error.issues[0]?.message }
|
||||||
|
}, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await db
|
||||||
|
.update(bugReports)
|
||||||
|
.set({
|
||||||
|
status: BUG_REPORT_STATUS.SYNCED,
|
||||||
|
syncedAt: new Date()
|
||||||
|
})
|
||||||
|
.where(inArray(bugReports.id, result.data.ids))
|
||||||
|
.returning({ id: bugReports.id });
|
||||||
|
|
||||||
|
return json({
|
||||||
|
data: {
|
||||||
|
updatedCount: updated.length,
|
||||||
|
updatedIds: updated.map((r) => r.id)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. UI Component
|
||||||
|
|
||||||
|
### Svelte 5 (with shadcn-svelte)
|
||||||
|
|
||||||
|
```svelte
|
||||||
|
<!-- src/lib/components/BugReportModal.svelte -->
|
||||||
|
<script lang="ts">
|
||||||
|
import * as Dialog from '$lib/components/ui/dialog';
|
||||||
|
import { Button } from '$lib/components/ui/button';
|
||||||
|
import { Input } from '$lib/components/ui/input';
|
||||||
|
import { Textarea } from '$lib/components/ui/textarea';
|
||||||
|
import { toast } from 'svelte-sonner';
|
||||||
|
import { Bug } from 'lucide-svelte';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { open = $bindable(), onClose }: Props = $props();
|
||||||
|
|
||||||
|
let title = $state('');
|
||||||
|
let description = $state('');
|
||||||
|
let loading = $state(false);
|
||||||
|
|
||||||
|
// Auto-detect environment
|
||||||
|
let platform = $derived(browser ? detectPlatform() : '');
|
||||||
|
let browserName = $derived(browser ? detectBrowser() : '');
|
||||||
|
let currentUrl = $derived(browser ? window.location.href : '');
|
||||||
|
|
||||||
|
function detectPlatform(): string {
|
||||||
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
if (ua.includes('iphone') || ua.includes('ipad')) return 'iOS';
|
||||||
|
if (ua.includes('android')) return 'Android';
|
||||||
|
if (ua.includes('mac')) return 'macOS';
|
||||||
|
if (ua.includes('win')) return 'Windows';
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectBrowser(): string {
|
||||||
|
const ua = navigator.userAgent;
|
||||||
|
if (ua.includes('Chrome') && !ua.includes('Edg')) return 'Chrome';
|
||||||
|
if (ua.includes('Safari') && !ua.includes('Chrome')) return 'Safari';
|
||||||
|
if (ua.includes('Firefox')) return 'Firefox';
|
||||||
|
if (ua.includes('Edg')) return 'Edge';
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSubmit() {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/bug-reports', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
pageUrl: currentUrl,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
platform,
|
||||||
|
browser: browserName
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json();
|
||||||
|
toast.error(data.error?.message || 'Failed to submit');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.success('Bug report submitted');
|
||||||
|
onClose();
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog.Root bind:open onOpenChange={(o) => !o && onClose()}>
|
||||||
|
<Dialog.Content class="sm:max-w-[500px]">
|
||||||
|
<Dialog.Header>
|
||||||
|
<Dialog.Title class="flex items-center gap-2">
|
||||||
|
<Bug class="h-5 w-5" />
|
||||||
|
Report a Bug
|
||||||
|
</Dialog.Title>
|
||||||
|
</Dialog.Header>
|
||||||
|
|
||||||
|
<form onsubmit={(e) => { e.preventDefault(); handleSubmit(); }} class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<Input bind:value={title} placeholder="Brief summary" maxlength={200} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Textarea bind:value={description} placeholder="What happened?" rows={4} />
|
||||||
|
</div>
|
||||||
|
<div class="rounded-md bg-muted p-3 text-sm text-muted-foreground">
|
||||||
|
{platform} / {browserName}
|
||||||
|
</div>
|
||||||
|
<Dialog.Footer>
|
||||||
|
<Button variant="outline" onclick={onClose} disabled={loading}>Cancel</Button>
|
||||||
|
<Button type="submit" disabled={loading}>Submit</Button>
|
||||||
|
</Dialog.Footer>
|
||||||
|
</form>
|
||||||
|
</Dialog.Content>
|
||||||
|
</Dialog.Root>
|
||||||
|
```
|
||||||
|
|
||||||
|
### React (with shadcn/ui)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/components/BugReportModal.tsx
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
|
import { Bug } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean;
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BugReportModal({ open, onClose }: Props) {
|
||||||
|
const [title, setTitle] = useState('');
|
||||||
|
const [description, setDescription] = useState('');
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const detectPlatform = () => {
|
||||||
|
const ua = navigator.userAgent.toLowerCase();
|
||||||
|
if (ua.includes('iphone') || ua.includes('ipad')) return 'iOS';
|
||||||
|
if (ua.includes('android')) return 'Android';
|
||||||
|
if (ua.includes('mac')) return 'macOS';
|
||||||
|
if (ua.includes('win')) return 'Windows';
|
||||||
|
return 'Unknown';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/bug-reports', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
pageUrl: window.location.href,
|
||||||
|
userAgent: navigator.userAgent,
|
||||||
|
platform: detectPlatform()
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to submit');
|
||||||
|
toast.success('Bug report submitted');
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to submit bug report');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={(o) => !o && onClose()}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Bug className="h-5 w-5" />
|
||||||
|
Report a Bug
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Brief summary" />
|
||||||
|
<Textarea value={description} onChange={(e) => setDescription(e.target.value)} placeholder="What happened?" />
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={onClose} disabled={loading}>Cancel</Button>
|
||||||
|
<Button type="submit" disabled={loading}>Submit</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</form>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. Workflow Configuration
|
||||||
|
|
||||||
|
Update your project's `.bmad/bmm/config.yaml` to set the `project_url`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .bmad/bmm/config.yaml
|
||||||
|
|
||||||
|
project_url: "http://localhost:5173" # Dev
|
||||||
|
# project_url: "https://your-app.com" # Prod
|
||||||
|
```
|
||||||
|
|
||||||
|
The triage workflow will use this to call your API endpoints.
|
||||||
|
|
||||||
|
## 6. API Response Format
|
||||||
|
|
||||||
|
The workflow expects these response formats:
|
||||||
|
|
||||||
|
### GET /api/bug-reports/pending
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"reports": [
|
||||||
|
{
|
||||||
|
"id": "uuid",
|
||||||
|
"title": "Bug title",
|
||||||
|
"description": "Bug description",
|
||||||
|
"reporterType": "staff",
|
||||||
|
"reporterName": "John Doe",
|
||||||
|
"platform": "macOS",
|
||||||
|
"browser": "Chrome",
|
||||||
|
"pageUrl": "https://...",
|
||||||
|
"screenshotUrl": "https://...",
|
||||||
|
"createdAt": "2025-01-01T00:00:00Z"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"count": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### POST /api/bug-reports/mark-synced
|
||||||
|
|
||||||
|
Request:
|
||||||
|
```json
|
||||||
|
{ "ids": ["uuid1", "uuid2"] }
|
||||||
|
```
|
||||||
|
|
||||||
|
Response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"updatedCount": 2,
|
||||||
|
"updatedIds": ["uuid1", "uuid2"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7. Optional: Screenshot Storage
|
||||||
|
|
||||||
|
For screenshot uploads, you'll need cloud storage (R2, S3, etc.):
|
||||||
|
|
||||||
|
1. Create an upload endpoint: `POST /api/bug-reports/[id]/upload-screenshot`
|
||||||
|
2. Upload to cloud storage
|
||||||
|
3. Update `screenshotUrl` on the bug report record
|
||||||
|
|
||||||
|
## 8. Security Considerations
|
||||||
|
|
||||||
|
- **Authentication**: Create endpoint should require auth
|
||||||
|
- **API Key**: Consider adding API key auth for pending/mark-synced endpoints in production
|
||||||
|
- **Rate Limiting**: Add rate limits to prevent spam
|
||||||
|
- **Input Sanitization**: Validate all user input (handled by Zod schemas)
|
||||||
|
|
||||||
|
## Without In-App Reporting
|
||||||
|
|
||||||
|
If you don't implement in-app reporting, the workflow still works:
|
||||||
|
|
||||||
|
1. Users manually add bugs to `docs/bugs.md` under `# manual input`
|
||||||
|
2. Run `/triage` to process them
|
||||||
|
3. Workflow skips Step 0 (API sync) when no API is available
|
||||||
|
|
||||||
|
The workflows are designed to be flexible and work with or without the in-app feature.
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
# Step 1: Bug Tracking Workflow Initialization
|
||||||
|
|
||||||
|
## MANDATORY EXECUTION RULES (READ FIRST):
|
||||||
|
|
||||||
|
- 🛑 NEVER generate content without user input
|
||||||
|
- 📖 CRITICAL: ALWAYS read the complete step file before taking any action - partial understanding leads to incomplete triage
|
||||||
|
- 🔄 CRITICAL: When loading next step with 'C', ensure the entire file is read and understood before proceeding
|
||||||
|
- ✅ ALWAYS treat this as collaborative triage between peers
|
||||||
|
- 📋 YOU ARE A FACILITATOR, not an automatic processor
|
||||||
|
- 💬 FOCUS on initialization and setup only - don't look ahead to future steps
|
||||||
|
- 🚪 DETECT existing workflow state and handle continuation properly
|
||||||
|
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||||
|
|
||||||
|
## EXECUTION PROTOCOLS:
|
||||||
|
|
||||||
|
- 🎯 Show your analysis before taking any action
|
||||||
|
- 💾 Initialize bugs.yaml if needed
|
||||||
|
- 📖 Track workflow state for potential continuation
|
||||||
|
- 🚫 FORBIDDEN to load next step until setup is complete
|
||||||
|
|
||||||
|
## CONTEXT BOUNDARIES:
|
||||||
|
|
||||||
|
- Variables from workflow.md are available in memory
|
||||||
|
- bugs.yaml tracks all structured bug metadata
|
||||||
|
- bugs.md is the user-facing input file
|
||||||
|
- Don't assume knowledge from other steps
|
||||||
|
|
||||||
|
## YOUR TASK:
|
||||||
|
|
||||||
|
Initialize the Bug Tracking workflow by detecting existing state, discovering input files, and setting up for collaborative triage.
|
||||||
|
|
||||||
|
## INITIALIZATION SEQUENCE:
|
||||||
|
|
||||||
|
### 1. Check for Existing Session
|
||||||
|
|
||||||
|
First, check workflow state:
|
||||||
|
|
||||||
|
- Look for existing `{bugs_output}` (bugs.yaml)
|
||||||
|
- If exists, grep for bugs with `status: triaged` (pending implementation)
|
||||||
|
- Check `{bugs_input}` (bugs.md) for items in "# manual input" section
|
||||||
|
|
||||||
|
### 2. Handle Continuation (If Pending Work Exists)
|
||||||
|
|
||||||
|
If bugs.yaml exists with triaged bugs awaiting action:
|
||||||
|
|
||||||
|
- **STOP here** and load `./step-01b-continue.md` immediately
|
||||||
|
- Do not proceed with fresh initialization
|
||||||
|
- Let step-01b handle the continuation logic
|
||||||
|
|
||||||
|
### 3. Fresh Workflow Setup (If No Pending Work)
|
||||||
|
|
||||||
|
If no bugs.yaml exists OR no pending triaged bugs:
|
||||||
|
|
||||||
|
#### A. Input File Discovery
|
||||||
|
|
||||||
|
Discover and validate required files:
|
||||||
|
|
||||||
|
**Required Files:**
|
||||||
|
- `{bugs_input}` (bugs.md) - User-facing bug reports
|
||||||
|
- Must have "# manual input" section for new bugs
|
||||||
|
- May have "# Tracked Bugs" and "# Fixed Bugs" sections
|
||||||
|
|
||||||
|
**Optional Context Files:**
|
||||||
|
- `{sprint_status}` - Current sprint context (which stories are in progress)
|
||||||
|
- `{epics_file}` - For mapping bugs to related stories/epics
|
||||||
|
|
||||||
|
#### B. Initialize bugs.yaml (If Not Exists)
|
||||||
|
|
||||||
|
If bugs.yaml doesn't exist, create it with header structure:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Bug Tracking Database
|
||||||
|
# Generated by bug-tracking workflow
|
||||||
|
# Last updated: {date}
|
||||||
|
|
||||||
|
# Severity Definitions:
|
||||||
|
# - critical: Prevents core functionality, crashes, data loss
|
||||||
|
# - high: Blocks major features, significantly degrades UX
|
||||||
|
# - medium: Affects subset of users, minor impact with workaround
|
||||||
|
# - low: Cosmetic, edge case, or minor inconvenience
|
||||||
|
|
||||||
|
# Complexity Definitions:
|
||||||
|
# - trivial: One-line fix, obvious solution
|
||||||
|
# - small: Single file/component, solution clear
|
||||||
|
# - medium: Multiple files OR requires investigation
|
||||||
|
# - complex: Architectural change, affects many areas
|
||||||
|
|
||||||
|
# Workflow Routing Matrix:
|
||||||
|
# - critical + any → correct-course
|
||||||
|
# - high + trivial → direct-fix
|
||||||
|
# - high + small → tech-spec
|
||||||
|
# - high + medium/complex → correct-course
|
||||||
|
# - medium + trivial → direct-fix
|
||||||
|
# - medium + small → tech-spec
|
||||||
|
# - medium + medium/complex → correct-course
|
||||||
|
# - low + trivial → direct-fix
|
||||||
|
# - low + small/medium/complex → backlog
|
||||||
|
|
||||||
|
bugs: []
|
||||||
|
features: []
|
||||||
|
closed_bugs: []
|
||||||
|
|
||||||
|
statistics:
|
||||||
|
total_active: 0
|
||||||
|
by_severity:
|
||||||
|
critical: 0
|
||||||
|
high: 0
|
||||||
|
medium: 0
|
||||||
|
low: 0
|
||||||
|
last_updated: {date}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### C. Scan for New Bugs
|
||||||
|
|
||||||
|
Read ONLY the "# manual input" section from bugs.md:
|
||||||
|
- Grep for "# manual input" to find starting line
|
||||||
|
- Grep for next section header to find ending line
|
||||||
|
- Read just that range (do NOT read entire file)
|
||||||
|
|
||||||
|
Count items found in manual input section.
|
||||||
|
|
||||||
|
#### D. Complete Initialization and Report
|
||||||
|
|
||||||
|
Report to user:
|
||||||
|
|
||||||
|
"Welcome {user_name}! I've initialized the Bug Tracking workspace for {project_name}.
|
||||||
|
|
||||||
|
**Files Status:**
|
||||||
|
- bugs.md: {found/created} - {count} item(s) in manual input section
|
||||||
|
- bugs.yaml: {found/created} - {active_count} active bugs tracked
|
||||||
|
|
||||||
|
**Context Files:**
|
||||||
|
- Sprint Status: {loaded/not found}
|
||||||
|
- Epics: {loaded/not found}
|
||||||
|
|
||||||
|
**Ready for Triage:**
|
||||||
|
{count} new item(s) found in manual input section.
|
||||||
|
|
||||||
|
[S] Sync bug reports from API first (if app integration configured)
|
||||||
|
[C] Continue to parse and triage bugs
|
||||||
|
[Q] Quit - no new bugs to triage"
|
||||||
|
|
||||||
|
## SUCCESS METRICS:
|
||||||
|
|
||||||
|
✅ Existing workflow detected and handed off to step-01b correctly
|
||||||
|
✅ Fresh workflow initialized with bugs.yaml structure
|
||||||
|
✅ Input files discovered and validated
|
||||||
|
✅ Manual input section scanned for new items
|
||||||
|
✅ User informed of status and can proceed
|
||||||
|
|
||||||
|
## FAILURE MODES:
|
||||||
|
|
||||||
|
❌ Proceeding with fresh initialization when pending work exists
|
||||||
|
❌ Not creating bugs.yaml with proper header/definitions
|
||||||
|
❌ Reading entire bugs.md instead of just manual input section
|
||||||
|
❌ Not reporting status to user before proceeding
|
||||||
|
|
||||||
|
❌ **CRITICAL**: Reading only partial step file - leads to incomplete understanding
|
||||||
|
❌ **CRITICAL**: Proceeding with 'C' without fully reading the next step file
|
||||||
|
|
||||||
|
## NEXT STEP:
|
||||||
|
|
||||||
|
- If user selects [S], load `./step-02-sync.md` to sync from API
|
||||||
|
- If user selects [C], load `./step-03-parse.md` to parse and identify new bugs
|
||||||
|
- If user selects [Q], end workflow gracefully
|
||||||
|
|
||||||
|
Remember: Do NOT proceed until user explicitly selects an option from the menu!
|
||||||
|
|
@ -0,0 +1,110 @@
|
||||||
|
# Step 1b: Continue Existing Bug Tracking Session
|
||||||
|
|
||||||
|
## MANDATORY EXECUTION RULES (READ FIRST):
|
||||||
|
|
||||||
|
- 🛑 NEVER generate content without user input
|
||||||
|
- 📖 CRITICAL: ALWAYS read the complete step file before taking any action
|
||||||
|
- 🔄 CRITICAL: When loading next step with 'C', ensure the entire file is read and understood before proceeding
|
||||||
|
- ✅ ALWAYS treat this as collaborative triage between peers
|
||||||
|
- 📋 YOU ARE A FACILITATOR, not an automatic processor
|
||||||
|
- 🚪 This step handles CONTINUATION of existing work
|
||||||
|
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||||
|
|
||||||
|
## EXECUTION PROTOCOLS:
|
||||||
|
|
||||||
|
- 🎯 Summarize existing state before offering options
|
||||||
|
- 💾 Preserve all existing bugs.yaml data
|
||||||
|
- 📖 Help user understand where they left off
|
||||||
|
- 🚫 FORBIDDEN to lose or overwrite existing triage work
|
||||||
|
|
||||||
|
## CONTEXT BOUNDARIES:
|
||||||
|
|
||||||
|
- Variables from workflow.md are available in memory
|
||||||
|
- bugs.yaml contains existing structured data
|
||||||
|
- User may have triaged bugs awaiting implementation
|
||||||
|
- Don't re-triage already processed bugs
|
||||||
|
|
||||||
|
## YOUR TASK:
|
||||||
|
|
||||||
|
Welcome user back and summarize the current state of bug tracking, offering relevant continuation options.
|
||||||
|
|
||||||
|
## CONTINUATION SEQUENCE:
|
||||||
|
|
||||||
|
### 1. Load Current State
|
||||||
|
|
||||||
|
Read bugs.yaml and extract:
|
||||||
|
- Total active bugs count
|
||||||
|
- Bugs by status (triaged, implemented, verified)
|
||||||
|
- Bugs by severity breakdown
|
||||||
|
- Bugs by recommended workflow
|
||||||
|
|
||||||
|
### 2. Check for New Input
|
||||||
|
|
||||||
|
Scan "# manual input" section of bugs.md:
|
||||||
|
- Count items not yet in bugs.yaml
|
||||||
|
- These are new bugs needing triage
|
||||||
|
|
||||||
|
### 3. Present Continuation Summary
|
||||||
|
|
||||||
|
Report to user:
|
||||||
|
|
||||||
|
"Welcome back, {user_name}! Here's your Bug Tracking status for {project_name}.
|
||||||
|
|
||||||
|
**Current State:**
|
||||||
|
- Active Bugs: {total_active}
|
||||||
|
- Triaged (awaiting action): {triaged_count}
|
||||||
|
- Implemented (awaiting verification): {implemented_count}
|
||||||
|
- By Severity: Critical: {critical} | High: {high} | Medium: {medium} | Low: {low}
|
||||||
|
|
||||||
|
**Workflow Routing:**
|
||||||
|
- Direct Fix: {direct_fix_count} bug(s)
|
||||||
|
- Tech-Spec: {tech_spec_count} bug(s)
|
||||||
|
- Correct-Course: {correct_course_count} bug(s)
|
||||||
|
- Backlog: {backlog_count} bug(s)
|
||||||
|
|
||||||
|
**New Items:**
|
||||||
|
- {new_count} new item(s) found in manual input section
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
[T] Triage new bugs ({new_count} items)
|
||||||
|
[I] Implement a bug - `/implement bug-NNN`
|
||||||
|
[V] Verify implemented bugs - `/verify`
|
||||||
|
[L] List bugs by status/severity
|
||||||
|
[Q] Quit"
|
||||||
|
|
||||||
|
### 4. Handle User Selection
|
||||||
|
|
||||||
|
Based on user choice:
|
||||||
|
|
||||||
|
- **[T] Triage**: Load `./step-03-parse.md` to process new bugs
|
||||||
|
- **[I] Implement**: Guide user to run `/implement bug-NNN` skill
|
||||||
|
- **[V] Verify**: Guide user to run `/verify` skill
|
||||||
|
- **[L] List**: Show filtered bug list, then return to menu
|
||||||
|
- **[Q] Quit**: End workflow gracefully
|
||||||
|
|
||||||
|
## SUCCESS METRICS:
|
||||||
|
|
||||||
|
✅ Existing state accurately summarized
|
||||||
|
✅ New items detected and counted
|
||||||
|
✅ User given clear options based on current state
|
||||||
|
✅ Appropriate next step loaded based on selection
|
||||||
|
|
||||||
|
## FAILURE MODES:
|
||||||
|
|
||||||
|
❌ Losing track of existing triaged bugs
|
||||||
|
❌ Re-triaging already processed bugs
|
||||||
|
❌ Not detecting new items in manual input
|
||||||
|
❌ Proceeding without user selection
|
||||||
|
|
||||||
|
❌ **CRITICAL**: Reading only partial step file
|
||||||
|
❌ **CRITICAL**: Proceeding without explicit user menu selection
|
||||||
|
|
||||||
|
## NEXT STEP:
|
||||||
|
|
||||||
|
Load appropriate step based on user selection:
|
||||||
|
- [T] → `./step-03-parse.md`
|
||||||
|
- [I], [V] → Guide to relevant skill, then return here
|
||||||
|
- [L] → Display list, return to this menu
|
||||||
|
- [Q] → End workflow
|
||||||
|
|
||||||
|
Remember: Do NOT proceed until user explicitly selects an option!
|
||||||
|
|
@ -0,0 +1,145 @@
|
||||||
|
# Step 2: Sync Bug Reports from API
|
||||||
|
|
||||||
|
## MANDATORY EXECUTION RULES (READ FIRST):
|
||||||
|
|
||||||
|
- 🛑 NEVER generate content without user input
|
||||||
|
- 📖 CRITICAL: ALWAYS read the complete step file before taking any action
|
||||||
|
- 🔄 CRITICAL: When loading next step with 'C', ensure the entire file is read and understood before proceeding
|
||||||
|
- ✅ ALWAYS treat this as collaborative triage between peers
|
||||||
|
- 📋 YOU ARE A FACILITATOR, not an automatic processor
|
||||||
|
- 🌐 This step handles OPTIONAL API integration for in-app bug reporting
|
||||||
|
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||||
|
|
||||||
|
## EXECUTION PROTOCOLS:
|
||||||
|
|
||||||
|
- 🎯 Attempt API sync only if configured
|
||||||
|
- 💾 Preserve existing manual input entries
|
||||||
|
- 📖 Format synced reports as markdown entries
|
||||||
|
- 🚫 FORBIDDEN to lose manually entered bugs
|
||||||
|
|
||||||
|
## CONTEXT BOUNDARIES:
|
||||||
|
|
||||||
|
- Variables from workflow.md are available in memory
|
||||||
|
- `project_url` may or may not be configured
|
||||||
|
- API endpoints are optional - gracefully handle if unavailable
|
||||||
|
- This step can be skipped if no API integration
|
||||||
|
|
||||||
|
## YOUR TASK:
|
||||||
|
|
||||||
|
Sync pending bug reports from the application's PostgreSQL database via API, formatting them as markdown entries in bugs.md.
|
||||||
|
|
||||||
|
## SYNC SEQUENCE:
|
||||||
|
|
||||||
|
### 1. Check API Configuration
|
||||||
|
|
||||||
|
Verify `{project_url}` is configured:
|
||||||
|
- If not configured or user skipped this step, proceed to step-03
|
||||||
|
- If configured, attempt API connection
|
||||||
|
|
||||||
|
### 2. Fetch Pending Reports
|
||||||
|
|
||||||
|
**API Call:**
|
||||||
|
```
|
||||||
|
GET {project_url}/api/bug-reports/pending
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"reports": [...],
|
||||||
|
"count": number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Report Fields:**
|
||||||
|
- `id` - Database ID
|
||||||
|
- `title` - Bug title
|
||||||
|
- `description` - Bug description
|
||||||
|
- `reporterType` - Type of reporter (user, staff, admin)
|
||||||
|
- `reporterName` - Name of reporter
|
||||||
|
- `platform` - Platform (iOS, Android, web)
|
||||||
|
- `browser` - Browser if web
|
||||||
|
- `pageUrl` - URL where bug occurred
|
||||||
|
- `screenshotUrl` - Optional screenshot
|
||||||
|
- `createdAt` - Timestamp
|
||||||
|
|
||||||
|
### 3. Handle No Reports
|
||||||
|
|
||||||
|
If count == 0:
|
||||||
|
|
||||||
|
"No new bug reports from the application API.
|
||||||
|
|
||||||
|
[C] Continue to triage existing manual input
|
||||||
|
[Q] Quit - nothing to process"
|
||||||
|
|
||||||
|
### 4. Format Reports as Markdown
|
||||||
|
|
||||||
|
For each report, create markdown entry:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Bug: {title}
|
||||||
|
|
||||||
|
{description}
|
||||||
|
|
||||||
|
Reported by: {reporterName} ({reporterType})
|
||||||
|
Date: {createdAt formatted as YYYY-MM-DD}
|
||||||
|
Platform: {platform} / {browser}
|
||||||
|
Page: {pageUrl}
|
||||||
|
{if screenshotUrl: Screenshot: {screenshotUrl}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. Insert into bugs.md
|
||||||
|
|
||||||
|
- Read the "# manual input" section location from bugs.md
|
||||||
|
- Insert new markdown entries after the "# manual input" header
|
||||||
|
- Preserve any existing manual input entries
|
||||||
|
- Write updated bugs.md
|
||||||
|
|
||||||
|
### 6. Mark Reports as Synced
|
||||||
|
|
||||||
|
**API Call:**
|
||||||
|
```
|
||||||
|
POST {project_url}/api/bug-reports/mark-synced
|
||||||
|
Body: { "ids": [array of synced report IDs] }
|
||||||
|
```
|
||||||
|
|
||||||
|
This updates status to 'synced' so reports won't be fetched again.
|
||||||
|
|
||||||
|
### 7. Report Sync Results
|
||||||
|
|
||||||
|
"**Synced {count} bug report(s) from application:**
|
||||||
|
|
||||||
|
{for each report:}
|
||||||
|
- {title} (from {reporterName})
|
||||||
|
{end for}
|
||||||
|
|
||||||
|
These have been added to the manual input section of bugs.md.
|
||||||
|
|
||||||
|
[C] Continue to parse and triage all bugs
|
||||||
|
[Q] Quit"
|
||||||
|
|
||||||
|
## SUCCESS METRICS:
|
||||||
|
|
||||||
|
✅ API availability checked gracefully
|
||||||
|
✅ Pending reports fetched and formatted
|
||||||
|
✅ Existing manual entries preserved
|
||||||
|
✅ Reports marked as synced in database
|
||||||
|
✅ User informed of sync results
|
||||||
|
|
||||||
|
## FAILURE MODES:
|
||||||
|
|
||||||
|
❌ Crashing if API unavailable (should gracefully skip)
|
||||||
|
❌ Overwriting existing manual input entries
|
||||||
|
❌ Not marking reports as synced (causes duplicates)
|
||||||
|
❌ Proceeding without user confirmation
|
||||||
|
|
||||||
|
❌ **CRITICAL**: Reading only partial step file
|
||||||
|
❌ **CRITICAL**: Proceeding without explicit user selection
|
||||||
|
|
||||||
|
## NEXT STEP:
|
||||||
|
|
||||||
|
After user selects [C], load `./step-03-parse.md` to parse and identify all bugs needing triage.
|
||||||
|
|
||||||
|
Remember: Do NOT proceed until user explicitly selects [C] from the menu!
|
||||||
|
|
@ -0,0 +1,164 @@
|
||||||
|
# Step 3: Parse and Identify New Bugs
|
||||||
|
|
||||||
|
## MANDATORY EXECUTION RULES (READ FIRST):
|
||||||
|
|
||||||
|
- 🛑 NEVER generate content without user input
|
||||||
|
- 📖 CRITICAL: ALWAYS read the complete step file before taking any action
|
||||||
|
- 🔄 CRITICAL: When loading next step with 'C', ensure the entire file is read and understood before proceeding
|
||||||
|
- ✅ ALWAYS treat this as collaborative triage between peers
|
||||||
|
- 📋 YOU ARE A FACILITATOR, not an automatic processor
|
||||||
|
- 🔍 This step PARSES input only - triage happens in next step
|
||||||
|
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||||
|
|
||||||
|
## EXECUTION PROTOCOLS:
|
||||||
|
|
||||||
|
- 🎯 Parse manual input section thoroughly
|
||||||
|
- 💾 Compare against existing bugs.yaml entries
|
||||||
|
- 📖 Extract all available information from informal reports
|
||||||
|
- 🚫 FORBIDDEN to start triage in this step - parsing only
|
||||||
|
|
||||||
|
## CONTEXT BOUNDARIES:
|
||||||
|
|
||||||
|
- Variables from workflow.md are available in memory
|
||||||
|
- bugs.yaml contains existing triaged bugs
|
||||||
|
- Only parse "# manual input" section of bugs.md
|
||||||
|
- Do NOT read entire bugs.md file
|
||||||
|
|
||||||
|
## YOUR TASK:
|
||||||
|
|
||||||
|
Parse the "# manual input" section of bugs.md, extract bug information, and identify which items need triage.
|
||||||
|
|
||||||
|
## PARSE SEQUENCE:
|
||||||
|
|
||||||
|
### 1. Read Manual Input Section
|
||||||
|
|
||||||
|
Section-based reading of bugs.md:
|
||||||
|
- Grep for "# manual input" to find starting line number
|
||||||
|
- Grep for next section header ("# Tracked Bugs", "# Tracked Feature Requests", "# Fixed Bugs") to find ending line
|
||||||
|
- Read just that range using offset/limit (do NOT read entire file)
|
||||||
|
- If no closing section found within initial window, expand read range and retry
|
||||||
|
|
||||||
|
### 2. Search Existing IDs in bugs.yaml
|
||||||
|
|
||||||
|
Do NOT read entire bugs.yaml file:
|
||||||
|
- Grep for `id: bug-[0-9]+` pattern to find all existing bug IDs
|
||||||
|
- Grep for `id: feature-[0-9]+` pattern to find all existing feature IDs
|
||||||
|
- This enables duplicate detection and next-ID generation
|
||||||
|
|
||||||
|
### 3. Parse Bug Reports
|
||||||
|
|
||||||
|
Expected formats in manual input (informal, user-written):
|
||||||
|
|
||||||
|
**Format A: Markdown Headers**
|
||||||
|
```markdown
|
||||||
|
## Bug: Title Here
|
||||||
|
|
||||||
|
Description text, possibly multi-paragraph.
|
||||||
|
|
||||||
|
Reported by: Name
|
||||||
|
Date: YYYY-MM-DD
|
||||||
|
Related: Story 2.7
|
||||||
|
Platform: iOS
|
||||||
|
```
|
||||||
|
|
||||||
|
**Format B: Bullet Lists**
|
||||||
|
```markdown
|
||||||
|
- **Title (Platform)**: Description text. CRITICAL if urgent.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Format C: Numbered Lists**
|
||||||
|
```markdown
|
||||||
|
1. Title - Description text
|
||||||
|
2. Another bug - More description
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Extract Information
|
||||||
|
|
||||||
|
For each bug report, extract:
|
||||||
|
|
||||||
|
| Field | Required | Notes |
|
||||||
|
|-------|----------|-------|
|
||||||
|
| Title | Yes | First line or header |
|
||||||
|
| Description | Yes | May be multi-paragraph |
|
||||||
|
| Reported by | No | Extract if mentioned |
|
||||||
|
| Date | No | Extract if mentioned |
|
||||||
|
| Related story | No | e.g., "2-7", "Story 2.7" |
|
||||||
|
| Platform | No | iOS, Android, web, all |
|
||||||
|
| Reproduction steps | No | If provided |
|
||||||
|
| Severity hints | No | "CRITICAL", "urgent", etc. |
|
||||||
|
|
||||||
|
### 5. Categorize Items
|
||||||
|
|
||||||
|
Compare extracted bugs with existing bugs.yaml:
|
||||||
|
|
||||||
|
- **New bugs**: Not in bugs.yaml yet (need full triage)
|
||||||
|
- **Updated bugs**: In bugs.yaml but description changed (need re-triage)
|
||||||
|
- **Feature requests**: Items that are enhancements, not bugs
|
||||||
|
- **Unchanged**: Already triaged, skip
|
||||||
|
|
||||||
|
### 6. Handle No New Bugs
|
||||||
|
|
||||||
|
If NO new bugs found:
|
||||||
|
|
||||||
|
"No new bugs found in the manual input section.
|
||||||
|
|
||||||
|
All items have already been triaged and are tracked in bugs.yaml.
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
1. Add new bugs to docs/bugs.md (informal format)
|
||||||
|
2. View bugs.yaml to see structured bug tracking
|
||||||
|
3. Route existing triaged bugs to workflows
|
||||||
|
|
||||||
|
[Q] Quit - nothing to triage"
|
||||||
|
|
||||||
|
**HALT** - Do not proceed.
|
||||||
|
|
||||||
|
### 7. Present Parsed Items
|
||||||
|
|
||||||
|
"**Parsed {total_count} item(s) from manual input:**
|
||||||
|
|
||||||
|
**New Bugs ({new_count}):**
|
||||||
|
{for each new bug:}
|
||||||
|
- {extracted_title}
|
||||||
|
- Description: {first 100 chars}...
|
||||||
|
- Platform: {platform or "not specified"}
|
||||||
|
- Related: {story or "not specified"}
|
||||||
|
{end for}
|
||||||
|
|
||||||
|
**Feature Requests ({feature_count}):**
|
||||||
|
{for each feature:}
|
||||||
|
- {title}
|
||||||
|
{end for}
|
||||||
|
|
||||||
|
**Already Triaged ({unchanged_count}):**
|
||||||
|
{list titles of skipped items}
|
||||||
|
|
||||||
|
Ready to triage {new_count} new bug(s) and {feature_count} feature request(s).
|
||||||
|
|
||||||
|
[C] Continue to triage
|
||||||
|
[E] Edit - re-parse with corrections
|
||||||
|
[Q] Quit"
|
||||||
|
|
||||||
|
## SUCCESS METRICS:
|
||||||
|
|
||||||
|
✅ Manual input section read efficiently (not entire file)
|
||||||
|
✅ All formats parsed correctly (headers, bullets, numbered)
|
||||||
|
✅ Existing bugs detected to prevent duplicates
|
||||||
|
✅ New vs updated vs unchanged correctly categorized
|
||||||
|
✅ User shown summary and can proceed
|
||||||
|
|
||||||
|
## FAILURE MODES:
|
||||||
|
|
||||||
|
❌ Reading entire bugs.md instead of section
|
||||||
|
❌ Missing bugs due to format not recognized
|
||||||
|
❌ Not detecting duplicates against bugs.yaml
|
||||||
|
❌ Starting triage in this step (should only parse)
|
||||||
|
|
||||||
|
❌ **CRITICAL**: Reading only partial step file
|
||||||
|
❌ **CRITICAL**: Proceeding without user selection
|
||||||
|
|
||||||
|
## NEXT STEP:
|
||||||
|
|
||||||
|
After user selects [C], load `./step-04-triage.md` to perform triage analysis on each new bug.
|
||||||
|
|
||||||
|
Remember: Do NOT proceed until user explicitly selects [C] from the menu!
|
||||||
|
|
@ -0,0 +1,212 @@
|
||||||
|
# Step 4: Triage Each Bug
|
||||||
|
|
||||||
|
## MANDATORY EXECUTION RULES (READ FIRST):
|
||||||
|
|
||||||
|
- 🛑 NEVER generate content without user input
|
||||||
|
- 📖 CRITICAL: ALWAYS read the complete step file before taking any action
|
||||||
|
- 🔄 CRITICAL: When loading next step with 'C', ensure the entire file is read and understood before proceeding
|
||||||
|
- ✅ ALWAYS treat this as collaborative triage between peers
|
||||||
|
- 📋 YOU ARE A FACILITATOR - ask clarifying questions when needed
|
||||||
|
- 🎯 This step performs the CORE TRIAGE analysis
|
||||||
|
- ⚠️ ABSOLUTELY NO TIME ESTIMATES - AI development speed varies widely
|
||||||
|
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||||
|
|
||||||
|
## EXECUTION PROTOCOLS:
|
||||||
|
|
||||||
|
- 🎯 Triage ONE bug at a time with user confirmation
|
||||||
|
- 💾 Track triage decisions for bugs.yaml update
|
||||||
|
- 📖 Ask clarifying questions when severity/complexity unclear
|
||||||
|
- 🚫 FORBIDDEN to auto-triage without user review
|
||||||
|
|
||||||
|
## CONTEXT BOUNDARIES:
|
||||||
|
|
||||||
|
- Parsed bugs from step-03 are in memory
|
||||||
|
- Reference bugs.yaml header for severity/complexity definitions
|
||||||
|
- Reference epics.md for story mapping
|
||||||
|
- Each bug gets full triage analysis
|
||||||
|
|
||||||
|
## YOUR TASK:
|
||||||
|
|
||||||
|
Perform collaborative triage analysis on each parsed bug, assessing severity, complexity, effort, workflow routing, and documentation impact.
|
||||||
|
|
||||||
|
## TRIAGE SEQUENCE (FOR EACH BUG):
|
||||||
|
|
||||||
|
### 1. Generate Bug ID
|
||||||
|
|
||||||
|
- Find highest existing bug-NNN from step-03 grep results
|
||||||
|
- Assign next sequential ID (e.g., bug-006)
|
||||||
|
- Format: `bug-` + zero-padded 3-digit number
|
||||||
|
- For features: `feature-` + zero-padded 3-digit number
|
||||||
|
|
||||||
|
### 2. Assess Severity
|
||||||
|
|
||||||
|
**Severity Levels:**
|
||||||
|
| Level | Criteria |
|
||||||
|
|-------|----------|
|
||||||
|
| critical | Prevents core functionality, crashes, data loss |
|
||||||
|
| high | Blocks major features, significantly degrades UX but has workaround |
|
||||||
|
| medium | Affects subset of users, minor impact |
|
||||||
|
| low | Cosmetic, edge case, minor inconvenience |
|
||||||
|
|
||||||
|
**Analysis Questions:**
|
||||||
|
- Does it prevent core functionality? → critical
|
||||||
|
- Does it cause crashes or data loss? → critical
|
||||||
|
- Does it block major features? → high
|
||||||
|
- Does it significantly degrade UX but have workaround? → high
|
||||||
|
- Does it affect subset of users with minor impact? → medium
|
||||||
|
- Is it cosmetic or edge case? → low
|
||||||
|
|
||||||
|
**If Unclear - ASK:**
|
||||||
|
"**Clarification needed for: {bug_title}**
|
||||||
|
|
||||||
|
I need more information to assess severity:
|
||||||
|
1. Does this bug prevent users from completing core flows?
|
||||||
|
2. Does the bug cause crashes or data loss?
|
||||||
|
3. How many users are affected? (all users, specific platform, edge case)
|
||||||
|
4. Is there a workaround available?
|
||||||
|
|
||||||
|
Please provide additional context."
|
||||||
|
|
||||||
|
### 3. Assess Complexity
|
||||||
|
|
||||||
|
**Complexity Levels:**
|
||||||
|
| Level | Criteria |
|
||||||
|
|-------|----------|
|
||||||
|
| trivial | One-line fix, obvious solution |
|
||||||
|
| small | Single file/component, solution clear |
|
||||||
|
| medium | Multiple files OR requires investigation |
|
||||||
|
| complex | Architectural change, affects many areas |
|
||||||
|
|
||||||
|
**If Unclear - ASK:**
|
||||||
|
"**Clarification needed for: {bug_title}**
|
||||||
|
|
||||||
|
To estimate complexity, I need:
|
||||||
|
1. Have you identified the root cause, or does it need investigation?
|
||||||
|
2. Which file(s) or component(s) are affected?
|
||||||
|
3. Is this isolated or does it affect multiple parts of the app?
|
||||||
|
|
||||||
|
Please provide technical details if available."
|
||||||
|
|
||||||
|
### 4. Determine Workflow Routing
|
||||||
|
|
||||||
|
**Routing Matrix:**
|
||||||
|
| Severity | Complexity | Workflow |
|
||||||
|
|----------|------------|----------|
|
||||||
|
| critical | any | correct-course |
|
||||||
|
| high | trivial | direct-fix |
|
||||||
|
| high | small | tech-spec |
|
||||||
|
| high | medium/complex | correct-course |
|
||||||
|
| medium | trivial | direct-fix |
|
||||||
|
| medium | small | tech-spec |
|
||||||
|
| medium | medium/complex | correct-course |
|
||||||
|
| low | trivial | direct-fix |
|
||||||
|
| low | small+ | backlog |
|
||||||
|
|
||||||
|
### 5. Map to Related Story/Epic
|
||||||
|
|
||||||
|
- If bug mentions story ID (e.g., "2-7"), use that
|
||||||
|
- Otherwise, infer from description using epic keywords
|
||||||
|
- Reference epics.md for story matching
|
||||||
|
- Format: `{epic_number}-{story_number}` or null
|
||||||
|
|
||||||
|
### 6. Determine Affected Platform
|
||||||
|
|
||||||
|
Extract from description:
|
||||||
|
- `all` - Default if not specified
|
||||||
|
- `ios` - iOS only
|
||||||
|
- `android` - Android only
|
||||||
|
- `web` - Web only
|
||||||
|
|
||||||
|
### 7. Assess Documentation Impact
|
||||||
|
|
||||||
|
**PRD Impact** (`doc_impact.prd: true/false`)
|
||||||
|
Set TRUE if issue:
|
||||||
|
- Conflicts with stated product goals
|
||||||
|
- Requires changing MVP scope
|
||||||
|
- Adds/removes/modifies core functionality
|
||||||
|
- Changes success metrics
|
||||||
|
- Affects multiple epics
|
||||||
|
|
||||||
|
**Architecture Impact** (`doc_impact.architecture: true/false`)
|
||||||
|
Set TRUE if issue:
|
||||||
|
- Requires new system components
|
||||||
|
- Changes data model (new tables, schema)
|
||||||
|
- Affects API contracts
|
||||||
|
- Introduces new dependencies
|
||||||
|
- Changes auth/security model
|
||||||
|
|
||||||
|
**UX Impact** (`doc_impact.ux: true/false`)
|
||||||
|
Set TRUE if issue:
|
||||||
|
- Adds new screens or navigation
|
||||||
|
- Changes existing user flows
|
||||||
|
- Requires new UI components
|
||||||
|
- Affects accessibility
|
||||||
|
|
||||||
|
**If any doc_impact is TRUE AND workflow != correct-course:**
|
||||||
|
- Override workflow to `correct-course`
|
||||||
|
- Add note: "Workflow elevated due to documentation impact"
|
||||||
|
|
||||||
|
### 8. Add Triage Notes
|
||||||
|
|
||||||
|
Document reasoning:
|
||||||
|
- Why this severity? (business impact, user impact)
|
||||||
|
- Why this complexity? (investigation needed, files affected)
|
||||||
|
- Why this workflow? (routing logic applied)
|
||||||
|
- Suggested next steps or investigation areas
|
||||||
|
|
||||||
|
### 9. Present Triage for Confirmation
|
||||||
|
|
||||||
|
"**Triage: {bug_id} - {bug_title}**
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Severity | {severity} |
|
||||||
|
| Complexity | {complexity} |
|
||||||
|
| Platform | {platform} |
|
||||||
|
| Workflow | {recommended_workflow} |
|
||||||
|
| Related | {related_story or 'None'} |
|
||||||
|
|
||||||
|
**Documentation Impact:**
|
||||||
|
- PRD: {yes/no}
|
||||||
|
- Architecture: {yes/no}
|
||||||
|
- UX: {yes/no}
|
||||||
|
|
||||||
|
**Triage Notes:**
|
||||||
|
{triage_notes}
|
||||||
|
|
||||||
|
[A] Accept triage
|
||||||
|
[M] Modify - adjust severity/complexity/workflow
|
||||||
|
[S] Skip - don't triage this item now
|
||||||
|
[N] Next bug (after accepting)"
|
||||||
|
|
||||||
|
### 10. Handle Modifications
|
||||||
|
|
||||||
|
If user selects [M]:
|
||||||
|
- Ask which field to modify
|
||||||
|
- Accept new value
|
||||||
|
- Re-present triage for confirmation
|
||||||
|
|
||||||
|
## SUCCESS METRICS:
|
||||||
|
|
||||||
|
✅ Each bug triaged with user confirmation
|
||||||
|
✅ Unclear items prompted for clarification
|
||||||
|
✅ Routing matrix applied correctly
|
||||||
|
✅ Documentation impact assessed
|
||||||
|
✅ Triage notes document reasoning
|
||||||
|
|
||||||
|
## FAILURE MODES:
|
||||||
|
|
||||||
|
❌ Auto-triaging without user review
|
||||||
|
❌ Not asking clarifying questions when needed
|
||||||
|
❌ Incorrect routing matrix application
|
||||||
|
❌ Missing documentation impact assessment
|
||||||
|
❌ Not documenting triage reasoning
|
||||||
|
|
||||||
|
❌ **CRITICAL**: Reading only partial step file
|
||||||
|
❌ **CRITICAL**: Proceeding without user confirmation per bug
|
||||||
|
|
||||||
|
## NEXT STEP:
|
||||||
|
|
||||||
|
After ALL bugs triaged (user selected [A] or [N] for each), load `./step-05-update.md` to update bugs.yaml and bugs.md.
|
||||||
|
|
||||||
|
Remember: Triage each bug individually with user confirmation!
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
# Step 5: Update Files with Triaged Metadata
|
||||||
|
|
||||||
|
## MANDATORY EXECUTION RULES (READ FIRST):
|
||||||
|
|
||||||
|
- 🛑 NEVER generate content without user input
|
||||||
|
- 📖 CRITICAL: ALWAYS read the complete step file before taking any action
|
||||||
|
- 🔄 CRITICAL: When loading next step with 'C', ensure the entire file is read and understood before proceeding
|
||||||
|
- ✅ ALWAYS treat this as collaborative triage between peers
|
||||||
|
- 📋 YOU ARE A FACILITATOR, not an automatic processor
|
||||||
|
- 💾 This step WRITES the triage results to files
|
||||||
|
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||||
|
|
||||||
|
## EXECUTION PROTOCOLS:
|
||||||
|
|
||||||
|
- 🎯 Update both bugs.yaml and bugs.md atomically
|
||||||
|
- 💾 Preserve ALL existing data - append only
|
||||||
|
- 📖 Move items from manual input to tracked sections
|
||||||
|
- 🚫 FORBIDDEN to lose or corrupt existing data
|
||||||
|
|
||||||
|
## CONTEXT BOUNDARIES:
|
||||||
|
|
||||||
|
- Triage decisions from step-04 are in memory
|
||||||
|
- bugs.yaml structure defined in step-01
|
||||||
|
- bugs.md sections: manual input, Tracked Bugs, Tracked Feature Requests, Fixed Bugs
|
||||||
|
- Preserve header comments and definitions
|
||||||
|
|
||||||
|
## YOUR TASK:
|
||||||
|
|
||||||
|
Write all triaged metadata to bugs.yaml and move triaged items from "# manual input" to appropriate tracked sections in bugs.md.
|
||||||
|
|
||||||
|
## UPDATE SEQUENCE:
|
||||||
|
|
||||||
|
### 1. Update bugs.yaml
|
||||||
|
|
||||||
|
#### A. Load Existing Structure
|
||||||
|
|
||||||
|
Read current bugs.yaml (if exists):
|
||||||
|
- Preserve ALL header comments and definitions
|
||||||
|
- Preserve existing `bugs:` array entries
|
||||||
|
- Preserve existing `features:` array entries
|
||||||
|
- Preserve existing `closed_bugs:` array
|
||||||
|
|
||||||
|
#### B. Add New Bug Entries
|
||||||
|
|
||||||
|
For each triaged bug, add to `bugs:` array:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- id: bug-NNN
|
||||||
|
title: "Bug title"
|
||||||
|
description: |
|
||||||
|
Full description text
|
||||||
|
Can be multi-line
|
||||||
|
severity: critical|high|medium|low
|
||||||
|
complexity: trivial|small|medium|complex
|
||||||
|
affected_platform: all|ios|android|web
|
||||||
|
recommended_workflow: direct-fix|tech-spec|correct-course|backlog
|
||||||
|
related_story: "X-Y" or null
|
||||||
|
status: triaged
|
||||||
|
reported_by: "Name" or null
|
||||||
|
reported_date: "YYYY-MM-DD" or null
|
||||||
|
triaged_date: "{date}"
|
||||||
|
doc_impact:
|
||||||
|
prd: true|false
|
||||||
|
architecture: true|false
|
||||||
|
ux: true|false
|
||||||
|
notes: "Impact description" or null
|
||||||
|
triage_notes: |
|
||||||
|
Reasoning for severity, complexity, workflow decisions
|
||||||
|
implemented_by: null
|
||||||
|
implemented_date: null
|
||||||
|
verified_by: null
|
||||||
|
verified_date: null
|
||||||
|
```
|
||||||
|
|
||||||
|
#### C. Add Feature Request Entries
|
||||||
|
|
||||||
|
For features, add to `features:` array with similar structure.
|
||||||
|
|
||||||
|
#### D. Update Statistics
|
||||||
|
|
||||||
|
Recalculate statistics section:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
statistics:
|
||||||
|
total_active: {count of non-closed bugs}
|
||||||
|
by_severity:
|
||||||
|
critical: {count}
|
||||||
|
high: {count}
|
||||||
|
medium: {count}
|
||||||
|
low: {count}
|
||||||
|
by_status:
|
||||||
|
triaged: {count}
|
||||||
|
implemented: {count}
|
||||||
|
verified: {count}
|
||||||
|
by_workflow:
|
||||||
|
direct-fix: {count}
|
||||||
|
tech-spec: {count}
|
||||||
|
correct-course: {count}
|
||||||
|
backlog: {count}
|
||||||
|
last_updated: "{date}"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### E. Write bugs.yaml
|
||||||
|
|
||||||
|
Write complete bugs.yaml file preserving all content.
|
||||||
|
|
||||||
|
### 2. Update bugs.md
|
||||||
|
|
||||||
|
#### A. Section-Based Reading
|
||||||
|
|
||||||
|
Use grep to locate section line numbers:
|
||||||
|
- "# manual input"
|
||||||
|
- "# Tracked Bugs"
|
||||||
|
- "# Tracked Feature Requests"
|
||||||
|
- "# Fixed Bugs"
|
||||||
|
|
||||||
|
Read only relevant sections with offset/limit.
|
||||||
|
|
||||||
|
#### B. Remove from Manual Input
|
||||||
|
|
||||||
|
For each triaged item:
|
||||||
|
- Remove the original entry from "# manual input" section
|
||||||
|
- Handle both header format and bullet format
|
||||||
|
|
||||||
|
#### C. Add to Tracked Bugs
|
||||||
|
|
||||||
|
For each triaged bug, add to "# Tracked Bugs" section:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### {bug_id}: {title}
|
||||||
|
|
||||||
|
{brief_description}
|
||||||
|
|
||||||
|
- **Severity:** {severity}
|
||||||
|
- **Complexity:** {complexity}
|
||||||
|
- **Platform:** {platform}
|
||||||
|
- **Workflow:** {workflow}
|
||||||
|
- **Related:** {story or "None"}
|
||||||
|
{if doc_impact flagged:}
|
||||||
|
- **Doc Impact:** {PRD|Architecture|UX as applicable}
|
||||||
|
{end if}
|
||||||
|
|
||||||
|
**Notes:** {triage_notes_summary}
|
||||||
|
|
||||||
|
---
|
||||||
|
```
|
||||||
|
|
||||||
|
Create "# Tracked Bugs" section if it doesn't exist.
|
||||||
|
|
||||||
|
#### D. Add to Tracked Feature Requests
|
||||||
|
|
||||||
|
For features, add to "# Tracked Feature Requests" section with similar format.
|
||||||
|
|
||||||
|
#### E. Write bugs.md
|
||||||
|
|
||||||
|
Write updated bugs.md preserving all sections.
|
||||||
|
|
||||||
|
### 3. Confirm Updates
|
||||||
|
|
||||||
|
"**Files Updated:**
|
||||||
|
|
||||||
|
**bugs.yaml:**
|
||||||
|
- Added {bug_count} new bug(s)
|
||||||
|
- Added {feature_count} new feature request(s)
|
||||||
|
- Total active bugs: {total_active}
|
||||||
|
- Statistics recalculated
|
||||||
|
|
||||||
|
**bugs.md:**
|
||||||
|
- Removed {count} item(s) from manual input
|
||||||
|
- Added {bug_count} bug(s) to Tracked Bugs section
|
||||||
|
- Added {feature_count} feature(s) to Tracked Feature Requests section
|
||||||
|
|
||||||
|
[C] Continue to summary
|
||||||
|
[R] Review changes - show diff
|
||||||
|
[U] Undo - restore previous state"
|
||||||
|
|
||||||
|
## SUCCESS METRICS:
|
||||||
|
|
||||||
|
✅ bugs.yaml updated with all triaged metadata
|
||||||
|
✅ bugs.md items moved from manual input to tracked sections
|
||||||
|
✅ Statistics accurately recalculated
|
||||||
|
✅ All existing data preserved
|
||||||
|
✅ User confirmed updates
|
||||||
|
|
||||||
|
## FAILURE MODES:
|
||||||
|
|
||||||
|
❌ Losing existing bugs.yaml entries
|
||||||
|
❌ Corrupting bugs.md structure
|
||||||
|
❌ Items remaining in manual input after triage
|
||||||
|
❌ Statistics not matching actual data
|
||||||
|
❌ Not preserving header comments/definitions
|
||||||
|
|
||||||
|
❌ **CRITICAL**: Reading only partial step file
|
||||||
|
❌ **CRITICAL**: Proceeding without user confirmation
|
||||||
|
|
||||||
|
## NEXT STEP:
|
||||||
|
|
||||||
|
After user selects [C], load `./step-06-complete.md` to present final triage summary.
|
||||||
|
|
||||||
|
Remember: Do NOT proceed until user explicitly selects [C] from the menu!
|
||||||
|
|
@ -0,0 +1,180 @@
|
||||||
|
# Step 6: Triage Complete - Summary and Next Steps
|
||||||
|
|
||||||
|
## MANDATORY EXECUTION RULES (READ FIRST):
|
||||||
|
|
||||||
|
- 🛑 NEVER generate content without user input
|
||||||
|
- 📖 CRITICAL: ALWAYS read the complete step file before taking any action
|
||||||
|
- ✅ ALWAYS treat this as collaborative triage between peers
|
||||||
|
- 📋 YOU ARE A FACILITATOR, not an automatic processor
|
||||||
|
- 🎉 This is the FINAL step - present comprehensive summary
|
||||||
|
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||||
|
|
||||||
|
## EXECUTION PROTOCOLS:
|
||||||
|
|
||||||
|
- 🎯 Present comprehensive triage summary
|
||||||
|
- 💾 All data already written in step-05
|
||||||
|
- 📖 Guide user to next actions
|
||||||
|
- 🚫 FORBIDDEN to modify files in this step
|
||||||
|
|
||||||
|
## CONTEXT BOUNDARIES:
|
||||||
|
|
||||||
|
- All triage decisions finalized in previous steps
|
||||||
|
- bugs.yaml and bugs.md already updated
|
||||||
|
- This step is READ-ONLY presentation
|
||||||
|
- Focus on actionable next steps
|
||||||
|
|
||||||
|
## YOUR TASK:
|
||||||
|
|
||||||
|
Present a comprehensive summary of the triage session and guide the user to appropriate next actions based on workflow recommendations.
|
||||||
|
|
||||||
|
## COMPLETION SEQUENCE:
|
||||||
|
|
||||||
|
### 1. Present Triage Summary
|
||||||
|
|
||||||
|
"**Bug Triage Complete, {user_name}!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Triaged Items
|
||||||
|
|
||||||
|
{for each triaged bug:}
|
||||||
|
|
||||||
|
### {bug_id}: {bug_title}
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|-------|-------|
|
||||||
|
| Severity | {severity} |
|
||||||
|
| Complexity | {complexity} |
|
||||||
|
| Platform | {platform} |
|
||||||
|
| Workflow | {recommended_workflow} |
|
||||||
|
| Related | {related_story or 'None'} |
|
||||||
|
|
||||||
|
{if doc_impact flagged:}
|
||||||
|
**Documentation Impact:**
|
||||||
|
- PRD: {yes/no}
|
||||||
|
- Architecture: {yes/no}
|
||||||
|
- UX: {yes/no}
|
||||||
|
- Notes: {doc_impact_notes}
|
||||||
|
{end if}
|
||||||
|
|
||||||
|
**Triage Reasoning:**
|
||||||
|
{triage_notes}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
{end for}
|
||||||
|
|
||||||
|
## Updated Files
|
||||||
|
|
||||||
|
- **bugs.yaml** - Structured metadata for all triaged items
|
||||||
|
- **bugs.md** - Moved triaged items to Tracked sections
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Statistics Summary
|
||||||
|
|
||||||
|
| Metric | Count |
|
||||||
|
|--------|-------|
|
||||||
|
| Total Active Bugs | {total_active} |
|
||||||
|
| Critical | {critical_count} |
|
||||||
|
| High | {high_count} |
|
||||||
|
| Medium | {medium_count} |
|
||||||
|
| Low | {low_count} |
|
||||||
|
|
||||||
|
{if any doc_impact flagged:}
|
||||||
|
|
||||||
|
## Documentation Updates Required
|
||||||
|
|
||||||
|
Items with documentation impact have been routed to `correct-course` workflow:
|
||||||
|
- PRD Impact: {prd_impact_count} item(s)
|
||||||
|
- Architecture Impact: {arch_impact_count} item(s)
|
||||||
|
- UX Impact: {ux_impact_count} item(s)
|
||||||
|
{end if}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Recommendations
|
||||||
|
|
||||||
|
### Direct Fix ({direct_fix_count} items)
|
||||||
|
Quick fixes with obvious solutions. No spec needed.
|
||||||
|
|
||||||
|
**Command:** `/implement bug-NNN`
|
||||||
|
|
||||||
|
{list bug IDs for direct-fix}
|
||||||
|
|
||||||
|
### Tech-Spec ({tech_spec_count} items)
|
||||||
|
Require technical specification before implementation.
|
||||||
|
|
||||||
|
**Process:** Create tech-spec first, then `/implement`
|
||||||
|
|
||||||
|
{list bug IDs for tech-spec}
|
||||||
|
|
||||||
|
### Correct-Course ({correct_course_count} items)
|
||||||
|
Need impact analysis before proceeding.
|
||||||
|
|
||||||
|
**Process:** Run correct-course workflow for impact analysis
|
||||||
|
|
||||||
|
{list bug IDs for correct-course}
|
||||||
|
|
||||||
|
### Backlog ({backlog_count} items)
|
||||||
|
Deferred - low priority items for future consideration.
|
||||||
|
|
||||||
|
{list bug IDs for backlog}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
**To implement a bug fix:**
|
||||||
|
```
|
||||||
|
/implement bug-NNN
|
||||||
|
```
|
||||||
|
|
||||||
|
**To verify after testing:**
|
||||||
|
```
|
||||||
|
/verify bug-NNN
|
||||||
|
```
|
||||||
|
|
||||||
|
**To verify all implemented bugs:**
|
||||||
|
```
|
||||||
|
/verify
|
||||||
|
```
|
||||||
|
|
||||||
|
**To list bugs by platform:**
|
||||||
|
```
|
||||||
|
/list-bugs android
|
||||||
|
/list-bugs ios
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Thank you for completing the triage session!"
|
||||||
|
|
||||||
|
### 2. End Workflow
|
||||||
|
|
||||||
|
The workflow is complete. No further steps.
|
||||||
|
|
||||||
|
## SUCCESS METRICS:
|
||||||
|
|
||||||
|
✅ Comprehensive summary presented
|
||||||
|
✅ All triaged items listed with metadata
|
||||||
|
✅ Statistics accurately displayed
|
||||||
|
✅ Workflow recommendations clear
|
||||||
|
✅ Next step commands provided
|
||||||
|
✅ User knows how to proceed
|
||||||
|
|
||||||
|
## FAILURE MODES:
|
||||||
|
|
||||||
|
❌ Incomplete summary missing items
|
||||||
|
❌ Statistics not matching bugs.yaml
|
||||||
|
❌ Unclear next step guidance
|
||||||
|
❌ Modifying files in this step (should be read-only)
|
||||||
|
|
||||||
|
## WORKFLOW COMPLETE
|
||||||
|
|
||||||
|
This is the final step. The bug tracking triage workflow is complete.
|
||||||
|
|
||||||
|
User can now:
|
||||||
|
- Run `/implement bug-NNN` to fix bugs
|
||||||
|
- Run `/verify` to verify implemented bugs
|
||||||
|
- Add new bugs to bugs.md and run triage again
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
---
|
||||||
|
name: bug-tracking
|
||||||
|
description: Triage user-reported bugs from bugs.md, generate structured metadata in bugs.yaml, and route to appropriate workflow
|
||||||
|
main_config: '{project-root}/_bmad/bmm/config.yaml'
|
||||||
|
web_bundle: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# Bug Tracking Workflow
|
||||||
|
|
||||||
|
**Goal:** Transform informal bug reports into structured, actionable metadata with severity assessment, complexity estimation, and workflow routing recommendations.
|
||||||
|
|
||||||
|
**Your Role:** You are a triage facilitator collaborating with a peer. This is a partnership, not a client-vendor relationship. You bring structured analysis and triage methodology, while the user brings domain expertise and context about their product. Work together to efficiently categorize and route bugs for resolution.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WORKFLOW ARCHITECTURE
|
||||||
|
|
||||||
|
This uses **micro-file architecture** for disciplined execution:
|
||||||
|
|
||||||
|
- Each step is a self-contained file with embedded rules
|
||||||
|
- Sequential progression with user control at each step
|
||||||
|
- State tracked via bugs.yaml metadata
|
||||||
|
- Append-only updates to bugs.md (move triaged items, never delete)
|
||||||
|
- You NEVER proceed to a step file if the current step file indicates the user must approve and indicate continuation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## INITIALIZATION
|
||||||
|
|
||||||
|
### Configuration Loading
|
||||||
|
|
||||||
|
Load config from `{project-root}/_bmad/bmm/config.yaml` and resolve:
|
||||||
|
|
||||||
|
- `project_name`, `output_folder`, `user_name`
|
||||||
|
- `communication_language`, `date` as system-generated current datetime
|
||||||
|
- `dev_ephemeral_location` for sprint-status.yaml location
|
||||||
|
- ✅ YOU MUST ALWAYS SPEAK OUTPUT In your Agent communication style with the config `{communication_language}`
|
||||||
|
|
||||||
|
### Paths
|
||||||
|
|
||||||
|
- `installed_path` = `{project-root}/_bmad/bmm/workflows/bug-tracking`
|
||||||
|
- `bugs_input` = `{output_folder}/bugs.md` (user-facing bug reports)
|
||||||
|
- `bugs_output` = `{output_folder}/bugs.yaml` (agent-facing structured metadata)
|
||||||
|
- `sprint_status` = `{dev_ephemeral_location}/sprint-status.yaml`
|
||||||
|
- `epics_file` = `{output_folder}/epics.md`
|
||||||
|
|
||||||
|
### Optional API Integration
|
||||||
|
|
||||||
|
- `project_url` = configurable base URL for in-app bug report sync (default: `http://localhost:5173`)
|
||||||
|
- See `reference-implementation.md` for in-app bug reporting setup
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## EXECUTION
|
||||||
|
|
||||||
|
Load and execute `steps/step-01-init.md` to begin the workflow.
|
||||||
|
|
||||||
|
**Note:** Input file discovery and initialization protocols are handled in step-01-init.md.
|
||||||
|
|
@ -0,0 +1,523 @@
|
||||||
|
# Implement Workflow (Bug Fix or Feature)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<critical>This workflow loads bug/feature context, implements the code, and updates tracking in both bugs.yaml and bugs.md</critical>
|
||||||
|
<critical>Communicate in {communication_language} with {user_name}</critical>
|
||||||
|
<critical>Auto-detects type from ID format: bug-NNN = bug fix, feature-NNN = feature implementation</critical>
|
||||||
|
|
||||||
|
<workflow>
|
||||||
|
|
||||||
|
<step n="1" goal="Get item ID from user">
|
||||||
|
<check if="item_id not provided in user input">
|
||||||
|
<ask>Which bug or feature should I implement? (e.g., bug-026 or feature-021)</ask>
|
||||||
|
</check>
|
||||||
|
<action>Extract item ID from user input</action>
|
||||||
|
<action>Detect type from ID format:</action>
|
||||||
|
<action>- "bug-NNN" -> type = "bug", action_verb = "fix", past_verb = "Fixed"</action>
|
||||||
|
<action>- "feature-NNN" -> type = "feature", action_verb = "implement", past_verb = "Implemented"</action>
|
||||||
|
<check if="ID doesn't match either format">
|
||||||
|
<output>Invalid ID format. Use bug-NNN (e.g., bug-026) or feature-NNN (e.g., feature-021)</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="2" goal="Load context from bugs.yaml">
|
||||||
|
<action>Search for {item_id} in {bugs_yaml} using grep with 50+ lines of context after the match (do NOT read entire file - it exceeds token limits)</action>
|
||||||
|
<check if="type == bug">
|
||||||
|
<action>Entry will be in bugs section, grep will capture all fields</action>
|
||||||
|
</check>
|
||||||
|
<check if="type == feature">
|
||||||
|
<action>Entry will be in feature_requests section, grep will capture all fields</action>
|
||||||
|
</check>
|
||||||
|
<check if="item not found in bugs.yaml">
|
||||||
|
<output>{item_id} not found in bugs.yaml. Please verify the ID or run bug-tracking workflow first.</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
<action>Extract and store metadata:</action>
|
||||||
|
<action>- title: Title/summary</action>
|
||||||
|
<action>- description: Full description</action>
|
||||||
|
<action>- severity/priority: Importance level</action>
|
||||||
|
<action>- complexity: trivial | small | medium | complex</action>
|
||||||
|
<action>- effort_estimate: Estimated hours</action>
|
||||||
|
<action>- affected_platform: all | ios | android (bugs only)</action>
|
||||||
|
<action>- related_story/related_epic: Related items if applicable</action>
|
||||||
|
<action>- doc_impact: Documentation impact flags (prd, architecture, ux) and notes</action>
|
||||||
|
<action>- notes: Triage notes including planned approach, files to check, implementation strategy</action>
|
||||||
|
|
||||||
|
<check if="recommended_workflow == 'backlog'">
|
||||||
|
<output>**BACKLOG ITEM - NOT READY FOR IMPLEMENTATION**
|
||||||
|
|
||||||
|
**{item_id}: {title}**
|
||||||
|
|
||||||
|
This item has `recommended_workflow: backlog` which means it's deferred and not scheduled for implementation.
|
||||||
|
|
||||||
|
**To implement this item, first promote it to the sprint:**
|
||||||
|
1. Run `*sprint-planning` and select this item for promotion
|
||||||
|
2. Or manually update bugs.yaml: change `recommended_workflow` to `direct-fix`, `tech-spec`, or `correct-course`
|
||||||
|
|
||||||
|
**Current Status:** {status}
|
||||||
|
**Priority:** {priority}
|
||||||
|
**Complexity:** {complexity}
|
||||||
|
</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="status == 'deferred'">
|
||||||
|
<output>**DEFERRED ITEM - NOT READY FOR IMPLEMENTATION**
|
||||||
|
|
||||||
|
**{item_id}: {title}**
|
||||||
|
|
||||||
|
This item is deferred (marked for future release, not current MVP).
|
||||||
|
|
||||||
|
**To implement this item:**
|
||||||
|
1. Update bugs.yaml: change `status` from `deferred` to `backlog`
|
||||||
|
2. Run `*sprint-planning` to promote to current sprint
|
||||||
|
|
||||||
|
**Notes:** {notes}
|
||||||
|
</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="status == 'blocked'">
|
||||||
|
<output>**BLOCKED ITEM - CANNOT IMPLEMENT**
|
||||||
|
|
||||||
|
**{item_id}: {title}**
|
||||||
|
|
||||||
|
This item is blocked and requires clarification before implementation.
|
||||||
|
|
||||||
|
**Blocking reason:** {notes}
|
||||||
|
|
||||||
|
**To unblock:**
|
||||||
|
1. Resolve the blocking issue
|
||||||
|
2. Update bugs.yaml: change `status` from `blocked` to `backlog`
|
||||||
|
3. Run `/triage {item_id}` to re-evaluate
|
||||||
|
</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="2.5" goal="Check for documentation impact and route to appropriate agents">
|
||||||
|
<action>Check doc_impact fields from bugs.yaml entry</action>
|
||||||
|
<check if="doc_impact.prd OR doc_impact.architecture OR doc_impact.ux is TRUE">
|
||||||
|
<output>**DOCUMENTATION IMPACT DETECTED**
|
||||||
|
|
||||||
|
**{item_id}: {title}**
|
||||||
|
|
||||||
|
This {type} requires documentation updates BEFORE implementation:
|
||||||
|
|
||||||
|
{if doc_impact.prd:}
|
||||||
|
- **PRD Impact:** Updates needed to product requirements
|
||||||
|
-> Route to PM Agent for PRD updates
|
||||||
|
{end if}
|
||||||
|
|
||||||
|
{if doc_impact.architecture:}
|
||||||
|
- **Architecture Impact:** Updates needed to architecture docs
|
||||||
|
-> Route to Architect Agent for architecture updates
|
||||||
|
{end if}
|
||||||
|
|
||||||
|
{if doc_impact.ux:}
|
||||||
|
- **UX Impact:** Updates needed to UX specifications
|
||||||
|
-> Route to UX Designer Agent for UX spec updates
|
||||||
|
{end if}
|
||||||
|
|
||||||
|
**Details:** {doc_impact.notes}
|
||||||
|
|
||||||
|
**Options:**
|
||||||
|
1. **update-docs-first** - Route to agents for documentation updates before implementation (recommended)
|
||||||
|
2. **proceed-anyway** - Skip documentation updates and implement directly (not recommended)
|
||||||
|
3. **cancel** - Return to review</output>
|
||||||
|
<ask>How should we proceed?</ask>
|
||||||
|
|
||||||
|
<check if="user chooses update-docs-first">
|
||||||
|
<output>Routing to documentation update workflow...
|
||||||
|
|
||||||
|
**Documentation Update Sequence:**</output>
|
||||||
|
|
||||||
|
<check if="doc_impact.prd">
|
||||||
|
<output>1. **PRD Update** - Invoking PM Agent...</output>
|
||||||
|
<action>Prepare PRD update context:</action>
|
||||||
|
<action>- Source item: {item_id}</action>
|
||||||
|
<action>- Change description: {description}</action>
|
||||||
|
<action>- Specific PRD sections: {doc_impact.notes PRD sections}</action>
|
||||||
|
<invoke-agent agent="pm">
|
||||||
|
<task>Review and update PRD for {item_id}: {title}
|
||||||
|
|
||||||
|
Change context: {description}
|
||||||
|
|
||||||
|
Documentation notes: {doc_impact.notes}
|
||||||
|
|
||||||
|
Please update the relevant PRD sections to reflect this change.
|
||||||
|
|
||||||
|
After updates:
|
||||||
|
1. Summarize what was changed
|
||||||
|
2. Return to the implement workflow by running: /implement {item_id}
|
||||||
|
|
||||||
|
IMPORTANT: You MUST return to /implement {item_id} after completing the PRD updates so the actual code implementation can proceed.</task>
|
||||||
|
</invoke-agent>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="doc_impact.architecture">
|
||||||
|
<output>2. **Architecture Update** - Invoking Architect Agent...</output>
|
||||||
|
<action>Prepare architecture update context:</action>
|
||||||
|
<action>- Source item: {item_id}</action>
|
||||||
|
<action>- Change description: {description}</action>
|
||||||
|
<action>- Specific architecture sections: {doc_impact.notes architecture sections}</action>
|
||||||
|
<invoke-agent agent="architect">
|
||||||
|
<task>Review and update Architecture documentation for {item_id}: {title}
|
||||||
|
|
||||||
|
Change context: {description}
|
||||||
|
|
||||||
|
Documentation notes: {doc_impact.notes}
|
||||||
|
|
||||||
|
Please update the relevant architecture sections (data model, APIs, security, etc.) to reflect this change.
|
||||||
|
|
||||||
|
After updates:
|
||||||
|
1. Summarize what was changed
|
||||||
|
2. Return to the implement workflow by running: /implement {item_id}
|
||||||
|
|
||||||
|
IMPORTANT: You MUST return to /implement {item_id} after completing the architecture updates so the actual code implementation can proceed.</task>
|
||||||
|
</invoke-agent>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="doc_impact.ux">
|
||||||
|
<output>3. **UX Spec Update** - Invoking UX Designer Agent...</output>
|
||||||
|
<action>Prepare UX update context:</action>
|
||||||
|
<action>- Source item: {item_id}</action>
|
||||||
|
<action>- Change description: {description}</action>
|
||||||
|
<action>- Specific UX sections: {doc_impact.notes UX sections}</action>
|
||||||
|
<invoke-agent agent="ux-designer">
|
||||||
|
<task>Review and update UX specification for {item_id}: {title}
|
||||||
|
|
||||||
|
Change context: {description}
|
||||||
|
|
||||||
|
Documentation notes: {doc_impact.notes}
|
||||||
|
|
||||||
|
Please update the relevant UX spec sections (screens, flows, components, etc.) to reflect this change.
|
||||||
|
|
||||||
|
After updates:
|
||||||
|
1. Summarize what was changed
|
||||||
|
2. Return to the implement workflow by running: /implement {item_id}
|
||||||
|
|
||||||
|
IMPORTANT: You MUST return to /implement {item_id} after completing the UX updates so the actual code implementation can proceed.</task>
|
||||||
|
</invoke-agent>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<output>**Documentation updates complete.**
|
||||||
|
|
||||||
|
Proceeding with implementation...</output>
|
||||||
|
<action>Continue to step 3</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="user chooses cancel">
|
||||||
|
<output>Cancelled. {item_id} remains in current state.</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action if="user chooses proceed-anyway">
|
||||||
|
<output>Proceeding without documentation updates. Remember to update docs after implementation.</output>
|
||||||
|
<action>Continue to step 3</action>
|
||||||
|
</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="3" goal="Evaluate routing and auto-route to correct-course if needed">
|
||||||
|
<action>Check recommended_workflow field from bugs.yaml</action>
|
||||||
|
|
||||||
|
<check if="recommended_workflow == 'correct-course'">
|
||||||
|
<output>**AUTO-ROUTING TO CORRECT-COURSE**
|
||||||
|
|
||||||
|
**{item_id}: {title}**
|
||||||
|
**Priority:** {severity_or_priority} | **Complexity:** {complexity}
|
||||||
|
|
||||||
|
This {type} has `recommended_workflow: correct-course` which requires impact analysis and story creation before implementation.
|
||||||
|
|
||||||
|
Invoking correct-course workflow via SM agent...</output>
|
||||||
|
|
||||||
|
<action>Invoke the correct-course workflow skill with item context</action>
|
||||||
|
<invoke-skill skill="bmad:bmm:workflows:correct-course">
|
||||||
|
<args>{item_id}: {title} - {description}
|
||||||
|
|
||||||
|
Priority: {severity_or_priority}
|
||||||
|
Complexity: {complexity}
|
||||||
|
Doc Impact: {doc_impact summary}
|
||||||
|
Notes: {notes}</args>
|
||||||
|
</invoke-skill>
|
||||||
|
<action>HALT - Correct Course workflow will handle story/epic creation</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="recommended_workflow == 'tech-spec'">
|
||||||
|
<output>**AUTO-ROUTING TO TECH-SPEC**
|
||||||
|
|
||||||
|
**{item_id}: {title}**
|
||||||
|
|
||||||
|
This {type} has `recommended_workflow: tech-spec`. Invoking tech-spec workflow...</output>
|
||||||
|
|
||||||
|
<invoke-skill skill="bmad:bmm:workflows:tech-spec">
|
||||||
|
<args>{item_id}: {title} - {description}</args>
|
||||||
|
</invoke-skill>
|
||||||
|
<action>HALT - Tech-spec workflow will create implementation spec</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="recommended_workflow == 'direct-fix'">
|
||||||
|
<output>**DIRECT IMPLEMENTATION**
|
||||||
|
|
||||||
|
This {type} is routed for direct implementation. Proceeding...</output>
|
||||||
|
<action>Continue to step 4</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="recommended_workflow is not set OR recommended_workflow is ambiguous">
|
||||||
|
<action>Evaluate the workflow routing matrix based on severity and complexity:</action>
|
||||||
|
<action>**Routing Matrix:**</action>
|
||||||
|
<action>- critical + any -> correct-course</action>
|
||||||
|
<action>- high/medium + medium/complex -> correct-course</action>
|
||||||
|
<action>- high + trivial -> direct-fix</action>
|
||||||
|
<action>- high/medium + small -> tech-spec</action>
|
||||||
|
<action>- medium + trivial -> direct-fix</action>
|
||||||
|
<action>- low + trivial -> direct-fix</action>
|
||||||
|
<action>- low + small+ -> backlog</action>
|
||||||
|
|
||||||
|
<action>Apply matrix to determine routing and continue accordingly</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="4" goal="Present context and confirm approach">
|
||||||
|
<output>**{item_id}: {title}**
|
||||||
|
|
||||||
|
**Type:** {type} | **Severity/Priority:** {severity_or_priority} | **Complexity:** {complexity} | **Effort:** ~{effort_estimate}h
|
||||||
|
|
||||||
|
**Description:**
|
||||||
|
{description}
|
||||||
|
|
||||||
|
**Planned Approach (from triage notes):**
|
||||||
|
{notes}
|
||||||
|
|
||||||
|
**Related:** {related_story} / {related_epic}
|
||||||
|
</output>
|
||||||
|
<ask>Ready to {action_verb} this {type}? (yes/no/clarify)</ask>
|
||||||
|
<check if="user says clarify">
|
||||||
|
<ask>What additional context do you need?</ask>
|
||||||
|
<action>Gather clarification, update mental model</action>
|
||||||
|
</check>
|
||||||
|
<check if="user says no">
|
||||||
|
<output>Cancelled. {item_id} remains in current state.</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="5" goal="Implement the fix/feature">
|
||||||
|
<action>Based on the notes/planned approach, identify files to modify or create</action>
|
||||||
|
<action>Read each affected file to understand current implementation</action>
|
||||||
|
<action>Implement following the planned approach:</action>
|
||||||
|
<action>- Make minimal, targeted changes</action>
|
||||||
|
<action>- Follow existing code patterns and style</action>
|
||||||
|
<action>- Add comments only where logic is non-obvious</action>
|
||||||
|
<action>- Do not over-engineer or add unrelated improvements</action>
|
||||||
|
<action>- Do not add extra features or "nice to haves"</action>
|
||||||
|
|
||||||
|
<action>For each file modified/created, track:</action>
|
||||||
|
<action>- File path</action>
|
||||||
|
<action>- What was changed/added</action>
|
||||||
|
<action>- How it addresses the bug/feature</action>
|
||||||
|
|
||||||
|
<check if="requires new files">
|
||||||
|
<action>Create new files following project conventions</action>
|
||||||
|
<action>Add appropriate imports/exports</action>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<check if="planned approach is unclear or insufficient">
|
||||||
|
<ask>The triage notes don't provide a clear approach.
|
||||||
|
|
||||||
|
Based on my analysis, I suggest: {proposed_approach}
|
||||||
|
|
||||||
|
Should I proceed with this approach?</ask>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="6" goal="Verify implementation compiles">
|
||||||
|
<action>Run TypeScript compilation check: npm run check</action>
|
||||||
|
<check if="compilation errors in modified files">
|
||||||
|
<action>Fix compilation errors</action>
|
||||||
|
<action>Re-run compilation check</action>
|
||||||
|
</check>
|
||||||
|
<output>Compilation check passed.</output>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="6.5" goal="Pre-update sync check">
|
||||||
|
<action>Search for {item_id} in both bugs.yaml and bugs.md using grep to check current status</action>
|
||||||
|
<check if="status differs between files OR item missing from one file">
|
||||||
|
<output>SYNC WARNING: {item_id} status mismatch detected
|
||||||
|
- bugs.yaml: {yaml_status}
|
||||||
|
- bugs.md: {md_status}
|
||||||
|
Proceeding will update both files to "{new_status}".</output>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="7" goal="Update bugs.yaml">
|
||||||
|
<action>Update entry in bugs.yaml:</action>
|
||||||
|
<check if="type == bug">
|
||||||
|
<action>- status: "fixed"</action>
|
||||||
|
<action>- fixed_date: {date} (YYYY-MM-DD format)</action>
|
||||||
|
</check>
|
||||||
|
<check if="type == feature">
|
||||||
|
<action>- status: "implemented"</action>
|
||||||
|
<action>- implemented_date: {date} (YYYY-MM-DD format)</action>
|
||||||
|
</check>
|
||||||
|
<action>- assigned_to: "dev-agent"</action>
|
||||||
|
<action>- files_modified: {list of files changed/created during implementation}</action>
|
||||||
|
<action>- Append to notes: "{past_verb} ({date}): {summary of changes made}"</action>
|
||||||
|
<action>Write updated bugs.yaml</action>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="8" goal="Update bugs.md">
|
||||||
|
<action>Search for {item_id} in {bugs_md} using grep with surrounding context to locate the entry</action>
|
||||||
|
|
||||||
|
<action>**8a. Remove from tracked section (if present)**</action>
|
||||||
|
<check if="type == bug">
|
||||||
|
<action>Search for "{item_id}:" in "# Tracked Bugs" section</action>
|
||||||
|
</check>
|
||||||
|
<check if="type == feature">
|
||||||
|
<action>Search for "{item_id}:" in "# Tracked Feature Requests" section</action>
|
||||||
|
</check>
|
||||||
|
<action>If found, remove the entire entry (including any indented sub-items)</action>
|
||||||
|
|
||||||
|
<action>**8b. Add to completed section (INSERT AT TOP - newest first)**</action>
|
||||||
|
<check if="type == bug">
|
||||||
|
<action>Locate "# Fixed Bugs" section in bugs.md</action>
|
||||||
|
<action>If section not found, create it</action>
|
||||||
|
<action>INSERT AT TOP of section (immediately after "# Fixed Bugs" header):</action>
|
||||||
|
<action>[IMPLEMENTED] {item_id}: {title} - {brief_description}. [Severity: {severity}, Platform: {platform}, Fixed: {date}, Verified: pending]</action>
|
||||||
|
<action> - Fix: {description of what was fixed}</action>
|
||||||
|
<action> - File(s): {list of modified files}</action>
|
||||||
|
</check>
|
||||||
|
<check if="type == feature">
|
||||||
|
<action>Locate "# Implemented Features" section in bugs.md</action>
|
||||||
|
<action>If section not found, create it before "# Fixed Bugs"</action>
|
||||||
|
<action>INSERT AT TOP of section (immediately after "# Implemented Features" header):</action>
|
||||||
|
<action>[IMPLEMENTED] {item_id}: {title} - {brief_description}. [Implemented: {date}, Platform: {platform}, Verified: pending]</action>
|
||||||
|
<action> - Files: {list of modified/created files}</action>
|
||||||
|
<action> - Features: {bullet list of what was implemented}</action>
|
||||||
|
</check>
|
||||||
|
<action>Write updated bugs.md</action>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="9" goal="Post-update validation">
|
||||||
|
<action>Search for {item_id} in both bugs.yaml and bugs.md using grep to validate updates</action>
|
||||||
|
<action>Confirm {item_id} shows status "fixed"/"implemented" in bugs.yaml</action>
|
||||||
|
<action>Confirm {item_id} has [IMPLEMENTED] tag in bugs.md</action>
|
||||||
|
<check if="validation fails">
|
||||||
|
<output>SYNC ERROR: Files may be out of sync. Please verify manually:
|
||||||
|
- bugs.yaml: Expected status "fixed"/"implemented"
|
||||||
|
- bugs.md: Expected [IMPLEMENTED] tag in appropriate section</output>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="10" goal="Present completion summary">
|
||||||
|
<output>**{item_id} {past_verb.upper()}**
|
||||||
|
|
||||||
|
**Changes Made:**
|
||||||
|
{for each modified file:}
|
||||||
|
- {file_path}: {what was changed}
|
||||||
|
{end for}
|
||||||
|
|
||||||
|
**Updated Tracking:**
|
||||||
|
- bugs.yaml: status -> "{status}", {date_field} -> {date}, files_modified updated
|
||||||
|
- bugs.md: Moved to "{target_section}" with [IMPLEMENTED] tag
|
||||||
|
|
||||||
|
**Verification Status:** pending
|
||||||
|
|
||||||
|
**Next Steps:**
|
||||||
|
1. Test manually
|
||||||
|
2. Run `/verify {item_id}` after verification to close
|
||||||
|
</output>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
</workflow>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/implement bug-026
|
||||||
|
/implement feature-021
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
1. **Auto-detect Type** - ID format determines bug vs feature handling
|
||||||
|
2. **Context First** - Always read and present details before implementing
|
||||||
|
3. **Confirm Approach** - Validate planned approach with user before coding
|
||||||
|
4. **Minimal Changes** - Only implement what's needed, no scope creep
|
||||||
|
5. **Dual Tracking** - ALWAYS update both bugs.yaml AND bugs.md
|
||||||
|
6. **[IMPLEMENTED] Tag** - Indicates complete but awaiting verification
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reference: Bug Tracking Definitions
|
||||||
|
|
||||||
|
### Severity Levels
|
||||||
|
|
||||||
|
| Level | Description | Action |
|
||||||
|
|-------|-------------|--------|
|
||||||
|
| **critical** | Blocks core functionality, prevents app use, or causes data loss (crashes, auth broken, data corruption) | Fix immediately, may require hotfix |
|
||||||
|
| **high** | Major feature broken, significant UX degradation, workaround exists but painful (platform-specific failure, 5+ sec delays, accessibility blocker) | Fix in current/next sprint |
|
||||||
|
| **medium** | Feature partially broken, UX degraded but usable (minor feature broken, unclear errors, 1-3 sec delays) | Fix when capacity allows |
|
||||||
|
| **low** | Minor issue, cosmetic, edge case (typos, spacing, visual glitches) | Fix opportunistically or defer |
|
||||||
|
|
||||||
|
### Complexity Levels
|
||||||
|
|
||||||
|
| Level | Description | Effort |
|
||||||
|
|-------|-------------|--------|
|
||||||
|
| **trivial** | Obvious fix, single line change, no investigation needed (typo, missing semicolon, wrong color) | < 30 minutes |
|
||||||
|
| **small** | Single file/component, clear root cause, solution known (missing validation, incorrect prop, logic error) | 30 min - 2 hours |
|
||||||
|
| **medium** | Multiple files affected OR investigation required (spans 2-3 components, debugging needed, integration issue) | 2-8 hours |
|
||||||
|
| **complex** | Architectural issue, affects multiple stories, requires design changes (race conditions, refactoring, profiling) | 8+ hours (1-2 days) |
|
||||||
|
|
||||||
|
### Workflow Routing Matrix
|
||||||
|
|
||||||
|
| Severity | Complexity | Workflow | Rationale |
|
||||||
|
|----------|------------|----------|-----------|
|
||||||
|
| critical | any | correct-course -> urgent | Need impact analysis even if small fix |
|
||||||
|
| high | trivial | direct-fix (urgent) | Fast path for obvious important fix |
|
||||||
|
| high | small | tech-spec (urgent) | Fast path with minimal overhead |
|
||||||
|
| high | medium+ | correct-course -> story | Need proper analysis + testing |
|
||||||
|
| medium | trivial | direct-fix | Too small for workflow overhead |
|
||||||
|
| medium | small | tech-spec | Isolated fix needs spec |
|
||||||
|
| medium | medium+ | correct-course -> story | Multi-file change needs story |
|
||||||
|
| low | trivial | direct-fix (defer) | Fix opportunistically |
|
||||||
|
| low | small+ | backlog (defer) | Document but don't schedule yet |
|
||||||
|
|
||||||
|
### Status Flow
|
||||||
|
|
||||||
|
```
|
||||||
|
reported -> triaged -> routed -> in-progress -> fixed -> verified -> closed
|
||||||
|
```
|
||||||
|
|
||||||
|
| Status | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| **reported** | Bug logged in bugs.md, not yet analyzed |
|
||||||
|
| **triaged** | PM analyzed, assigned severity/complexity/workflow |
|
||||||
|
| **routed** | Workflow determined, story/tech-spec created |
|
||||||
|
| **in-progress** | Developer actively working on fix |
|
||||||
|
| **fixed** | Code changed, tests passing, ready for verification |
|
||||||
|
| **verified** | Bug confirmed fixed by reporter or QA |
|
||||||
|
| **closed** | Bug resolved and verified, no further action |
|
||||||
|
|
||||||
|
### Metadata Fields
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| id | Unique identifier (bug-NNN or feature-NNN) |
|
||||||
|
| title | Short description (< 80 chars) |
|
||||||
|
| description | Detailed explanation |
|
||||||
|
| severity | critical \| high \| medium \| low |
|
||||||
|
| complexity | trivial \| small \| medium \| complex |
|
||||||
|
| status | Current workflow state |
|
||||||
|
| recommended_workflow | direct-fix \| tech-spec \| correct-course \| backlog |
|
||||||
|
| effort_estimate | Hours (based on complexity) |
|
||||||
|
| reported_by / reported_date | Who found it and when |
|
||||||
|
| triaged_by / triaged_date | Who triaged and when |
|
||||||
|
| fixed_date / verified_date | Implementation and verification dates |
|
||||||
|
| related_story / related_epic | Context links |
|
||||||
|
| affected_platform | all \| ios \| android |
|
||||||
|
| doc_impact | Documentation impact: prd, architecture, ux flags + notes |
|
||||||
|
| notes | Investigation notes, decisions, implementation details |
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
name: implement
|
||||||
|
description: "Implement a bug fix or feature - loads context from bugs.yaml, implements the code, updates both bugs.yaml and bugs.md with [IMPLEMENTED] tag"
|
||||||
|
author: "BMad"
|
||||||
|
|
||||||
|
# Critical variables from config
|
||||||
|
config_source: "{project-root}/.bmad/bmm/config.yaml"
|
||||||
|
output_folder: "{config_source}:output_folder"
|
||||||
|
user_name: "{config_source}:user_name"
|
||||||
|
communication_language: "{config_source}:communication_language"
|
||||||
|
date: system-generated
|
||||||
|
|
||||||
|
# Workflow components
|
||||||
|
installed_path: "{project-root}/.bmad/bmm/workflows/implement"
|
||||||
|
instructions: "{installed_path}/instructions.md"
|
||||||
|
template: false
|
||||||
|
|
||||||
|
# Input and output files
|
||||||
|
variables:
|
||||||
|
bugs_md: "{output_folder}/bugs.md"
|
||||||
|
bugs_yaml: "{output_folder}/bugs.yaml"
|
||||||
|
|
||||||
|
standalone: true
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
# Verify Workflow (Close Implemented Bugs/Features)
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<critical>This workflow verifies implemented items and closes them in both bugs.yaml and bugs.md</critical>
|
||||||
|
<critical>Communicate in {communication_language} with {user_name}</critical>
|
||||||
|
<critical>Removes [IMPLEMENTED] tag and updates status to CLOSED (bugs) or COMPLETE (features)</critical>
|
||||||
|
|
||||||
|
<workflow>
|
||||||
|
|
||||||
|
<step n="1" goal="Get item ID or list pending items">
|
||||||
|
<check if="item_id provided in user input">
|
||||||
|
<action>Extract item ID from user input</action>
|
||||||
|
<action>Detect type from ID format:</action>
|
||||||
|
<action>- "bug-NNN" -> type = "bug", final_status = "CLOSED"</action>
|
||||||
|
<action>- "feature-NNN" -> type = "feature", final_status = "COMPLETE"</action>
|
||||||
|
<action>Proceed to Step 2</action>
|
||||||
|
</check>
|
||||||
|
<check if="no item_id provided OR user says 'list'">
|
||||||
|
<action>Search {bugs_yaml} for 'status: "fixed"' or 'status: "implemented"' using grep (do NOT read entire file)</action>
|
||||||
|
<action>Search {bugs_md} for '[IMPLEMENTED]' entries using grep</action>
|
||||||
|
<action>Find all items with:</action>
|
||||||
|
<action>- status == "fixed" or "implemented" in bugs.yaml</action>
|
||||||
|
<action>- [IMPLEMENTED] tag in bugs.md</action>
|
||||||
|
<output>**Pending Verification:**
|
||||||
|
|
||||||
|
{for each pending item:}
|
||||||
|
- **{item_id}**: {title} [{type}, {fixed_date or implemented_date}]
|
||||||
|
{end for}
|
||||||
|
|
||||||
|
**Total:** {count} item(s) awaiting verification
|
||||||
|
|
||||||
|
To verify an item: `/verify bug-026`
|
||||||
|
To verify all: Type "verify all"
|
||||||
|
</output>
|
||||||
|
<ask>Which item would you like to verify?</ask>
|
||||||
|
</check>
|
||||||
|
<check if="user says 'verify all' or 'all'">
|
||||||
|
<action>Set batch_mode = true</action>
|
||||||
|
<action>Collect all pending items</action>
|
||||||
|
<action>Proceed to Step 2 with batch processing</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="2" goal="Load item context and check sync">
|
||||||
|
<action>Search for {item_id} in {bugs_yaml} using grep with 50+ lines of context after the match (do NOT read entire file - it exceeds token limits)</action>
|
||||||
|
<check if="type == bug">
|
||||||
|
<action>Entry will be in bugs section, verify status == "fixed"</action>
|
||||||
|
</check>
|
||||||
|
<check if="type == feature">
|
||||||
|
<action>Entry will be in feature_requests section, verify status == "implemented"</action>
|
||||||
|
</check>
|
||||||
|
<check if="item not found OR status not fixed/implemented">
|
||||||
|
<output>{item_id} is not in an implemented state. Current status: {status}
|
||||||
|
|
||||||
|
Only items with status "fixed" (bugs) or "implemented" (features) can be verified.</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
<action>Extract metadata: title, description, fixed_date/implemented_date, notes</action>
|
||||||
|
|
||||||
|
<action>**Sync Check:** Also read {bugs_md} to verify sync status</action>
|
||||||
|
<check if="bugs.yaml says fixed/implemented but bugs.md missing [IMPLEMENTED] tag">
|
||||||
|
<output>SYNC WARNING: {item_id} status mismatch detected
|
||||||
|
- bugs.yaml: {yaml_status}
|
||||||
|
- bugs.md: Missing [IMPLEMENTED] tag (may have been implemented outside workflow)
|
||||||
|
|
||||||
|
Proceeding will update both files to CLOSED/COMPLETE.</output>
|
||||||
|
<ask>Continue with verification? (yes/no)</ask>
|
||||||
|
<check if="user says no">
|
||||||
|
<output>Cancelled. Please run /implement {item_id} first to sync files.</output>
|
||||||
|
<action>HALT</action>
|
||||||
|
</check>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="3" goal="Confirm verification">
|
||||||
|
<output>**Verify {item_id}: {title}**
|
||||||
|
|
||||||
|
**Type:** {type}
|
||||||
|
**{past_verb}:** {fixed_date or implemented_date}
|
||||||
|
|
||||||
|
**Implementation Notes:**
|
||||||
|
{notes - show the FIXED/IMPLEMENTED section}
|
||||||
|
|
||||||
|
**Files Changed:**
|
||||||
|
{extract file list from notes}
|
||||||
|
</output>
|
||||||
|
<ask>Has this been tested and verified working? (yes/no/skip)</ask>
|
||||||
|
<check if="user says no">
|
||||||
|
<ask>What issue did you find? (I'll add it to the notes)</ask>
|
||||||
|
<action>Append verification failure note to bugs.yaml notes field</action>
|
||||||
|
<output>Noted. {item_id} remains in implemented state for rework.</output>
|
||||||
|
<action>HALT or continue to next item in batch</action>
|
||||||
|
</check>
|
||||||
|
<check if="user says skip">
|
||||||
|
<output>Skipped. {item_id} remains in implemented state.</output>
|
||||||
|
<action>Continue to next item in batch or HALT</action>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="4" goal="Update bugs.yaml">
|
||||||
|
<action>Update entry in bugs.yaml:</action>
|
||||||
|
<action>- status: "closed"</action>
|
||||||
|
<action>- verified_by: {user_name}</action>
|
||||||
|
<action>- verified_date: {date} (YYYY-MM-DD format)</action>
|
||||||
|
<action>- Append to notes: "Verified ({date}) by {user_name}"</action>
|
||||||
|
<action>Write updated bugs.yaml</action>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="5" goal="Update bugs.md">
|
||||||
|
<action>Search for {item_id} in {bugs_md} using grep with surrounding context to locate the entry</action>
|
||||||
|
|
||||||
|
<action>**5a. Find the entry**</action>
|
||||||
|
<check if="type == bug">
|
||||||
|
<action>Search for "[IMPLEMENTED] {item_id}:" in "# Fixed Bugs" section</action>
|
||||||
|
<check if="not found">
|
||||||
|
<action>Search for "{item_id}:" in "# Tracked Bugs" section (implemented outside workflow)</action>
|
||||||
|
</check>
|
||||||
|
</check>
|
||||||
|
<check if="type == feature">
|
||||||
|
<action>Search for "[IMPLEMENTED] {item_id}:" in "# Implemented Features" section</action>
|
||||||
|
<check if="not found">
|
||||||
|
<action>Search for "{item_id}:" in "# Tracked Feature Requests" section (implemented outside workflow)</action>
|
||||||
|
</check>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>**5b. Move entry if in wrong section**</action>
|
||||||
|
<check if="entry found in Tracked section (implemented outside workflow)">
|
||||||
|
<action>DELETE the entry from "# Tracked Bugs" or "# Tracked Feature Requests"</action>
|
||||||
|
<action>ADD entry to correct section:</action>
|
||||||
|
<check if="type == bug">
|
||||||
|
<action>Add to "# Fixed Bugs" section</action>
|
||||||
|
</check>
|
||||||
|
<check if="type == feature">
|
||||||
|
<action>Add to "# Implemented Features" section (at top, before other entries)</action>
|
||||||
|
</check>
|
||||||
|
</check>
|
||||||
|
|
||||||
|
<action>**5c. Update the entry format**</action>
|
||||||
|
<action>Remove "[IMPLEMENTED] " prefix if present</action>
|
||||||
|
<action>Update the status tag in brackets:</action>
|
||||||
|
<check if="type == bug">
|
||||||
|
<action>Change from "[Severity: X, Fixed: DATE, Verified: pending]" or "[Severity: X, Complexity: Y, Workflow: Z]"</action>
|
||||||
|
<action>To "[Severity: X, Platform: Y, Fixed: {date}, Verified: {date}, CLOSED]"</action>
|
||||||
|
</check>
|
||||||
|
<check if="type == feature">
|
||||||
|
<action>Change from "[Implemented: DATE, Verified: pending]" or "[Priority: X, Complexity: Y, Workflow: Z]"</action>
|
||||||
|
<action>To "[Implemented: {date}, Platform: Y, Verified: {date}, COMPLETE]"</action>
|
||||||
|
</check>
|
||||||
|
<action>Add implementation notes if available from bugs.yaml</action>
|
||||||
|
|
||||||
|
<action>Write updated bugs.md</action>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="5.5" goal="Post-update validation">
|
||||||
|
<action>Search for {item_id} in both bugs.yaml and bugs.md using grep to validate updates</action>
|
||||||
|
<action>Confirm bugs.yaml: status="closed", verified_by set, verified_date set</action>
|
||||||
|
<action>Confirm bugs.md: No [IMPLEMENTED] tag, has CLOSED/COMPLETE in status tag</action>
|
||||||
|
<check if="validation fails">
|
||||||
|
<output>SYNC ERROR: Verification may be incomplete. Please check both files:
|
||||||
|
- bugs.yaml: Expected status "closed", verified_by/verified_date set
|
||||||
|
- bugs.md: Expected CLOSED/COMPLETE tag, no [IMPLEMENTED] prefix</output>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
<step n="6" goal="Present completion summary">
|
||||||
|
<check if="batch_mode">
|
||||||
|
<output>**Verification Complete**
|
||||||
|
|
||||||
|
**Verified {verified_count} item(s):**
|
||||||
|
{for each verified item:}
|
||||||
|
- {item_id}: {title} -> {final_status}
|
||||||
|
{end for}
|
||||||
|
|
||||||
|
**Skipped:** {skipped_count}
|
||||||
|
**Failed verification:** {failed_count}
|
||||||
|
|
||||||
|
**Updated Files:**
|
||||||
|
- bugs.yaml: status -> "closed", verified_by/verified_date set
|
||||||
|
- bugs.md: [IMPLEMENTED] tag removed, status -> {final_status}
|
||||||
|
</output>
|
||||||
|
</check>
|
||||||
|
<check if="not batch_mode">
|
||||||
|
<output>**{item_id} VERIFIED and {final_status}**
|
||||||
|
|
||||||
|
**Updated:**
|
||||||
|
- bugs.yaml: status -> "closed", verified_by -> {user_name}, verified_date -> {date}
|
||||||
|
- bugs.md: Removed [IMPLEMENTED] tag, added "Verified: {date}, {final_status}"
|
||||||
|
|
||||||
|
This item is now fully closed.
|
||||||
|
</output>
|
||||||
|
</check>
|
||||||
|
</step>
|
||||||
|
|
||||||
|
</workflow>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/verify # List all pending verification
|
||||||
|
/verify bug-026 # Verify specific bug
|
||||||
|
/verify feature-021 # Verify specific feature
|
||||||
|
/verify all # Verify all pending items
|
||||||
|
```
|
||||||
|
|
||||||
|
## Status Transitions
|
||||||
|
|
||||||
|
| Type | Before | After |
|
||||||
|
|------|--------|-------|
|
||||||
|
| Bug | status: "fixed", [IMPLEMENTED] | status: "closed", CLOSED |
|
||||||
|
| Feature | status: "implemented", [IMPLEMENTED] | status: "closed", COMPLETE |
|
||||||
|
|
||||||
|
## Key Principles
|
||||||
|
|
||||||
|
1. **Verification Gate** - User must confirm item was tested and works
|
||||||
|
2. **Failure Handling** - If verification fails, add note and keep in implemented state
|
||||||
|
3. **Batch Support** - Can verify multiple items at once
|
||||||
|
4. **Dual Tracking** - ALWAYS update both bugs.yaml AND bugs.md
|
||||||
|
5. **Proper Closure** - Removes [IMPLEMENTED] tag, adds final CLOSED/COMPLETE status
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
name: verify
|
||||||
|
description: "Verify and close implemented bugs/features - removes [IMPLEMENTED] tag, updates status to CLOSED/COMPLETE in both bugs.yaml and bugs.md"
|
||||||
|
author: "BMad"
|
||||||
|
|
||||||
|
# Critical variables from config
|
||||||
|
config_source: "{project-root}/.bmad/bmm/config.yaml"
|
||||||
|
output_folder: "{config_source}:output_folder"
|
||||||
|
user_name: "{config_source}:user_name"
|
||||||
|
communication_language: "{config_source}:communication_language"
|
||||||
|
date: system-generated
|
||||||
|
|
||||||
|
# Workflow components
|
||||||
|
installed_path: "{project-root}/.bmad/bmm/workflows/verify"
|
||||||
|
instructions: "{installed_path}/instructions.md"
|
||||||
|
template: false
|
||||||
|
|
||||||
|
# Input and output files
|
||||||
|
variables:
|
||||||
|
bugs_md: "{output_folder}/bugs.md"
|
||||||
|
bugs_yaml: "{output_folder}/bugs.yaml"
|
||||||
|
|
||||||
|
standalone: true
|
||||||
|
|
@ -3,7 +3,7 @@ const path = require('node:path');
|
||||||
const fs = require('node:fs');
|
const fs = require('node:fs');
|
||||||
|
|
||||||
// Fix for stdin issues when running through npm on Windows
|
// Fix for stdin issues when running through npm on Windows
|
||||||
// Ensures keyboard interaction works properly with inquirer prompts
|
// Ensures keyboard interaction works properly with CLI prompts
|
||||||
if (process.stdin.isTTY) {
|
if (process.stdin.isTTY) {
|
||||||
try {
|
try {
|
||||||
process.stdin.resume();
|
process.stdin.resume();
|
||||||
|
|
|
||||||
|
|
@ -71,14 +71,10 @@ module.exports = {
|
||||||
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
|
console.log(chalk.dim(' • ElevenLabs AI (150+ premium voices)'));
|
||||||
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
|
console.log(chalk.dim(' • Piper TTS (50+ free voices)\n'));
|
||||||
|
|
||||||
const { default: inquirer } = await import('inquirer');
|
const prompts = require('../lib/prompts');
|
||||||
await inquirer.prompt([
|
await prompts.text({
|
||||||
{
|
message: chalk.green('Press Enter to start AgentVibes installer...'),
|
||||||
type: 'input',
|
});
|
||||||
name: 'continue',
|
|
||||||
message: chalk.green('Press Enter to start AgentVibes installer...'),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,15 +4,7 @@ const yaml = require('yaml');
|
||||||
const chalk = require('chalk');
|
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');
|
||||||
// Lazy-load inquirer (ESM module) to avoid ERR_REQUIRE_ESM
|
|
||||||
let _inquirer = null;
|
|
||||||
async function getInquirer() {
|
|
||||||
if (!_inquirer) {
|
|
||||||
_inquirer = (await import('inquirer')).default;
|
|
||||||
}
|
|
||||||
return _inquirer;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ConfigCollector {
|
class ConfigCollector {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|
@ -183,7 +175,6 @@ class ConfigCollector {
|
||||||
* @returns {boolean} True if new fields were prompted, false if all fields existed
|
* @returns {boolean} True if new fields were prompted, false if all fields existed
|
||||||
*/
|
*/
|
||||||
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
async collectModuleConfigQuick(moduleName, projectDir, silentMode = true) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
this.currentProjectDir = projectDir;
|
this.currentProjectDir = projectDir;
|
||||||
|
|
||||||
// Load existing config if not already loaded
|
// Load existing config if not already loaded
|
||||||
|
|
@ -359,7 +350,7 @@ class ConfigCollector {
|
||||||
// Only show header if we actually have questions
|
// Only show header if we actually have questions
|
||||||
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
|
CLIUtils.displayModuleConfigHeader(moduleName, moduleConfig.header, moduleConfig.subheader);
|
||||||
console.log(); // Line break before questions
|
console.log(); // Line break before questions
|
||||||
const promptedAnswers = await inquirer.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);
|
||||||
|
|
@ -502,7 +493,6 @@ class ConfigCollector {
|
||||||
* @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
|
* @param {boolean} skipCompletion - Skip showing completion message (for early core collection)
|
||||||
*/
|
*/
|
||||||
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
|
async collectModuleConfig(moduleName, projectDir, skipLoadExisting = false, skipCompletion = false) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
this.currentProjectDir = projectDir;
|
this.currentProjectDir = projectDir;
|
||||||
// Load existing config if needed and not already loaded
|
// Load existing config if needed and not already loaded
|
||||||
if (!skipLoadExisting && !this.existingConfig) {
|
if (!skipLoadExisting && !this.existingConfig) {
|
||||||
|
|
@ -597,7 +587,7 @@ class ConfigCollector {
|
||||||
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
||||||
let customize = true;
|
let customize = true;
|
||||||
if (moduleName !== 'core') {
|
if (moduleName !== 'core') {
|
||||||
const customizeAnswer = await inquirer.prompt([
|
const customizeAnswer = await prompts.prompt([
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'customize',
|
name: 'customize',
|
||||||
|
|
@ -614,7 +604,7 @@ class ConfigCollector {
|
||||||
|
|
||||||
if (questionsWithoutDefaults.length > 0) {
|
if (questionsWithoutDefaults.length > 0) {
|
||||||
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
|
console.log(chalk.dim(`\n Asking required questions for ${moduleName.toUpperCase()}...`));
|
||||||
const promptedAnswers = await inquirer.prompt(questionsWithoutDefaults);
|
const promptedAnswers = await prompts.prompt(questionsWithoutDefaults);
|
||||||
Object.assign(allAnswers, promptedAnswers);
|
Object.assign(allAnswers, promptedAnswers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -628,7 +618,7 @@ class ConfigCollector {
|
||||||
allAnswers[question.name] = question.default;
|
allAnswers[question.name] = question.default;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const promptedAnswers = await inquirer.prompt(questions);
|
const promptedAnswers = await prompts.prompt(questions);
|
||||||
Object.assign(allAnswers, promptedAnswers);
|
Object.assign(allAnswers, promptedAnswers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -750,7 +740,7 @@ class ConfigCollector {
|
||||||
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
console.log(chalk.cyan('?') + ' ' + chalk.magenta(moduleDisplayName));
|
||||||
|
|
||||||
// Ask user if they want to accept defaults or customize on the next line
|
// Ask user if they want to accept defaults or customize on the next line
|
||||||
const { customize } = await inquirer.prompt([
|
const { customize } = await prompts.prompt([
|
||||||
{
|
{
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
name: 'customize',
|
name: 'customize',
|
||||||
|
|
@ -845,7 +835,7 @@ class ConfigCollector {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an inquirer question from a config item
|
* Build a prompt question from a config item
|
||||||
* @param {string} moduleName - Module name
|
* @param {string} moduleName - Module name
|
||||||
* @param {string} key - Config key
|
* @param {string} key - Config key
|
||||||
* @param {Object} item - Config item definition
|
* @param {Object} item - Config item definition
|
||||||
|
|
@ -1007,7 +997,7 @@ class ConfigCollector {
|
||||||
message: message,
|
message: message,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set default - if it's dynamic, use a function that inquirer will evaluate with current answers
|
// Set default - if it's dynamic, use a function that the prompt will evaluate with current answers
|
||||||
// But if we have an existing value, always use that instead
|
// But if we have an existing value, always use that instead
|
||||||
if (existingValue !== null && existingValue !== undefined && questionType !== 'list') {
|
if (existingValue !== null && existingValue !== undefined && questionType !== 'list') {
|
||||||
question.default = existingValue;
|
question.default = existingValue;
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ const { CLIUtils } = require('../../../lib/cli-utils');
|
||||||
const { ManifestGenerator } = require('./manifest-generator');
|
const { ManifestGenerator } = require('./manifest-generator');
|
||||||
const { IdeConfigManager } = require('./ide-config-manager');
|
const { IdeConfigManager } = require('./ide-config-manager');
|
||||||
const { CustomHandler } = require('../custom/handler');
|
const { CustomHandler } = require('../custom/handler');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
// BMAD installation folder name - this is constant and should never change
|
// BMAD installation folder name - this is constant and should never change
|
||||||
const BMAD_FOLDER_NAME = '_bmad';
|
const BMAD_FOLDER_NAME = '_bmad';
|
||||||
|
|
@ -758,6 +759,9 @@ class Installer {
|
||||||
config.skipIde = toolSelection.skipIde;
|
config.skipIde = toolSelection.skipIde;
|
||||||
const ideConfigurations = toolSelection.configurations;
|
const ideConfigurations = toolSelection.configurations;
|
||||||
|
|
||||||
|
// Add spacing after prompts before installation progress
|
||||||
|
console.log('');
|
||||||
|
|
||||||
if (spinner.isSpinning) {
|
if (spinner.isSpinning) {
|
||||||
spinner.text = 'Continuing installation...';
|
spinner.text = 'Continuing installation...';
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -2139,15 +2143,11 @@ class Installer {
|
||||||
* Private: Prompt for update action
|
* Private: Prompt for update action
|
||||||
*/
|
*/
|
||||||
async promptUpdateAction() {
|
async promptUpdateAction() {
|
||||||
const { default: inquirer } = await import('inquirer');
|
const action = await prompts.select({
|
||||||
return await inquirer.prompt([
|
message: 'What would you like to do?',
|
||||||
{
|
choices: [{ name: 'Update existing installation', value: 'update' }],
|
||||||
type: 'list',
|
});
|
||||||
name: 'action',
|
return { action };
|
||||||
message: 'What would you like to do?',
|
|
||||||
choices: [{ name: 'Update existing installation', value: 'update' }],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -2156,8 +2156,6 @@ class Installer {
|
||||||
* @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
|
* @param {Object} _legacyV4 - Legacy V4 detection result (unused in simplified version)
|
||||||
*/
|
*/
|
||||||
async handleLegacyV4Migration(_projectDir, _legacyV4) {
|
async handleLegacyV4Migration(_projectDir, _legacyV4) {
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
|
|
||||||
console.log('');
|
console.log('');
|
||||||
console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected'));
|
console.log(chalk.yellow.bold('⚠️ Legacy BMAD v4 detected'));
|
||||||
console.log(chalk.yellow('─'.repeat(80)));
|
console.log(chalk.yellow('─'.repeat(80)));
|
||||||
|
|
@ -2172,26 +2170,22 @@ class Installer {
|
||||||
console.log(chalk.dim('If your v4 installation set up rules or commands, you should remove those as well.'));
|
console.log(chalk.dim('If your v4 installation set up rules or commands, you should remove those as well.'));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
const { proceed } = await inquirer.prompt([
|
const proceed = await prompts.select({
|
||||||
{
|
message: 'What would you like to do?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'proceed',
|
{
|
||||||
message: 'What would you like to do?',
|
name: 'Exit and clean up manually (recommended)',
|
||||||
choices: [
|
value: 'exit',
|
||||||
{
|
hint: 'Exit installation',
|
||||||
name: 'Exit and clean up manually (recommended)',
|
},
|
||||||
value: 'exit',
|
{
|
||||||
short: 'Exit installation',
|
name: 'Continue with installation anyway',
|
||||||
},
|
value: 'continue',
|
||||||
{
|
hint: 'Continue',
|
||||||
name: 'Continue with installation anyway',
|
},
|
||||||
value: 'continue',
|
],
|
||||||
short: 'Continue',
|
default: 'exit',
|
||||||
},
|
});
|
||||||
],
|
|
||||||
default: 'exit',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (proceed === 'exit') {
|
if (proceed === 'exit') {
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
@ -2437,7 +2431,6 @@ class Installer {
|
||||||
|
|
||||||
console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
|
console.log(chalk.yellow(`\n⚠️ Found ${customModulesWithMissingSources.length} custom module(s) with missing sources:`));
|
||||||
|
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
let keptCount = 0;
|
let keptCount = 0;
|
||||||
let updatedCount = 0;
|
let updatedCount = 0;
|
||||||
let removedCount = 0;
|
let removedCount = 0;
|
||||||
|
|
@ -2451,12 +2444,12 @@ class Installer {
|
||||||
{
|
{
|
||||||
name: 'Keep installed (will not be processed)',
|
name: 'Keep installed (will not be processed)',
|
||||||
value: 'keep',
|
value: 'keep',
|
||||||
short: 'Keep',
|
hint: 'Keep',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Specify new source location',
|
name: 'Specify new source location',
|
||||||
value: 'update',
|
value: 'update',
|
||||||
short: 'Update',
|
hint: 'Update',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
@ -2465,47 +2458,40 @@ class Installer {
|
||||||
choices.push({
|
choices.push({
|
||||||
name: '⚠️ REMOVE module completely (destructive!)',
|
name: '⚠️ REMOVE module completely (destructive!)',
|
||||||
value: 'remove',
|
value: 'remove',
|
||||||
short: 'Remove',
|
hint: 'Remove',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { action } = await inquirer.prompt([
|
const action = await prompts.select({
|
||||||
{
|
message: `How would you like to handle "${missing.name}"?`,
|
||||||
type: 'list',
|
choices,
|
||||||
name: 'action',
|
});
|
||||||
message: `How would you like to handle "${missing.name}"?`,
|
|
||||||
choices,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'update': {
|
case 'update': {
|
||||||
const { newSourcePath } = await inquirer.prompt([
|
// Use sync validation because @clack/prompts doesn't support async validate
|
||||||
{
|
const newSourcePath = await prompts.text({
|
||||||
type: 'input',
|
message: 'Enter the new path to the custom module:',
|
||||||
name: 'newSourcePath',
|
default: missing.sourcePath,
|
||||||
message: 'Enter the new path to the custom module:',
|
validate: (input) => {
|
||||||
default: missing.sourcePath,
|
if (!input || input.trim() === '') {
|
||||||
validate: async (input) => {
|
return 'Please enter a path';
|
||||||
if (!input || input.trim() === '') {
|
}
|
||||||
return 'Please enter a path';
|
const expandedPath = path.resolve(input.trim());
|
||||||
}
|
if (!fs.pathExistsSync(expandedPath)) {
|
||||||
const expandedPath = path.resolve(input.trim());
|
return 'Path does not exist';
|
||||||
if (!(await fs.pathExists(expandedPath))) {
|
}
|
||||||
return 'Path does not exist';
|
// Check if it looks like a valid module
|
||||||
}
|
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
||||||
// Check if it looks like a valid module
|
const agentsPath = path.join(expandedPath, 'agents');
|
||||||
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
const workflowsPath = path.join(expandedPath, 'workflows');
|
||||||
const agentsPath = path.join(expandedPath, 'agents');
|
|
||||||
const workflowsPath = path.join(expandedPath, 'workflows');
|
|
||||||
|
|
||||||
if (!(await fs.pathExists(moduleYamlPath)) && !(await fs.pathExists(agentsPath)) && !(await fs.pathExists(workflowsPath))) {
|
if (!fs.pathExistsSync(moduleYamlPath) && !fs.pathExistsSync(agentsPath) && !fs.pathExistsSync(workflowsPath)) {
|
||||||
return 'Path does not appear to contain a valid custom module';
|
return 'Path does not appear to contain a valid custom module';
|
||||||
}
|
}
|
||||||
return true;
|
return; // clack expects undefined for valid input
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]);
|
});
|
||||||
|
|
||||||
// Update the source in manifest
|
// Update the source in manifest
|
||||||
const resolvedPath = path.resolve(newSourcePath.trim());
|
const resolvedPath = path.resolve(newSourcePath.trim());
|
||||||
|
|
@ -2531,46 +2517,38 @@ class Installer {
|
||||||
console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
|
console.log(chalk.red.bold(`\n⚠️ WARNING: This will PERMANENTLY DELETE "${missing.name}" and all its files!`));
|
||||||
console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`));
|
console.log(chalk.red(` Module location: ${path.join(bmadDir, missing.id)}`));
|
||||||
|
|
||||||
const { confirm } = await inquirer.prompt([
|
const confirmDelete = await prompts.confirm({
|
||||||
{
|
message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'confirm',
|
});
|
||||||
message: chalk.red.bold('Are you absolutely sure you want to delete this module?'),
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (confirm) {
|
if (confirmDelete) {
|
||||||
const { typedConfirm } = await inquirer.prompt([
|
const typedConfirm = await prompts.text({
|
||||||
{
|
message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
|
||||||
type: 'input',
|
validate: (input) => {
|
||||||
name: 'typedConfirm',
|
if (input !== 'DELETE') {
|
||||||
message: chalk.red.bold('Type "DELETE" to confirm permanent deletion:'),
|
return chalk.red('You must type "DELETE" exactly to proceed');
|
||||||
validate: (input) => {
|
}
|
||||||
if (input !== 'DELETE') {
|
return; // clack expects undefined for valid input
|
||||||
return chalk.red('You must type "DELETE" exactly to proceed');
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]);
|
});
|
||||||
|
|
||||||
if (typedConfirm === 'DELETE') {
|
if (typedConfirm === 'DELETE') {
|
||||||
// Remove the module from filesystem and manifest
|
// Remove the module from filesystem and manifest
|
||||||
const modulePath = path.join(bmadDir, moduleId);
|
const modulePath = path.join(bmadDir, missing.id);
|
||||||
if (await fs.pathExists(modulePath)) {
|
if (await fs.pathExists(modulePath)) {
|
||||||
const fsExtra = require('fs-extra');
|
const fsExtra = require('fs-extra');
|
||||||
await fsExtra.remove(modulePath);
|
await fsExtra.remove(modulePath);
|
||||||
console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`));
|
console.log(chalk.yellow(` ✓ Deleted module directory: ${path.relative(projectRoot, modulePath)}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.manifest.removeModule(bmadDir, moduleId);
|
await this.manifest.removeModule(bmadDir, missing.id);
|
||||||
await this.manifest.removeCustomModule(bmadDir, moduleId);
|
await this.manifest.removeCustomModule(bmadDir, missing.id);
|
||||||
console.log(chalk.yellow(` ✓ Removed from manifest`));
|
console.log(chalk.yellow(` ✓ Removed from manifest`));
|
||||||
|
|
||||||
// Also remove from installedModules list
|
// Also remove from installedModules list
|
||||||
if (installedModules && installedModules.includes(moduleId)) {
|
if (installedModules && installedModules.includes(missing.id)) {
|
||||||
const index = installedModules.indexOf(moduleId);
|
const index = installedModules.indexOf(missing.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
installedModules.splice(index, 1);
|
installedModules.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
@ -2591,7 +2569,7 @@ class Installer {
|
||||||
}
|
}
|
||||||
case 'keep': {
|
case 'keep': {
|
||||||
keptCount++;
|
keptCount++;
|
||||||
keptModulesWithoutSources.push(moduleId);
|
keptModulesWithoutSources.push(missing.id);
|
||||||
console.log(chalk.dim(` Module will be kept as-is`));
|
console.log(chalk.dim(` Module will be kept as-is`));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const {
|
||||||
resolveSubagentFiles,
|
resolveSubagentFiles,
|
||||||
} = require('./shared/module-injections');
|
} = require('./shared/module-injections');
|
||||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Google Antigravity IDE setup handler
|
* Google Antigravity IDE setup handler
|
||||||
|
|
@ -26,6 +27,21 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
this.workflowsDir = 'workflows';
|
this.workflowsDir = 'workflows';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt for subagent installation location
|
||||||
|
* @returns {Promise<string>} Selected location ('project' or 'user')
|
||||||
|
*/
|
||||||
|
async _promptInstallLocation() {
|
||||||
|
return prompts.select({
|
||||||
|
message: 'Where would you like to install Antigravity subagents?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
||||||
|
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
||||||
|
],
|
||||||
|
default: 'project',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect configuration choices before installation
|
* Collect configuration choices before installation
|
||||||
* @param {Object} options - Configuration options
|
* @param {Object} options - Configuration options
|
||||||
|
|
@ -57,21 +73,7 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
||||||
|
|
||||||
if (config.subagentChoices.install !== 'none') {
|
if (config.subagentChoices.install !== 'none') {
|
||||||
// Ask for installation location
|
config.installLocation = await this._promptInstallLocation();
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
const locationAnswer = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'list',
|
|
||||||
name: 'location',
|
|
||||||
message: 'Where would you like to install Antigravity subagents?',
|
|
||||||
choices: [
|
|
||||||
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
|
||||||
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
|
||||||
],
|
|
||||||
default: 'project',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
config.installLocation = locationAnswer.location;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -297,20 +299,7 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
choices = await this.promptSubagentInstallation(config.subagents);
|
choices = await this.promptSubagentInstallation(config.subagents);
|
||||||
|
|
||||||
if (choices.install !== 'none') {
|
if (choices.install !== 'none') {
|
||||||
const { default: inquirer } = await import('inquirer');
|
location = await this._promptInstallLocation();
|
||||||
const locationAnswer = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'list',
|
|
||||||
name: 'location',
|
|
||||||
message: 'Where would you like to install Antigravity subagents?',
|
|
||||||
choices: [
|
|
||||||
{ name: 'Project level (.agent/agents/)', value: 'project' },
|
|
||||||
{ name: 'User level (~/.agent/agents/)', value: 'user' },
|
|
||||||
],
|
|
||||||
default: 'project',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
location = locationAnswer.location;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -334,22 +323,16 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
* Prompt user for subagent installation preferences
|
* Prompt user for subagent installation preferences
|
||||||
*/
|
*/
|
||||||
async promptSubagentInstallation(subagentConfig) {
|
async promptSubagentInstallation(subagentConfig) {
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
|
|
||||||
// First ask if they want to install subagents
|
// First ask if they want to install subagents
|
||||||
const { install } = await inquirer.prompt([
|
const install = await prompts.select({
|
||||||
{
|
message: 'Would you like to install Antigravity subagents for enhanced functionality?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'install',
|
{ name: 'Yes, install all subagents', value: 'all' },
|
||||||
message: 'Would you like to install Antigravity subagents for enhanced functionality?',
|
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
||||||
choices: [
|
{ name: 'No, skip subagent installation', value: 'none' },
|
||||||
{ name: 'Yes, install all subagents', value: 'all' },
|
],
|
||||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
default: 'all',
|
||||||
{ name: 'No, skip subagent installation', value: 'none' },
|
});
|
||||||
],
|
|
||||||
default: 'all',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (install === 'selective') {
|
if (install === 'selective') {
|
||||||
// Show list of available subagents with descriptions
|
// Show list of available subagents with descriptions
|
||||||
|
|
@ -361,18 +344,14 @@ class AntigravitySetup extends BaseIdeSetup {
|
||||||
'document-reviewer.md': 'Document quality review',
|
'document-reviewer.md': 'Document quality review',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { selected } = await inquirer.prompt([
|
const selected = await prompts.multiselect({
|
||||||
{
|
message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
||||||
type: 'checkbox',
|
choices: subagentConfig.files.map((file) => ({
|
||||||
name: 'selected',
|
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||||
message: 'Select subagents to install:',
|
value: file,
|
||||||
choices: subagentConfig.files.map((file) => ({
|
checked: true,
|
||||||
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
})),
|
||||||
value: file,
|
});
|
||||||
checked: true,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { install: 'selective', selected };
|
return { install: 'selective', selected };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const {
|
||||||
resolveSubagentFiles,
|
resolveSubagentFiles,
|
||||||
} = require('./shared/module-injections');
|
} = require('./shared/module-injections');
|
||||||
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
const { getAgentsFromBmad, getAgentsFromDir } = require('./shared/bmad-artifacts');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Claude Code IDE setup handler
|
* Claude Code IDE setup handler
|
||||||
|
|
@ -25,6 +26,21 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
this.agentsDir = 'agents';
|
this.agentsDir = 'agents';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prompt for subagent installation location
|
||||||
|
* @returns {Promise<string>} Selected location ('project' or 'user')
|
||||||
|
*/
|
||||||
|
async promptInstallLocation() {
|
||||||
|
return prompts.select({
|
||||||
|
message: 'Where would you like to install Claude Code subagents?',
|
||||||
|
choices: [
|
||||||
|
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
||||||
|
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
||||||
|
],
|
||||||
|
default: 'project',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Collect configuration choices before installation
|
* Collect configuration choices before installation
|
||||||
* @param {Object} options - Configuration options
|
* @param {Object} options - Configuration options
|
||||||
|
|
@ -56,21 +72,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
config.subagentChoices = await this.promptSubagentInstallation(injectionConfig.subagents);
|
||||||
|
|
||||||
if (config.subagentChoices.install !== 'none') {
|
if (config.subagentChoices.install !== 'none') {
|
||||||
// Ask for installation location
|
config.installLocation = await this.promptInstallLocation();
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
const locationAnswer = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'list',
|
|
||||||
name: 'location',
|
|
||||||
message: 'Where would you like to install Claude Code subagents?',
|
|
||||||
choices: [
|
|
||||||
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
|
||||||
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
|
||||||
],
|
|
||||||
default: 'project',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
config.installLocation = locationAnswer.location;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -305,20 +307,7 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
choices = await this.promptSubagentInstallation(config.subagents);
|
choices = await this.promptSubagentInstallation(config.subagents);
|
||||||
|
|
||||||
if (choices.install !== 'none') {
|
if (choices.install !== 'none') {
|
||||||
const { default: inquirer } = await import('inquirer');
|
location = await this.promptInstallLocation();
|
||||||
const locationAnswer = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'list',
|
|
||||||
name: 'location',
|
|
||||||
message: 'Where would you like to install Claude Code subagents?',
|
|
||||||
choices: [
|
|
||||||
{ name: 'Project level (.claude/agents/)', value: 'project' },
|
|
||||||
{ name: 'User level (~/.claude/agents/)', value: 'user' },
|
|
||||||
],
|
|
||||||
default: 'project',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
location = locationAnswer.location;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -342,22 +331,16 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
* Prompt user for subagent installation preferences
|
* Prompt user for subagent installation preferences
|
||||||
*/
|
*/
|
||||||
async promptSubagentInstallation(subagentConfig) {
|
async promptSubagentInstallation(subagentConfig) {
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
|
|
||||||
// First ask if they want to install subagents
|
// First ask if they want to install subagents
|
||||||
const { install } = await inquirer.prompt([
|
const install = await prompts.select({
|
||||||
{
|
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'install',
|
{ name: 'Yes, install all subagents', value: 'all' },
|
||||||
message: 'Would you like to install Claude Code subagents for enhanced functionality?',
|
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
||||||
choices: [
|
{ name: 'No, skip subagent installation', value: 'none' },
|
||||||
{ name: 'Yes, install all subagents', value: 'all' },
|
],
|
||||||
{ name: 'Yes, let me choose specific subagents', value: 'selective' },
|
default: 'all',
|
||||||
{ name: 'No, skip subagent installation', value: 'none' },
|
});
|
||||||
],
|
|
||||||
default: 'all',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (install === 'selective') {
|
if (install === 'selective') {
|
||||||
// Show list of available subagents with descriptions
|
// Show list of available subagents with descriptions
|
||||||
|
|
@ -369,18 +352,14 @@ class ClaudeCodeSetup extends BaseIdeSetup {
|
||||||
'document-reviewer.md': 'Document quality review',
|
'document-reviewer.md': 'Document quality review',
|
||||||
};
|
};
|
||||||
|
|
||||||
const { selected } = await inquirer.prompt([
|
const selected = await prompts.multiselect({
|
||||||
{
|
message: `Select subagents to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
||||||
type: 'checkbox',
|
options: subagentConfig.files.map((file) => ({
|
||||||
name: 'selected',
|
label: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
||||||
message: 'Select subagents to install:',
|
value: file,
|
||||||
choices: subagentConfig.files.map((file) => ({
|
})),
|
||||||
name: `${file.replace('.md', '')} - ${subagentInfo[file] || 'Specialized assistant'}`,
|
initialValues: subagentConfig.files,
|
||||||
value: file,
|
});
|
||||||
checked: true,
|
|
||||||
})),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { install: 'selective', selected };
|
return { install: 'selective', selected };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ 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');
|
||||||
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
|
const { getTasksFromBmad } = require('./shared/bmad-artifacts');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Codex setup handler (CLI mode)
|
* Codex setup handler (CLI mode)
|
||||||
|
|
@ -21,32 +22,24 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
* @returns {Object} Collected configuration
|
* @returns {Object} Collected configuration
|
||||||
*/
|
*/
|
||||||
async collectConfiguration(options = {}) {
|
async collectConfiguration(options = {}) {
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
|
|
||||||
let confirmed = false;
|
let confirmed = false;
|
||||||
let installLocation = 'global';
|
let installLocation = 'global';
|
||||||
|
|
||||||
while (!confirmed) {
|
while (!confirmed) {
|
||||||
const { location } = await inquirer.prompt([
|
installLocation = await prompts.select({
|
||||||
{
|
message: 'Where would you like to install Codex CLI prompts?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'location',
|
{
|
||||||
message: 'Where would you like to install Codex CLI prompts?',
|
name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)',
|
||||||
choices: [
|
value: 'global',
|
||||||
{
|
},
|
||||||
name: 'Global - Simple for single project ' + '(~/.codex/prompts, but references THIS project only)',
|
{
|
||||||
value: 'global',
|
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`,
|
||||||
},
|
value: 'project',
|
||||||
{
|
},
|
||||||
name: `Project-specific - Recommended for real work (requires CODEX_HOME=<project-dir>${path.sep}.codex)`,
|
],
|
||||||
value: 'project',
|
default: 'global',
|
||||||
},
|
});
|
||||||
],
|
|
||||||
default: 'global',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
installLocation = location;
|
|
||||||
|
|
||||||
// Display detailed instructions for the chosen option
|
// Display detailed instructions for the chosen option
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
@ -57,16 +50,10 @@ class CodexSetup extends BaseIdeSetup {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Confirm the choice
|
// Confirm the choice
|
||||||
const { proceed } = await inquirer.prompt([
|
confirmed = await prompts.confirm({
|
||||||
{
|
message: 'Proceed with this installation option?',
|
||||||
type: 'confirm',
|
default: true,
|
||||||
name: 'proceed',
|
});
|
||||||
message: 'Proceed with this installation option?',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
confirmed = proceed;
|
|
||||||
|
|
||||||
if (!confirmed) {
|
if (!confirmed) {
|
||||||
console.log(chalk.yellow("\n Let's choose a different installation option.\n"));
|
console.log(chalk.yellow("\n Let's choose a different installation option.\n"));
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ const path = require('node:path');
|
||||||
const { BaseIdeSetup } = require('./_base-ide');
|
const { BaseIdeSetup } = require('./_base-ide');
|
||||||
const chalk = require('chalk');
|
const chalk = require('chalk');
|
||||||
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
const { AgentCommandGenerator } = require('./shared/agent-command-generator');
|
||||||
|
const prompts = require('../../../lib/prompts');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GitHub Copilot setup handler
|
* GitHub Copilot setup handler
|
||||||
|
|
@ -21,29 +22,23 @@ class GitHubCopilotSetup extends BaseIdeSetup {
|
||||||
* @returns {Object} Collected configuration
|
* @returns {Object} Collected configuration
|
||||||
*/
|
*/
|
||||||
async collectConfiguration(options = {}) {
|
async collectConfiguration(options = {}) {
|
||||||
const { default: inquirer } = await import('inquirer');
|
|
||||||
const config = {};
|
const config = {};
|
||||||
|
|
||||||
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
|
console.log('\n' + chalk.blue(' 🔧 VS Code Settings Configuration'));
|
||||||
console.log(chalk.dim(' GitHub Copilot works best with specific settings\n'));
|
console.log(chalk.dim(' GitHub Copilot works best with specific settings\n'));
|
||||||
|
|
||||||
const response = await inquirer.prompt([
|
config.vsCodeConfig = await prompts.select({
|
||||||
{
|
message: 'How would you like to configure VS Code settings?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'configChoice',
|
{ name: 'Use recommended defaults (fastest)', value: 'defaults' },
|
||||||
message: 'How would you like to configure VS Code settings?',
|
{ name: 'Configure each setting manually', value: 'manual' },
|
||||||
choices: [
|
{ name: 'Skip settings configuration', value: 'skip' },
|
||||||
{ name: 'Use recommended defaults (fastest)', value: 'defaults' },
|
],
|
||||||
{ name: 'Configure each setting manually', value: 'manual' },
|
default: 'defaults',
|
||||||
{ name: 'Skip settings configuration', value: 'skip' },
|
});
|
||||||
],
|
|
||||||
default: 'defaults',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
config.vsCodeConfig = response.configChoice;
|
|
||||||
|
|
||||||
if (response.configChoice === 'manual') {
|
if (config.vsCodeConfig === 'manual') {
|
||||||
config.manualSettings = await inquirer.prompt([
|
config.manualSettings = await prompts.prompt([
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'maxRequests',
|
name: 'maxRequests',
|
||||||
|
|
@ -52,7 +47,8 @@ class GitHubCopilotSetup extends BaseIdeSetup {
|
||||||
validate: (input) => {
|
validate: (input) => {
|
||||||
const num = parseInt(input, 10);
|
const num = parseInt(input, 10);
|
||||||
if (isNaN(num)) return 'Enter a valid number 1-50';
|
if (isNaN(num)) return 'Enter a valid number 1-50';
|
||||||
return (num >= 1 && num <= 50) || 'Enter 1-50';
|
if (num < 1 || num > 50) return 'Enter a number between 1-50';
|
||||||
|
return true;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,432 @@
|
||||||
|
/**
|
||||||
|
* @clack/prompts wrapper for BMAD CLI
|
||||||
|
*
|
||||||
|
* This module provides a unified interface for CLI prompts using @clack/prompts.
|
||||||
|
* It replaces Inquirer.js to fix Windows arrow key navigation issues (libuv #852).
|
||||||
|
*
|
||||||
|
* @module prompts
|
||||||
|
*/
|
||||||
|
|
||||||
|
let _clack = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lazy-load @clack/prompts (ESM module)
|
||||||
|
* @returns {Promise<Object>} The clack prompts module
|
||||||
|
*/
|
||||||
|
async function getClack() {
|
||||||
|
if (!_clack) {
|
||||||
|
_clack = await import('@clack/prompts');
|
||||||
|
}
|
||||||
|
return _clack;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle user cancellation gracefully
|
||||||
|
* @param {any} value - The value to check
|
||||||
|
* @param {string} [message='Operation cancelled'] - Message to display
|
||||||
|
* @returns {boolean} True if cancelled
|
||||||
|
*/
|
||||||
|
async function handleCancel(value, message = 'Operation cancelled') {
|
||||||
|
const clack = await getClack();
|
||||||
|
if (clack.isCancel(value)) {
|
||||||
|
clack.cancel(message);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display intro message
|
||||||
|
* @param {string} message - The intro message
|
||||||
|
*/
|
||||||
|
async function intro(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.intro(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display outro message
|
||||||
|
* @param {string} message - The outro message
|
||||||
|
*/
|
||||||
|
async function outro(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.outro(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a note/info box
|
||||||
|
* @param {string} message - The note content
|
||||||
|
* @param {string} [title] - Optional title
|
||||||
|
*/
|
||||||
|
async function note(message, title) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.note(message, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display a spinner for async operations
|
||||||
|
* @returns {Object} Spinner controller with start, stop, message methods
|
||||||
|
*/
|
||||||
|
async function spinner() {
|
||||||
|
const clack = await getClack();
|
||||||
|
return clack.spinner();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Single-select prompt (replaces Inquirer 'list' type)
|
||||||
|
* @param {Object} options - Prompt options
|
||||||
|
* @param {string} options.message - The question to ask
|
||||||
|
* @param {Array} options.choices - Array of choices [{name, value, hint?}]
|
||||||
|
* @param {any} [options.default] - Default selected value
|
||||||
|
* @returns {Promise<any>} Selected value
|
||||||
|
*/
|
||||||
|
async function select(options) {
|
||||||
|
const clack = await getClack();
|
||||||
|
|
||||||
|
// Convert Inquirer-style choices to clack format
|
||||||
|
// Handle both object choices {name, value, hint} and primitive choices (string/number)
|
||||||
|
const clackOptions = options.choices
|
||||||
|
.filter((c) => c.type !== 'separator') // Skip separators for now
|
||||||
|
.map((choice) => {
|
||||||
|
if (typeof choice === 'string' || typeof choice === 'number') {
|
||||||
|
return { value: choice, label: String(choice) };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: choice.value === undefined ? choice.name : choice.value,
|
||||||
|
label: choice.name || choice.label || String(choice.value),
|
||||||
|
hint: choice.hint || choice.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find initial value
|
||||||
|
let initialValue;
|
||||||
|
if (options.default !== undefined) {
|
||||||
|
initialValue = options.default;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await clack.select({
|
||||||
|
message: options.message,
|
||||||
|
options: clackOptions,
|
||||||
|
initialValue,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleCancel(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Multi-select prompt (replaces Inquirer 'checkbox' type)
|
||||||
|
* @param {Object} options - Prompt options
|
||||||
|
* @param {string} options.message - The question to ask
|
||||||
|
* @param {Array} options.choices - Array of choices [{name, value, checked?, hint?}]
|
||||||
|
* @param {boolean} [options.required=false] - Whether at least one must be selected
|
||||||
|
* @returns {Promise<Array>} Array of selected values
|
||||||
|
*/
|
||||||
|
async function multiselect(options) {
|
||||||
|
const clack = await getClack();
|
||||||
|
|
||||||
|
// Support both clack-native (options) and Inquirer-style (choices) APIs
|
||||||
|
let clackOptions;
|
||||||
|
let initialValues;
|
||||||
|
|
||||||
|
if (options.options) {
|
||||||
|
// Native clack format: options with label/value
|
||||||
|
clackOptions = options.options;
|
||||||
|
initialValues = options.initialValues || [];
|
||||||
|
} else {
|
||||||
|
// Convert Inquirer-style choices to clack format
|
||||||
|
// Handle both object choices {name, value, hint} and primitive choices (string/number)
|
||||||
|
clackOptions = options.choices
|
||||||
|
.filter((c) => c.type !== 'separator') // Skip separators
|
||||||
|
.map((choice) => {
|
||||||
|
if (typeof choice === 'string' || typeof choice === 'number') {
|
||||||
|
return { value: choice, label: String(choice) };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
value: choice.value === undefined ? choice.name : choice.value,
|
||||||
|
label: choice.name || choice.label || String(choice.value),
|
||||||
|
hint: choice.hint || choice.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find initial values (pre-checked items)
|
||||||
|
initialValues = options.choices
|
||||||
|
.filter((c) => c.checked && c.type !== 'separator')
|
||||||
|
.map((c) => (c.value === undefined ? c.name : c.value));
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await clack.multiselect({
|
||||||
|
message: options.message,
|
||||||
|
options: clackOptions,
|
||||||
|
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
||||||
|
required: options.required || false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleCancel(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,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleCancel(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirm prompt (replaces Inquirer 'confirm' type)
|
||||||
|
* @param {Object} options - Prompt options
|
||||||
|
* @param {string} options.message - The question to ask
|
||||||
|
* @param {boolean} [options.default=true] - Default value
|
||||||
|
* @returns {Promise<boolean>} User's answer
|
||||||
|
*/
|
||||||
|
async function confirm(options) {
|
||||||
|
const clack = await getClack();
|
||||||
|
|
||||||
|
const result = await clack.confirm({
|
||||||
|
message: options.message,
|
||||||
|
initialValue: options.default === undefined ? true : options.default,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleCancel(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Text input prompt (replaces Inquirer 'input' type)
|
||||||
|
* @param {Object} options - Prompt options
|
||||||
|
* @param {string} options.message - The question to ask
|
||||||
|
* @param {string} [options.default] - Default value
|
||||||
|
* @param {string} [options.placeholder] - Placeholder text (defaults to options.default if not provided)
|
||||||
|
* @param {Function} [options.validate] - Validation function
|
||||||
|
* @returns {Promise<string>} User's input
|
||||||
|
*/
|
||||||
|
async function text(options) {
|
||||||
|
const clack = await getClack();
|
||||||
|
|
||||||
|
// Use default as placeholder if placeholder not explicitly provided
|
||||||
|
// This shows the default value as grayed-out hint text
|
||||||
|
const placeholder = options.placeholder === undefined ? options.default : options.placeholder;
|
||||||
|
|
||||||
|
const result = await clack.text({
|
||||||
|
message: options.message,
|
||||||
|
defaultValue: options.default,
|
||||||
|
placeholder: typeof placeholder === 'string' ? placeholder : undefined,
|
||||||
|
validate: options.validate,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleCancel(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Password input prompt (replaces Inquirer 'password' type)
|
||||||
|
* @param {Object} options - Prompt options
|
||||||
|
* @param {string} options.message - The question to ask
|
||||||
|
* @param {Function} [options.validate] - Validation function
|
||||||
|
* @returns {Promise<string>} User's input
|
||||||
|
*/
|
||||||
|
async function password(options) {
|
||||||
|
const clack = await getClack();
|
||||||
|
|
||||||
|
const result = await clack.password({
|
||||||
|
message: options.message,
|
||||||
|
validate: options.validate,
|
||||||
|
});
|
||||||
|
|
||||||
|
await handleCancel(result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Group multiple prompts together
|
||||||
|
* @param {Object} prompts - Object of prompt functions
|
||||||
|
* @param {Object} [options] - Group options
|
||||||
|
* @returns {Promise<Object>} Object with all answers
|
||||||
|
*/
|
||||||
|
async function group(prompts, options = {}) {
|
||||||
|
const clack = await getClack();
|
||||||
|
|
||||||
|
const result = await clack.group(prompts, {
|
||||||
|
onCancel: () => {
|
||||||
|
clack.cancel('Operation cancelled');
|
||||||
|
process.exit(0);
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run tasks with spinner feedback
|
||||||
|
* @param {Array} tasks - Array of task objects [{title, task, enabled?}]
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function tasks(taskList) {
|
||||||
|
const clack = await getClack();
|
||||||
|
await clack.tasks(taskList);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log messages with styling
|
||||||
|
*/
|
||||||
|
const log = {
|
||||||
|
async info(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.log.info(message);
|
||||||
|
},
|
||||||
|
async success(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.log.success(message);
|
||||||
|
},
|
||||||
|
async warn(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.log.warn(message);
|
||||||
|
},
|
||||||
|
async error(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.log.error(message);
|
||||||
|
},
|
||||||
|
async message(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.log.message(message);
|
||||||
|
},
|
||||||
|
async step(message) {
|
||||||
|
const clack = await getClack();
|
||||||
|
clack.log.step(message);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute an array of Inquirer-style questions using @clack/prompts
|
||||||
|
* This provides compatibility with dynamic question arrays
|
||||||
|
* @param {Array} questions - Array of Inquirer-style question objects
|
||||||
|
* @returns {Promise<Object>} Object with answers keyed by question name
|
||||||
|
*/
|
||||||
|
async function prompt(questions) {
|
||||||
|
const answers = {};
|
||||||
|
|
||||||
|
for (const question of questions) {
|
||||||
|
const { type, name, message, choices, default: defaultValue, validate, when } = question;
|
||||||
|
|
||||||
|
// Handle conditional questions via 'when' property
|
||||||
|
if (when !== undefined) {
|
||||||
|
const shouldAsk = typeof when === 'function' ? await when(answers) : when;
|
||||||
|
if (!shouldAsk) continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let answer;
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'input': {
|
||||||
|
// Note: @clack/prompts doesn't support async validation, so validate must be sync
|
||||||
|
answer = await text({
|
||||||
|
message,
|
||||||
|
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
||||||
|
validate: validate
|
||||||
|
? (val) => {
|
||||||
|
const result = validate(val, answers);
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
throw new TypeError('Async validation is not supported by @clack/prompts. Please use synchronous validation.');
|
||||||
|
}
|
||||||
|
return result === true ? undefined : result;
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'confirm': {
|
||||||
|
answer = await confirm({
|
||||||
|
message,
|
||||||
|
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'list': {
|
||||||
|
answer = await select({
|
||||||
|
message,
|
||||||
|
choices: choices || [],
|
||||||
|
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'checkbox': {
|
||||||
|
answer = await multiselect({
|
||||||
|
message,
|
||||||
|
choices: choices || [],
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'password': {
|
||||||
|
// Note: @clack/prompts doesn't support async validation, so validate must be sync
|
||||||
|
answer = await password({
|
||||||
|
message,
|
||||||
|
validate: validate
|
||||||
|
? (val) => {
|
||||||
|
const result = validate(val, answers);
|
||||||
|
if (result instanceof Promise) {
|
||||||
|
throw new TypeError('Async validation is not supported by @clack/prompts. Please use synchronous validation.');
|
||||||
|
}
|
||||||
|
return result === true ? undefined : result;
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
// Default to text input for unknown types
|
||||||
|
answer = await text({
|
||||||
|
message,
|
||||||
|
default: typeof defaultValue === 'function' ? defaultValue(answers) : defaultValue,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
answers[name] = answer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return answers;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getClack,
|
||||||
|
handleCancel,
|
||||||
|
intro,
|
||||||
|
outro,
|
||||||
|
note,
|
||||||
|
spinner,
|
||||||
|
select,
|
||||||
|
multiselect,
|
||||||
|
groupMultiselect,
|
||||||
|
confirm,
|
||||||
|
text,
|
||||||
|
password,
|
||||||
|
group,
|
||||||
|
tasks,
|
||||||
|
log,
|
||||||
|
prompt,
|
||||||
|
};
|
||||||
|
|
@ -4,16 +4,21 @@ const os = require('node:os');
|
||||||
const fs = require('fs-extra');
|
const fs = require('fs-extra');
|
||||||
const { CLIUtils } = require('./cli-utils');
|
const { CLIUtils } = require('./cli-utils');
|
||||||
const { CustomHandler } = require('../installers/lib/custom/handler');
|
const { CustomHandler } = require('../installers/lib/custom/handler');
|
||||||
|
const prompts = require('./prompts');
|
||||||
|
|
||||||
// Lazy-load inquirer (ESM module) to avoid ERR_REQUIRE_ESM
|
// Separator class for visual grouping in select/multiselect prompts
|
||||||
let _inquirer = null;
|
// Note: @clack/prompts doesn't support separators natively, they are filtered out
|
||||||
async function getInquirer() {
|
class Separator {
|
||||||
if (!_inquirer) {
|
constructor(text = '────────') {
|
||||||
_inquirer = (await import('inquirer')).default;
|
this.line = text;
|
||||||
|
this.name = text;
|
||||||
}
|
}
|
||||||
return _inquirer;
|
type = 'separator';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Separator for choice lists (compatible interface)
|
||||||
|
const choiceUtils = { Separator };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UI utilities for the installer
|
* UI utilities for the installer
|
||||||
*/
|
*/
|
||||||
|
|
@ -23,7 +28,6 @@ class UI {
|
||||||
* @returns {Object} Installation configuration
|
* @returns {Object} Installation configuration
|
||||||
*/
|
*/
|
||||||
async promptInstall() {
|
async promptInstall() {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
CLIUtils.displayLogo();
|
CLIUtils.displayLogo();
|
||||||
|
|
||||||
// Display version-specific start message from install-messages.yaml
|
// Display version-specific start message from install-messages.yaml
|
||||||
|
|
@ -113,26 +117,20 @@ class UI {
|
||||||
console.log(chalk.yellow('─'.repeat(80)));
|
console.log(chalk.yellow('─'.repeat(80)));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
const { proceed } = await inquirer.prompt([
|
const proceed = await prompts.select({
|
||||||
{
|
message: 'What would you like to do?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'proceed',
|
{
|
||||||
message: 'What would you like to do?',
|
name: 'Cancel and do a fresh install (recommended)',
|
||||||
choices: [
|
value: 'cancel',
|
||||||
{
|
},
|
||||||
name: 'Cancel and do a fresh install (recommended)',
|
{
|
||||||
value: 'cancel',
|
name: 'Proceed anyway (will attempt update, potentially may fail or have unstable behavior)',
|
||||||
short: 'Cancel installation',
|
value: 'proceed',
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
name: 'Proceed anyway (will attempt update, potentially may fail or have unstable behavior)',
|
default: 'cancel',
|
||||||
value: 'proceed',
|
});
|
||||||
short: 'Proceed with update',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'cancel',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (proceed === 'cancel') {
|
if (proceed === 'cancel') {
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
@ -188,14 +186,10 @@ class UI {
|
||||||
|
|
||||||
// If Claude Code was selected, ask about TTS
|
// If Claude Code was selected, ask about TTS
|
||||||
if (claudeCodeSelected) {
|
if (claudeCodeSelected) {
|
||||||
const { enableTts } = await inquirer.prompt([
|
const enableTts = await prompts.confirm({
|
||||||
{
|
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'enableTts',
|
});
|
||||||
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (enableTts) {
|
if (enableTts) {
|
||||||
agentVibesConfig = { enabled: true, alreadyInstalled: false };
|
agentVibesConfig = { enabled: true, alreadyInstalled: false };
|
||||||
|
|
@ -250,18 +244,11 @@ class UI {
|
||||||
// Common actions
|
// Common actions
|
||||||
choices.push({ name: 'Modify BMAD Installation', value: 'update' });
|
choices.push({ name: 'Modify BMAD Installation', value: 'update' });
|
||||||
|
|
||||||
const promptResult = await inquirer.prompt([
|
actionType = await prompts.select({
|
||||||
{
|
message: 'What would you like to do?',
|
||||||
type: 'list',
|
choices: choices,
|
||||||
name: 'actionType',
|
default: choices[0].value,
|
||||||
message: 'What would you like to do?',
|
});
|
||||||
choices: choices,
|
|
||||||
default: choices[0].value, // Use the first option as default
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Extract actionType from prompt result
|
|
||||||
actionType = promptResult.actionType;
|
|
||||||
|
|
||||||
// Handle quick update separately
|
// Handle quick update separately
|
||||||
if (actionType === 'quick-update') {
|
if (actionType === 'quick-update') {
|
||||||
|
|
@ -290,14 +277,10 @@ class UI {
|
||||||
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
||||||
|
|
||||||
console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
|
console.log(chalk.dim(` Found existing modules: ${[...installedModuleIds].join(', ')}`));
|
||||||
const { changeModuleSelection } = await inquirer.prompt([
|
const changeModuleSelection = await prompts.confirm({
|
||||||
{
|
message: 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'changeModuleSelection',
|
});
|
||||||
message: 'Modify official module selection (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
let selectedModules = [];
|
let selectedModules = [];
|
||||||
if (changeModuleSelection) {
|
if (changeModuleSelection) {
|
||||||
|
|
@ -310,14 +293,10 @@ class UI {
|
||||||
|
|
||||||
// After module selection, ask about custom modules
|
// After module selection, ask about custom modules
|
||||||
console.log('');
|
console.log('');
|
||||||
const { changeCustomModules } = await inquirer.prompt([
|
const changeCustomModules = await prompts.confirm({
|
||||||
{
|
message: 'Modify custom module selection (add, update, or remove custom modules/agents/workflows)?',
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'changeCustomModules',
|
});
|
||||||
message: 'Modify custom module selection (add, update, or remove custom modules/agents/workflows)?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
|
let customModuleResult = { selectedCustomModules: [], customContentConfig: { hasCustomContent: false } };
|
||||||
if (changeCustomModules) {
|
if (changeCustomModules) {
|
||||||
|
|
@ -352,15 +331,10 @@ class UI {
|
||||||
let enableTts = false;
|
let enableTts = false;
|
||||||
|
|
||||||
if (hasClaudeCode) {
|
if (hasClaudeCode) {
|
||||||
const { enableTts: enable } = await inquirer.prompt([
|
enableTts = await prompts.confirm({
|
||||||
{
|
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'enableTts',
|
});
|
||||||
message: 'Claude Code supports TTS (Text-to-Speech). Would you like to enable it?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
enableTts = enable;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Core config with existing defaults (ask after TTS)
|
// Core config with existing defaults (ask after TTS)
|
||||||
|
|
@ -385,14 +359,10 @@ class UI {
|
||||||
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
const { installedModuleIds } = await this.getExistingInstallation(confirmedDirectory);
|
||||||
|
|
||||||
// Ask about official modules for new installations
|
// Ask about official modules for new installations
|
||||||
const { wantsOfficialModules } = await inquirer.prompt([
|
const wantsOfficialModules = await prompts.confirm({
|
||||||
{
|
message: 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
||||||
type: 'confirm',
|
default: true,
|
||||||
name: 'wantsOfficialModules',
|
});
|
||||||
message: 'Will you be installing any official BMad modules (BMad Method, BMad Builder, Creative Innovation Suite)?',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
let selectedOfficialModules = [];
|
let selectedOfficialModules = [];
|
||||||
if (wantsOfficialModules) {
|
if (wantsOfficialModules) {
|
||||||
|
|
@ -401,14 +371,10 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask about custom content
|
// Ask about custom content
|
||||||
const { wantsCustomContent } = await inquirer.prompt([
|
const wantsCustomContent = await prompts.confirm({
|
||||||
{
|
message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'wantsCustomContent',
|
});
|
||||||
message: 'Would you like to install a local custom module (this includes custom agents and workflows also)?',
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (wantsCustomContent) {
|
if (wantsCustomContent) {
|
||||||
customContentConfig = await this.promptCustomContentSource();
|
customContentConfig = await this.promptCustomContentSource();
|
||||||
|
|
@ -459,7 +425,6 @@ class UI {
|
||||||
* @returns {Object} Tool configuration
|
* @returns {Object} Tool configuration
|
||||||
*/
|
*/
|
||||||
async promptToolSelection(projectDir, selectedModules) {
|
async promptToolSelection(projectDir, selectedModules) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
|
// Check for existing configured IDEs - use findBmadDir to detect custom folder names
|
||||||
const { Detector } = require('../installers/lib/core/detector');
|
const { Detector } = require('../installers/lib/core/detector');
|
||||||
const { Installer } = require('../installers/lib/core/installer');
|
const { Installer } = require('../installers/lib/core/installer');
|
||||||
|
|
@ -477,13 +442,14 @@ class UI {
|
||||||
const preferredIdes = ideManager.getPreferredIdes();
|
const preferredIdes = ideManager.getPreferredIdes();
|
||||||
const otherIdes = ideManager.getOtherIdes();
|
const otherIdes = ideManager.getOtherIdes();
|
||||||
|
|
||||||
// Build IDE choices array with separators
|
// Build grouped options object for groupMultiselect
|
||||||
const ideChoices = [];
|
const groupedOptions = {};
|
||||||
const processedIdes = new Set();
|
const processedIdes = new Set();
|
||||||
|
const initialValues = [];
|
||||||
|
|
||||||
// First, add previously configured IDEs at the top, marked with ✅
|
// First, add previously configured IDEs at the top, marked with ✅
|
||||||
if (configuredIdes.length > 0) {
|
if (configuredIdes.length > 0) {
|
||||||
ideChoices.push(new inquirer.Separator('── Previously Configured ──'));
|
const configuredGroup = [];
|
||||||
for (const ideValue of configuredIdes) {
|
for (const ideValue of configuredIdes) {
|
||||||
// Skip empty or invalid IDE values
|
// Skip empty or invalid IDE values
|
||||||
if (!ideValue || typeof ideValue !== 'string') {
|
if (!ideValue || typeof ideValue !== 'string') {
|
||||||
|
|
@ -496,81 +462,71 @@ class UI {
|
||||||
const ide = preferredIde || otherIde;
|
const ide = preferredIde || otherIde;
|
||||||
|
|
||||||
if (ide) {
|
if (ide) {
|
||||||
ideChoices.push({
|
configuredGroup.push({
|
||||||
name: `${ide.name} ✅`,
|
label: `${ide.name} ✅`,
|
||||||
value: ide.value,
|
value: ide.value,
|
||||||
checked: true, // Previously configured IDEs are checked by default
|
|
||||||
});
|
});
|
||||||
processedIdes.add(ide.value);
|
processedIdes.add(ide.value);
|
||||||
|
initialValues.push(ide.value); // Pre-select configured IDEs
|
||||||
} else {
|
} else {
|
||||||
// Warn about unrecognized IDE (but don't fail)
|
// Warn about unrecognized IDE (but don't fail)
|
||||||
console.log(chalk.yellow(`⚠️ Previously configured IDE '${ideValue}' is no longer available`));
|
console.log(chalk.yellow(`⚠️ Previously configured IDE '${ideValue}' is no longer available`));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (configuredGroup.length > 0) {
|
||||||
|
groupedOptions['Previously Configured'] = configuredGroup;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add preferred tools (excluding already processed)
|
// Add preferred tools (excluding already processed)
|
||||||
const remainingPreferred = preferredIdes.filter((ide) => !processedIdes.has(ide.value));
|
const remainingPreferred = preferredIdes.filter((ide) => !processedIdes.has(ide.value));
|
||||||
if (remainingPreferred.length > 0) {
|
if (remainingPreferred.length > 0) {
|
||||||
ideChoices.push(new inquirer.Separator('── Recommended Tools ──'));
|
groupedOptions['Recommended Tools'] = remainingPreferred.map((ide) => {
|
||||||
for (const ide of remainingPreferred) {
|
|
||||||
ideChoices.push({
|
|
||||||
name: `${ide.name} ⭐`,
|
|
||||||
value: ide.value,
|
|
||||||
checked: false,
|
|
||||||
});
|
|
||||||
processedIdes.add(ide.value);
|
processedIdes.add(ide.value);
|
||||||
}
|
return {
|
||||||
|
label: `${ide.name} ⭐`,
|
||||||
|
value: ide.value,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add other tools (excluding already processed)
|
// Add other tools (excluding already processed)
|
||||||
const remainingOther = otherIdes.filter((ide) => !processedIdes.has(ide.value));
|
const remainingOther = otherIdes.filter((ide) => !processedIdes.has(ide.value));
|
||||||
if (remainingOther.length > 0) {
|
if (remainingOther.length > 0) {
|
||||||
ideChoices.push(new inquirer.Separator('── Additional Tools ──'));
|
groupedOptions['Additional Tools'] = remainingOther.map((ide) => ({
|
||||||
for (const ide of remainingOther) {
|
label: ide.name,
|
||||||
ideChoices.push({
|
value: ide.value,
|
||||||
name: ide.name,
|
}));
|
||||||
value: ide.value,
|
|
||||||
checked: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let answers;
|
let selectedIdes = [];
|
||||||
let userConfirmedNoTools = false;
|
let userConfirmedNoTools = false;
|
||||||
|
|
||||||
// Loop until user selects at least one tool OR explicitly confirms no tools
|
// Loop until user selects at least one tool OR explicitly confirms no tools
|
||||||
while (!userConfirmedNoTools) {
|
while (!userConfirmedNoTools) {
|
||||||
answers = await inquirer.prompt([
|
selectedIdes = await prompts.groupMultiselect({
|
||||||
{
|
message: `Select tools to configure ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
||||||
type: 'checkbox',
|
options: groupedOptions,
|
||||||
name: 'ides',
|
initialValues: initialValues.length > 0 ? initialValues : undefined,
|
||||||
message: 'Select tools to configure:',
|
required: false,
|
||||||
choices: ideChoices,
|
});
|
||||||
pageSize: 30,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// If tools were selected, we're done
|
// If tools were selected, we're done
|
||||||
if (answers.ides && answers.ides.length > 0) {
|
if (selectedIdes && selectedIdes.length > 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn that no tools were selected - users often miss the spacebar requirement
|
// Warn that no tools were selected - users often miss the spacebar requirement
|
||||||
console.log();
|
console.log();
|
||||||
console.log(chalk.red.bold('⚠️ WARNING: No tools were selected!'));
|
console.log(chalk.red.bold('⚠️ WARNING: No tools were selected!'));
|
||||||
console.log(chalk.red(' You must press SPACEBAR to select items, then ENTER to confirm.'));
|
console.log(chalk.red(' You must press SPACE to select items, then ENTER to confirm.'));
|
||||||
console.log(chalk.red(' Simply highlighting an item does NOT select it.'));
|
console.log(chalk.red(' Simply highlighting an item does NOT select it.'));
|
||||||
console.log();
|
console.log();
|
||||||
|
|
||||||
const { goBack } = await inquirer.prompt([
|
const goBack = await prompts.confirm({
|
||||||
{
|
message: chalk.yellow('Would you like to go back and select at least one tool?'),
|
||||||
type: 'confirm',
|
default: true,
|
||||||
name: 'goBack',
|
});
|
||||||
message: chalk.yellow('Would you like to go back and select at least one tool?'),
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (goBack) {
|
if (goBack) {
|
||||||
// Re-display a message before looping back
|
// Re-display a message before looping back
|
||||||
|
|
@ -582,8 +538,8 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ides: answers.ides || [],
|
ides: selectedIdes || [],
|
||||||
skipIde: !answers.ides || answers.ides.length === 0,
|
skipIde: !selectedIdes || selectedIdes.length === 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -592,23 +548,17 @@ class UI {
|
||||||
* @returns {Object} Update configuration
|
* @returns {Object} Update configuration
|
||||||
*/
|
*/
|
||||||
async promptUpdate() {
|
async promptUpdate() {
|
||||||
const inquirer = await getInquirer();
|
const backupFirst = await prompts.confirm({
|
||||||
const answers = await inquirer.prompt([
|
message: 'Create backup before updating?',
|
||||||
{
|
default: true,
|
||||||
type: 'confirm',
|
});
|
||||||
name: 'backupFirst',
|
|
||||||
message: 'Create backup before updating?',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'preserveCustomizations',
|
|
||||||
message: 'Preserve local customizations?',
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return answers;
|
const preserveCustomizations = await prompts.confirm({
|
||||||
|
message: 'Preserve local customizations?',
|
||||||
|
default: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { backupFirst, preserveCustomizations };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -617,27 +567,17 @@ class UI {
|
||||||
* @returns {Array} Selected modules
|
* @returns {Array} Selected modules
|
||||||
*/
|
*/
|
||||||
async promptModules(modules) {
|
async promptModules(modules) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
const choices = modules.map((mod) => ({
|
const choices = modules.map((mod) => ({
|
||||||
name: `${mod.name} - ${mod.description}`,
|
name: `${mod.name} - ${mod.description}`,
|
||||||
value: mod.id,
|
value: mod.id,
|
||||||
checked: false,
|
checked: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { selectedModules } = await inquirer.prompt([
|
const selectedModules = await prompts.multiselect({
|
||||||
{
|
message: `Select modules to add ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
||||||
type: 'checkbox',
|
choices,
|
||||||
name: 'selectedModules',
|
required: true,
|
||||||
message: 'Select modules to add:',
|
});
|
||||||
choices,
|
|
||||||
validate: (answer) => {
|
|
||||||
if (answer.length === 0) {
|
|
||||||
return 'You must choose at least one module.';
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return selectedModules;
|
return selectedModules;
|
||||||
}
|
}
|
||||||
|
|
@ -649,17 +589,10 @@ class UI {
|
||||||
* @returns {boolean} User confirmation
|
* @returns {boolean} User confirmation
|
||||||
*/
|
*/
|
||||||
async confirm(message, defaultValue = false) {
|
async confirm(message, defaultValue = false) {
|
||||||
const inquirer = await getInquirer();
|
return await prompts.confirm({
|
||||||
const { confirmed } = await inquirer.prompt([
|
message,
|
||||||
{
|
default: defaultValue,
|
||||||
type: 'confirm',
|
});
|
||||||
name: 'confirmed',
|
|
||||||
message,
|
|
||||||
default: defaultValue,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return confirmed;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -753,10 +686,9 @@ class UI {
|
||||||
* Get module choices for selection
|
* Get module choices for selection
|
||||||
* @param {Set} installedModuleIds - Currently installed module IDs
|
* @param {Set} installedModuleIds - Currently installed module IDs
|
||||||
* @param {Object} customContentConfig - Custom content configuration
|
* @param {Object} customContentConfig - Custom content configuration
|
||||||
* @returns {Array} Module choices for inquirer
|
* @returns {Array} Module choices for prompt
|
||||||
*/
|
*/
|
||||||
async getModuleChoices(installedModuleIds, customContentConfig = null) {
|
async getModuleChoices(installedModuleIds, customContentConfig = null) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
const moduleChoices = [];
|
const moduleChoices = [];
|
||||||
const isNewInstallation = installedModuleIds.size === 0;
|
const isNewInstallation = installedModuleIds.size === 0;
|
||||||
|
|
||||||
|
|
@ -811,9 +743,9 @@ class UI {
|
||||||
if (allCustomModules.length > 0) {
|
if (allCustomModules.length > 0) {
|
||||||
// Add separator for custom content, all custom modules, and official content separator
|
// Add separator for custom content, all custom modules, and official content separator
|
||||||
moduleChoices.push(
|
moduleChoices.push(
|
||||||
new inquirer.Separator('── Custom Content ──'),
|
new choiceUtils.Separator('── Custom Content ──'),
|
||||||
...allCustomModules,
|
...allCustomModules,
|
||||||
new inquirer.Separator('── Official Content ──'),
|
new choiceUtils.Separator('── Official Content ──'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -837,44 +769,43 @@ class UI {
|
||||||
* @returns {Array} Selected module IDs
|
* @returns {Array} Selected module IDs
|
||||||
*/
|
*/
|
||||||
async selectModules(moduleChoices, defaultSelections = []) {
|
async selectModules(moduleChoices, defaultSelections = []) {
|
||||||
const inquirer = await getInquirer();
|
// Mark choices as checked based on defaultSelections
|
||||||
const moduleAnswer = await inquirer.prompt([
|
const choicesWithDefaults = moduleChoices.map((choice) => ({
|
||||||
{
|
...choice,
|
||||||
type: 'checkbox',
|
checked: defaultSelections.includes(choice.value),
|
||||||
name: 'modules',
|
}));
|
||||||
message: 'Select modules to install:',
|
|
||||||
choices: moduleChoices,
|
|
||||||
default: defaultSelections,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const selected = moduleAnswer.modules || [];
|
const selected = await prompts.multiselect({
|
||||||
|
message: `Select modules to install ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
||||||
|
choices: choicesWithDefaults,
|
||||||
|
required: false,
|
||||||
|
});
|
||||||
|
|
||||||
return selected;
|
return selected || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompt for directory selection
|
* Prompt for directory selection
|
||||||
* @returns {Object} Directory answer from inquirer
|
* @returns {Object} Directory answer from prompt
|
||||||
*/
|
*/
|
||||||
async promptForDirectory() {
|
async promptForDirectory() {
|
||||||
const inquirer = await getInquirer();
|
// Use sync validation because @clack/prompts doesn't support async validate
|
||||||
return await inquirer.prompt([
|
const directory = await prompts.text({
|
||||||
{
|
message: 'Installation directory:',
|
||||||
type: 'input',
|
default: process.cwd(),
|
||||||
name: 'directory',
|
placeholder: process.cwd(),
|
||||||
message: `Installation directory:`,
|
validate: (input) => this.validateDirectorySync(input),
|
||||||
default: process.cwd(),
|
});
|
||||||
validate: async (input) => this.validateDirectory(input),
|
|
||||||
filter: (input) => {
|
// Apply filter logic
|
||||||
// If empty, use the default
|
let filteredDir = directory;
|
||||||
if (!input || input.trim() === '') {
|
if (!filteredDir || filteredDir.trim() === '') {
|
||||||
return process.cwd();
|
filteredDir = process.cwd();
|
||||||
}
|
} else {
|
||||||
return this.expandUserPath(input);
|
filteredDir = this.expandUserPath(filteredDir);
|
||||||
},
|
}
|
||||||
},
|
|
||||||
]);
|
return { directory: filteredDir };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -915,45 +846,92 @@ class UI {
|
||||||
* @returns {boolean} Whether user confirmed
|
* @returns {boolean} Whether user confirmed
|
||||||
*/
|
*/
|
||||||
async confirmDirectory(directory) {
|
async confirmDirectory(directory) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
const dirExists = await fs.pathExists(directory);
|
const dirExists = await fs.pathExists(directory);
|
||||||
|
|
||||||
if (dirExists) {
|
if (dirExists) {
|
||||||
const confirmAnswer = await inquirer.prompt([
|
const proceed = await prompts.confirm({
|
||||||
{
|
message: 'Install to this directory?',
|
||||||
type: 'confirm',
|
default: true,
|
||||||
name: 'proceed',
|
});
|
||||||
message: `Install to this directory?`,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!confirmAnswer.proceed) {
|
if (!proceed) {
|
||||||
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
|
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return confirmAnswer.proceed;
|
return proceed;
|
||||||
} else {
|
} else {
|
||||||
// Ask for confirmation to create the directory
|
// Ask for confirmation to create the directory
|
||||||
const createConfirm = await inquirer.prompt([
|
const create = await prompts.confirm({
|
||||||
{
|
message: `The directory '${directory}' doesn't exist. Would you like to create it?`,
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'create',
|
});
|
||||||
message: `The directory '${directory}' doesn't exist. Would you like to create it?`,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!createConfirm.create) {
|
if (!create) {
|
||||||
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
|
console.log(chalk.yellow("\nLet's try again with a different path.\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
return createConfirm.create;
|
return create;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate directory path for installation
|
* Validate directory path for installation (sync version for clack prompts)
|
||||||
|
* @param {string} input - User input path
|
||||||
|
* @returns {string|undefined} Error message or undefined if valid
|
||||||
|
*/
|
||||||
|
validateDirectorySync(input) {
|
||||||
|
// Allow empty input to use the default
|
||||||
|
if (!input || input.trim() === '') {
|
||||||
|
return; // Empty means use default, undefined = valid for clack
|
||||||
|
}
|
||||||
|
|
||||||
|
let expandedPath;
|
||||||
|
try {
|
||||||
|
expandedPath = this.expandUserPath(input.trim());
|
||||||
|
} catch (error) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the path exists
|
||||||
|
const pathExists = fs.pathExistsSync(expandedPath);
|
||||||
|
|
||||||
|
if (!pathExists) {
|
||||||
|
// Find the first existing parent directory
|
||||||
|
const existingParent = this.findExistingParentSync(expandedPath);
|
||||||
|
|
||||||
|
if (!existingParent) {
|
||||||
|
return 'Cannot create directory: no existing parent directory found';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the existing parent is writable
|
||||||
|
try {
|
||||||
|
fs.accessSync(existingParent, fs.constants.W_OK);
|
||||||
|
// Path doesn't exist but can be created - will prompt for confirmation later
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
// Provide a detailed error message explaining both issues
|
||||||
|
return `Directory '${expandedPath}' does not exist and cannot be created: parent directory '${existingParent}' is not writable`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it exists, validate it's a directory and writable
|
||||||
|
const stat = fs.statSync(expandedPath);
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
return `Path exists but is not a directory: ${expandedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check write permissions
|
||||||
|
try {
|
||||||
|
fs.accessSync(expandedPath, fs.constants.W_OK);
|
||||||
|
} catch {
|
||||||
|
return `Directory is not writable: ${expandedPath}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate directory path for installation (async version)
|
||||||
* @param {string} input - User input path
|
* @param {string} input - User input path
|
||||||
* @returns {string|true} Error message or true if valid
|
* @returns {string|true} Error message or true if valid
|
||||||
*/
|
*/
|
||||||
|
|
@ -1009,7 +987,28 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the first existing parent directory
|
* Find the first existing parent directory (sync version)
|
||||||
|
* @param {string} targetPath - The path to check
|
||||||
|
* @returns {string|null} The first existing parent directory, or null if none found
|
||||||
|
*/
|
||||||
|
findExistingParentSync(targetPath) {
|
||||||
|
let currentPath = path.resolve(targetPath);
|
||||||
|
|
||||||
|
// Walk up the directory tree until we find an existing directory
|
||||||
|
while (currentPath !== path.dirname(currentPath)) {
|
||||||
|
// Stop at root
|
||||||
|
const parent = path.dirname(currentPath);
|
||||||
|
if (fs.pathExistsSync(parent)) {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
currentPath = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // No existing parent found (shouldn't happen in practice)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the first existing parent directory (async version)
|
||||||
* @param {string} targetPath - The path to check
|
* @param {string} targetPath - The path to check
|
||||||
* @returns {string|null} The first existing parent directory, or null if none found
|
* @returns {string|null} The first existing parent directory, or null if none found
|
||||||
*/
|
*/
|
||||||
|
|
@ -1071,7 +1070,7 @@ class UI {
|
||||||
* @sideeffects None - pure user input collection, no files written
|
* @sideeffects None - pure user input collection, no files written
|
||||||
* @edgecases Shows warning if user enables TTS but AgentVibes not detected
|
* @edgecases Shows warning if user enables TTS but AgentVibes not detected
|
||||||
* @calledby promptInstall() during installation flow, after core config, before IDE selection
|
* @calledby promptInstall() during installation flow, after core config, before IDE selection
|
||||||
* @calls checkAgentVibesInstalled(), inquirer.prompt(), chalk.green/yellow/dim()
|
* @calls checkAgentVibesInstalled(), prompts.select(), chalk.green/yellow/dim()
|
||||||
*
|
*
|
||||||
* AI NOTE: This prompt is strategically positioned in installation flow:
|
* AI NOTE: This prompt is strategically positioned in installation flow:
|
||||||
* - AFTER core config (user_name, etc)
|
* - AFTER core config (user_name, etc)
|
||||||
|
|
@ -1102,7 +1101,6 @@ class UI {
|
||||||
* - GitHub Issue: paulpreibisch/AgentVibes#36
|
* - GitHub Issue: paulpreibisch/AgentVibes#36
|
||||||
*/
|
*/
|
||||||
async promptAgentVibes(projectDir) {
|
async promptAgentVibes(projectDir) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
CLIUtils.displaySection('🎤 Voice Features', 'Enable TTS for multi-agent conversations');
|
CLIUtils.displaySection('🎤 Voice Features', 'Enable TTS for multi-agent conversations');
|
||||||
|
|
||||||
// Check if AgentVibes is already installed
|
// Check if AgentVibes is already installed
|
||||||
|
|
@ -1114,23 +1112,19 @@ class UI {
|
||||||
console.log(chalk.dim(' AgentVibes not detected'));
|
console.log(chalk.dim(' AgentVibes not detected'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const answers = await inquirer.prompt([
|
const enableTts = await prompts.confirm({
|
||||||
{
|
message: 'Enable Agents to Speak Out loud (powered by Agent Vibes? Claude Code only currently)',
|
||||||
type: 'confirm',
|
default: false,
|
||||||
name: 'enableTts',
|
});
|
||||||
message: 'Enable Agents to Speak Out loud (powered by Agent Vibes? Claude Code only currently)',
|
|
||||||
default: false, // Default to yes - recommended for best experience
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (answers.enableTts && !agentVibesInstalled) {
|
if (enableTts && !agentVibesInstalled) {
|
||||||
console.log(chalk.yellow('\n ⚠️ AgentVibes not installed'));
|
console.log(chalk.yellow('\n ⚠️ AgentVibes not installed'));
|
||||||
console.log(chalk.dim(' Install AgentVibes separately to enable TTS:'));
|
console.log(chalk.dim(' Install AgentVibes separately to enable TTS:'));
|
||||||
console.log(chalk.dim(' https://github.com/paulpreibisch/AgentVibes\n'));
|
console.log(chalk.dim(' https://github.com/paulpreibisch/AgentVibes\n'));
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: answers.enableTts,
|
enabled: enableTts,
|
||||||
alreadyInstalled: agentVibesInstalled,
|
alreadyInstalled: agentVibesInstalled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -1248,30 +1242,75 @@ class UI {
|
||||||
return existingInstall.ides || [];
|
return existingInstall.ides || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate custom content path synchronously
|
||||||
|
* @param {string} input - User input path
|
||||||
|
* @returns {string|undefined} Error message or undefined if valid
|
||||||
|
*/
|
||||||
|
validateCustomContentPathSync(input) {
|
||||||
|
// Allow empty input to cancel
|
||||||
|
if (!input || input.trim() === '') {
|
||||||
|
return; // Allow empty to exit
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Expand the path
|
||||||
|
const expandedPath = this.expandUserPath(input.trim());
|
||||||
|
|
||||||
|
// Check if path exists
|
||||||
|
if (!fs.pathExistsSync(expandedPath)) {
|
||||||
|
return 'Path does not exist';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a directory
|
||||||
|
const stat = fs.statSync(expandedPath);
|
||||||
|
if (!stat.isDirectory()) {
|
||||||
|
return 'Path must be a directory';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for module.yaml in the root
|
||||||
|
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
||||||
|
if (!fs.pathExistsSync(moduleYamlPath)) {
|
||||||
|
return 'Directory must contain a module.yaml file in the root';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to parse the module.yaml to get the module ID
|
||||||
|
try {
|
||||||
|
const yaml = require('yaml');
|
||||||
|
const content = fs.readFileSync(moduleYamlPath, 'utf8');
|
||||||
|
const moduleData = yaml.parse(content);
|
||||||
|
if (!moduleData.code) {
|
||||||
|
return 'module.yaml must contain a "code" field for the module ID';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return 'Invalid module.yaml file: ' + error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return; // Valid
|
||||||
|
} catch (error) {
|
||||||
|
return 'Error validating path: ' + error.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Prompt user for custom content source location
|
* Prompt user for custom content source location
|
||||||
* @returns {Object} Custom content configuration
|
* @returns {Object} Custom content configuration
|
||||||
*/
|
*/
|
||||||
async promptCustomContentSource() {
|
async promptCustomContentSource() {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
const customContentConfig = { hasCustomContent: true, sources: [] };
|
const customContentConfig = { hasCustomContent: true, sources: [] };
|
||||||
|
|
||||||
// Keep asking for more sources until user is done
|
// Keep asking for more sources until user is done
|
||||||
while (true) {
|
while (true) {
|
||||||
// First ask if user wants to add another module or continue
|
// First ask if user wants to add another module or continue
|
||||||
if (customContentConfig.sources.length > 0) {
|
if (customContentConfig.sources.length > 0) {
|
||||||
const { action } = await inquirer.prompt([
|
const action = await prompts.select({
|
||||||
{
|
message: 'Would you like to:',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'action',
|
{ name: 'Add another custom module', value: 'add' },
|
||||||
message: 'Would you like to:',
|
{ name: 'Continue with installation', value: 'continue' },
|
||||||
choices: [
|
],
|
||||||
{ name: 'Add another custom module', value: 'add' },
|
default: 'continue',
|
||||||
{ name: 'Continue with installation', value: 'continue' },
|
});
|
||||||
],
|
|
||||||
default: 'continue',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (action === 'continue') {
|
if (action === 'continue') {
|
||||||
break;
|
break;
|
||||||
|
|
@ -1282,57 +1321,11 @@ class UI {
|
||||||
let isValid = false;
|
let isValid = false;
|
||||||
|
|
||||||
while (!isValid) {
|
while (!isValid) {
|
||||||
const { path: inputPath } = await inquirer.prompt([
|
// Use sync validation because @clack/prompts doesn't support async validate
|
||||||
{
|
const inputPath = await prompts.text({
|
||||||
type: 'input',
|
message: 'Enter the path to your custom content folder (or press Enter to cancel):',
|
||||||
name: 'path',
|
validate: (input) => this.validateCustomContentPathSync(input),
|
||||||
message: 'Enter the path to your custom content folder (or press Enter to cancel):',
|
});
|
||||||
validate: async (input) => {
|
|
||||||
// Allow empty input to cancel
|
|
||||||
if (!input || input.trim() === '') {
|
|
||||||
return true; // Allow empty to exit
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Expand the path
|
|
||||||
const expandedPath = this.expandUserPath(input.trim());
|
|
||||||
|
|
||||||
// Check if path exists
|
|
||||||
if (!(await fs.pathExists(expandedPath))) {
|
|
||||||
return 'Path does not exist';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a directory
|
|
||||||
const stat = await fs.stat(expandedPath);
|
|
||||||
if (!stat.isDirectory()) {
|
|
||||||
return 'Path must be a directory';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for module.yaml in the root
|
|
||||||
const moduleYamlPath = path.join(expandedPath, 'module.yaml');
|
|
||||||
if (!(await fs.pathExists(moduleYamlPath))) {
|
|
||||||
return 'Directory must contain a module.yaml file in the root';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Try to parse the module.yaml to get the module ID
|
|
||||||
try {
|
|
||||||
const yaml = require('yaml');
|
|
||||||
const content = await fs.readFile(moduleYamlPath, 'utf8');
|
|
||||||
const moduleData = yaml.parse(content);
|
|
||||||
if (!moduleData.code) {
|
|
||||||
return 'module.yaml must contain a "code" field for the module ID';
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
return 'Invalid module.yaml file: ' + error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
return 'Error validating path: ' + error.message;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
// If user pressed Enter without typing anything, exit the loop
|
// If user pressed Enter without typing anything, exit the loop
|
||||||
if (!inputPath || inputPath.trim() === '') {
|
if (!inputPath || inputPath.trim() === '') {
|
||||||
|
|
@ -1364,14 +1357,10 @@ class UI {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask if user wants to add these to the installation
|
// Ask if user wants to add these to the installation
|
||||||
const { shouldInstall } = await inquirer.prompt([
|
const shouldInstall = await prompts.confirm({
|
||||||
{
|
message: `Install ${customContentConfig.sources.length} custom module(s) now?`,
|
||||||
type: 'confirm',
|
default: true,
|
||||||
name: 'shouldInstall',
|
});
|
||||||
message: `Install ${customContentConfig.sources.length} custom module(s) now?`,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (shouldInstall) {
|
if (shouldInstall) {
|
||||||
customContentConfig.selected = true;
|
customContentConfig.selected = true;
|
||||||
|
|
@ -1391,7 +1380,6 @@ class UI {
|
||||||
* @returns {Object} Result with selected custom modules and custom content config
|
* @returns {Object} Result with selected custom modules and custom content config
|
||||||
*/
|
*/
|
||||||
async handleCustomModulesInModifyFlow(directory, selectedModules) {
|
async handleCustomModulesInModifyFlow(directory, selectedModules) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
// Get existing installation to find custom modules
|
// Get existing installation to find custom modules
|
||||||
const { existingInstall } = await this.getExistingInstallation(directory);
|
const { existingInstall } = await this.getExistingInstallation(directory);
|
||||||
|
|
||||||
|
|
@ -1451,16 +1439,11 @@ class UI {
|
||||||
choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' });
|
choices.push({ name: 'Add new custom modules', value: 'add' }, { name: 'Cancel (no custom modules)', value: 'cancel' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { customAction } = await inquirer.prompt([
|
const customAction = await prompts.select({
|
||||||
{
|
message: cachedCustomModules.length > 0 ? 'What would you like to do with custom modules?' : 'Would you like to add custom modules?',
|
||||||
type: 'list',
|
choices: choices,
|
||||||
name: 'customAction',
|
default: cachedCustomModules.length > 0 ? 'keep' : 'add',
|
||||||
message:
|
});
|
||||||
cachedCustomModules.length > 0 ? 'What would you like to do with custom modules?' : 'Would you like to add custom modules?',
|
|
||||||
choices: choices,
|
|
||||||
default: cachedCustomModules.length > 0 ? 'keep' : 'add',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
switch (customAction) {
|
switch (customAction) {
|
||||||
case 'keep': {
|
case 'keep': {
|
||||||
|
|
@ -1472,21 +1455,18 @@ class UI {
|
||||||
|
|
||||||
case 'select': {
|
case 'select': {
|
||||||
// Let user choose which to keep
|
// Let user choose which to keep
|
||||||
const choices = cachedCustomModules.map((m) => ({
|
const selectChoices = cachedCustomModules.map((m) => ({
|
||||||
name: `${m.name} ${chalk.gray(`(${m.id})`)}`,
|
name: `${m.name} ${chalk.gray(`(${m.id})`)}`,
|
||||||
value: m.id,
|
value: m.id,
|
||||||
|
checked: m.checked,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const { keepModules } = await inquirer.prompt([
|
const keepModules = await prompts.multiselect({
|
||||||
{
|
message: `Select custom modules to keep ${chalk.dim('(↑/↓ navigate, SPACE select, ENTER confirm)')}:`,
|
||||||
type: 'checkbox',
|
choices: selectChoices,
|
||||||
name: 'keepModules',
|
required: false,
|
||||||
message: 'Select custom modules to keep:',
|
});
|
||||||
choices: choices,
|
result.selectedCustomModules = keepModules || [];
|
||||||
default: cachedCustomModules.filter((m) => m.checked).map((m) => m.id),
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
result.selectedCustomModules = keepModules;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1586,7 +1566,6 @@ class UI {
|
||||||
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
|
* @returns {Promise<boolean>} True if user wants to proceed, false if they cancel
|
||||||
*/
|
*/
|
||||||
async showOldAlphaVersionWarning(installedVersion, currentVersion, bmadFolderName) {
|
async showOldAlphaVersionWarning(installedVersion, currentVersion, bmadFolderName) {
|
||||||
const inquirer = await getInquirer();
|
|
||||||
const versionInfo = this.checkAlphaVersionAge(installedVersion, currentVersion);
|
const versionInfo = this.checkAlphaVersionAge(installedVersion, currentVersion);
|
||||||
|
|
||||||
// Also warn if version is unknown or can't be parsed (legacy/unsupported)
|
// Also warn if version is unknown or can't be parsed (legacy/unsupported)
|
||||||
|
|
@ -1627,26 +1606,20 @@ class UI {
|
||||||
console.log(chalk.yellow('─'.repeat(80)));
|
console.log(chalk.yellow('─'.repeat(80)));
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
||||||
const { proceed } = await inquirer.prompt([
|
const proceed = await prompts.select({
|
||||||
{
|
message: 'What would you like to do?',
|
||||||
type: 'list',
|
choices: [
|
||||||
name: 'proceed',
|
{
|
||||||
message: 'What would you like to do?',
|
name: 'Proceed with update anyway (may have issues)',
|
||||||
choices: [
|
value: 'proceed',
|
||||||
{
|
},
|
||||||
name: 'Proceed with update anyway (may have issues)',
|
{
|
||||||
value: 'proceed',
|
name: 'Cancel (recommended - do a fresh install instead)',
|
||||||
short: 'Proceed with update',
|
value: 'cancel',
|
||||||
},
|
},
|
||||||
{
|
],
|
||||||
name: 'Cancel (recommended - do a fresh install instead)',
|
default: 'cancel',
|
||||||
value: 'cancel',
|
});
|
||||||
short: 'Cancel installation',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'cancel',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (proceed === 'cancel') {
|
if (proceed === 'cancel') {
|
||||||
console.log('');
|
console.log('');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue