feat: implement UAT validation with self-healing fix loop

Add complete UAT validation workflow implementation:

- scripts/uat-validate.sh: Core UAT validation script with scenario
  classification, automated execution, and self-healing fix loop
- scripts/epic-execute.sh: Add metrics instrumentation for story
  execution tracking (init, update, finalize metrics)
- scripts/epic-chain.sh: Integrate UAT gate after each epic with
  configurable blocking mode, and add report generation phase

New workflow step documentation:
- step-01-load-uat.md: UAT document loading and parsing
- step-02-classify-scenarios.md: Scenario classification logic
- step-03-execute-scenarios.md: Automated scenario execution
- step-04-evaluate-gate.md: Gate evaluation and fix loop
- step-05-report-results.md: Metrics and signal output

Key features:
- Gate modes: quick (automatable only), full (+ semi-auto), skip
- Self-healing: spawns fresh Claude context for targeted fixes
- Metrics: YAML files track execution stats and UAT results
- Signals: parseable output for orchestration (UAT_GATE_RESULT, etc.)
- Handoffs: include UAT status and fix context references

Also fixes lint/format issues in pre-existing files.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Caleb 2026-01-05 19:24:18 -06:00
parent 6b1ca9333f
commit 15b7bb1ae6
14 changed files with 3899 additions and 155 deletions

File diff suppressed because it is too large Load Diff

526
epic-1-uat-sample.md Normal file
View File

@ -0,0 +1,526 @@
# User Acceptance Testing: Epic 1 - Foundation, CLI & Deployment Infrastructure
**Version:** 1.0
**Date:** January 2, 2026
**Epic:** Foundation, CLI & Deployment Infrastructure
**Status:** Ready for Testing
---
## 1. Overview
### What Was Built
Epic 1 delivers the foundation of the Heimdall Customer Management system. After completing this epic, administrators can:
- **Initialize a new Heimdall project** using a simple command-line tool
- **Configure connections** to Supabase (your database) and Resend (your email service)
- **Set up the database** with all necessary tables for job processing
- **Run a worker process** that continuously processes background jobs
- **Deploy to Railway** (a cloud hosting platform) for production use
- **Send test emails** to verify your email configuration is working
In plain terms: This is the "plumbing" that makes everything else possible. Once Epic 1 is working, you have a running system ready to handle customer management workflows.
---
## 2. Prerequisites
### Test Environment Requirements
Before starting UAT, ensure you have:
| Requirement | Details | How to Verify |
|-------------|---------|---------------|
| **Node.js 20.x** | JavaScript runtime | Run `node --version` - should show v20.x.x |
| **npm** | Package manager | Run `npm --version` - should show 9.x or higher |
| **Git** | Version control | Run `git --version` |
| **Terminal access** | Command line interface | macOS Terminal, Windows PowerShell, or Linux terminal |
### Required Accounts
| Service | Purpose | Sign-up URL |
|---------|---------|-------------|
| **Supabase** | Database hosting | <https://supabase.com> (free tier available) |
| **Resend** | Email delivery | <https://resend.com> (free tier: 100 emails/day) |
| **Railway** (optional) | Cloud deployment | <https://railway.app> (for production testing) |
### Credentials You Will Need
Before testing, gather these from your service dashboards:
1. **From Supabase Dashboard:**
- Project URL (e.g., `https://xxxxx.supabase.co`)
- Anon/Public Key (starts with `eyJ...`)
- Database Connection String (from Settings > Database > Connection string > URI)
2. **From Resend Dashboard:**
- API Key (starts with `re_...`)
- A verified sending domain (or use their default for testing)
3. **Your Information:**
- Admin email address (for receiving notifications and test emails)
- Workspace name (a label for your installation)
---
## 3. Test Scenarios
### Scenario 1: Project Initialization
**Goal:** Verify that you can create a new Heimdall project with the correct structure.
**Steps:**
1. Open your terminal application
2. Navigate to a folder where you want to create the project:
```
cd ~/Documents
```
3. Clone and set up the Heimdall project:
```
git clone <repository-url> heimdall-test
cd heimdall-test
npm install
npm run build
```
4. Verify the CLI is available:
```
npx heimdall --version
```
**Expected Results:**
- [ ] `npm install` completes without errors
- [ ] `npm run build` shows "5 packages built successfully" or similar
- [ ] `npx heimdall --version` displays a version number (e.g., `1.0.0`)
- [ ] `npx heimdall --help` shows available commands including `config`, `db`, `start`, `test-send`, `test-queue`
**Notes for Tester:**
_Record any error messages or unexpected behavior here:_
---
### Scenario 2: Configuration Setup
**Goal:** Create and validate your configuration file with Supabase and Resend credentials.
**Steps:**
1. Generate a configuration template:
```
npx heimdall config init
```
2. Open the created file `heimdall.config.yaml` in a text editor
3. Replace the placeholder values with your real credentials:
- Under `workspace:` - enter your workspace name and admin email
- Under `supabase:` - enter your Supabase URL, anon key, and database URL
- Under `resend:` - enter your Resend API key
4. Save the file
5. Validate your configuration:
```
npx heimdall config validate
```
**Expected Results:**
- [ ] `config init` creates a file named `heimdall.config.yaml`
- [ ] The file contains sections for `workspace`, `supabase`, `resend`, and `ai` (optional)
- [ ] `config validate` shows your workspace name and admin email (with secrets partially hidden)
- [ ] Validation shows "Configuration is valid" or similar success message
- [ ] No error messages appear about missing or invalid fields
**Notes for Tester:**
_If validation fails, record the error message:_
---
### Scenario 3: Database Migration
**Goal:** Set up the required database tables in your Supabase instance.
**Prerequisites:** Scenario 2 must be completed successfully.
**Steps:**
1. Run the database migration:
```
npx heimdall db migrate
```
2. Check the database status:
```
npx heimdall db status
```
**Expected Results:**
- [ ] `db migrate` completes with a success message (e.g., "pg-boss initialized successfully")
- [ ] `db status` shows:
- Database connected: Yes
- pg-boss schema: Exists
- Tables: job, schedule, subscription, version (or similar)
- [ ] Running `db migrate` a second time does NOT cause errors (idempotent)
**Verification in Supabase Dashboard:**
1. Log into your Supabase project
2. Go to the Table Editor
3. Look for a schema named `pgboss`
4. Verify tables exist: `job`, `schedule`, `subscription`, `version`
- [ ] pg-boss tables visible in Supabase Dashboard
**Notes for Tester:**
_Record database status output:_
---
### Scenario 4: Connection Validation (Detailed)
**Goal:** Verify both Supabase API and direct database connections work correctly.
**Prerequisites:** Scenario 3 must be completed successfully.
**Steps:**
1. Run the full validation:
```
npx heimdall config validate
```
**Expected Results:**
- [ ] Message: "Supabase API connected" with response time in milliseconds
- [ ] Message: "Operational DB connected" with response time in milliseconds
- [ ] Message: "Resend API connected" (if Resend key is configured)
- [ ] All connections show as successful (green checkmarks or similar)
**Notes for Tester:**
_Record connection times and any warnings:_
---
### Scenario 5: Worker Process Startup
**Goal:** Start the background worker that processes jobs.
**Prerequisites:** Scenario 3 must be completed successfully.
**Steps:**
1. Start the worker process:
```
npx heimdall start
```
2. Observe the output for approximately 30 seconds
3. Press `Ctrl+C` to stop the worker
**Expected Results:**
- [ ] Message appears: "Heimdall worker started, polling for jobs..." or similar
- [ ] No error messages during startup
- [ ] Worker continues running without crashing
- [ ] When you press `Ctrl+C`, message appears: "Heimdall worker shutting down gracefully..."
- [ ] Process exits cleanly (returns to command prompt)
**Notes for Tester:**
_Record startup messages:_
---
### Scenario 6: Job Queue Testing
**Goal:** Verify that jobs can be queued and processed by the worker.
**Prerequisites:** Scenario 5 must work (worker can start).
**Steps:**
1. Open **two terminal windows** side by side
2. In Terminal 1, start the worker:
```
npx heimdall start
```
3. Wait for the "polling for jobs" message
4. In Terminal 2, enqueue a test job:
```
npx heimdall test-queue
```
5. Watch Terminal 1 for job processing
6. Stop the worker in Terminal 1 with `Ctrl+C`
**Expected Results:**
- [ ] Terminal 2 shows: "Test job enqueued: test-job-{some-id}"
- [ ] Within 60 seconds, Terminal 1 shows: "Job executed: test-job-{same-id}"
- [ ] No errors in either terminal
- [ ] Job ID in Terminal 2 matches the ID in Terminal 1
**Notes for Tester:**
_Record time between enqueueing and execution:_
_Record job ID:_
---
### Scenario 7: Test Email Sending
**Goal:** Verify that Heimdall can send emails through Resend.
**Prerequisites:**
- Scenario 2 completed with valid Resend API key
- You have access to the email inbox specified
**Steps:**
1. Send a test email to yourself:
```
npx heimdall test-send --to your-email@example.com
```
(Replace with your actual email address)
2. Check your email inbox (including spam/junk folder)
**Expected Results:**
- [ ] Command shows: "Test email sent: {resend-message-id}"
- [ ] Email arrives in inbox within 2-5 minutes
- [ ] Email subject: "Heimdall Test Email"
- [ ] Email body confirms configuration is working
**Notes for Tester:**
_Record Resend message ID:_
_Time until email arrived:_
_Did email land in spam?:_
---
### Scenario 8: Health Endpoint (Local)
**Goal:** Verify the worker's health endpoint responds correctly.
**Prerequisites:** Worker can start (Scenario 5).
**Steps:**
1. Start the worker:
```
npx heimdall start
```
2. Open a web browser
3. Navigate to: `http://localhost:3000/health`
4. Stop the worker with `Ctrl+C`
**Expected Results:**
- [ ] Browser shows JSON response
- [ ] Response contains: `"status": "healthy"`
- [ ] Response contains: `"queue": "connected"`
- [ ] Response contains: `"uptime": {some-number}`
**Sample Expected Response:**
```json
{
"status": "healthy",
"queue": "connected",
"uptime": 45
}
```
**Notes for Tester:**
_Copy the actual response here:_
---
### Scenario 9: Railway Deployment (Optional)
**Goal:** Deploy Heimdall to Railway for production use.
**Prerequisites:**
- Railway account created
- All previous scenarios pass locally
**Steps:**
1. Log into Railway Dashboard
2. Create a new project from the GitHub repository
3. Add environment variables in Railway settings:
- `DATABASE_URL` = your Supabase connection string
- `SUPABASE_URL` = your Supabase project URL
- `SUPABASE_ANON_KEY` = your Supabase anon key
- `RESEND_API_KEY` = your Resend API key
- `ADMIN_EMAIL` = your admin email
- `WORKSPACE_NAME` = your workspace name
4. Deploy the service
5. Once deployed, access the health endpoint at your Railway URL + `/health`
**Expected Results:**
- [ ] Deployment completes without build errors
- [ ] Service shows as "Running" in Railway dashboard
- [ ] Health endpoint at `https://your-app.railway.app/health` returns:
- `"status": "healthy"`
- `"queue": "connected"`
- [ ] Logs show "Heimdall worker started, polling for jobs..."
**Notes for Tester:**
_Railway deployment URL:_
_Any deployment warnings or notes:_
---
## 4. Success Criteria
### Minimum Requirements for Sign-off
All of the following must pass for Epic 1 to be accepted:
| # | Criteria | Scenario | Status |
|---|----------|----------|--------|
| 1 | Project builds successfully | Scenario 1 | [ ] Pass / [ ] Fail |
| 2 | CLI commands are accessible | Scenario 1 | [ ] Pass / [ ] Fail |
| 3 | Configuration file is created correctly | Scenario 2 | [ ] Pass / [ ] Fail |
| 4 | Configuration validation works | Scenario 2 | [ ] Pass / [ ] Fail |
| 5 | Database migration completes | Scenario 3 | [ ] Pass / [ ] Fail |
| 6 | Database migration is idempotent | Scenario 3 | [ ] Pass / [ ] Fail |
| 7 | Supabase API connection validated | Scenario 4 | [ ] Pass / [ ] Fail |
| 8 | Operational DB connection validated | Scenario 4 | [ ] Pass / [ ] Fail |
| 9 | Worker process starts and polls | Scenario 5 | [ ] Pass / [ ] Fail |
| 10 | Worker shuts down gracefully | Scenario 5 | [ ] Pass / [ ] Fail |
| 11 | Test jobs are processed | Scenario 6 | [ ] Pass / [ ] Fail |
| 12 | Jobs processed within 60 seconds | Scenario 6 | [ ] Pass / [ ] Fail |
| 13 | Test emails are sent and received | Scenario 7 | [ ] Pass / [ ] Fail |
| 14 | Health endpoint responds correctly | Scenario 8 | [ ] Pass / [ ] Fail |
### Optional (Recommended)
| # | Criteria | Scenario | Status |
|---|----------|----------|--------|
| 15 | Railway deployment successful | Scenario 9 | [ ] Pass / [ ] Fail / [ ] Skipped |
| 16 | Production health endpoint works | Scenario 9 | [ ] Pass / [ ] Fail / [ ] Skipped |
---
## 5. Known Limitations
The following are expected behaviors, not bugs:
1. **Error messages for invalid credentials** are intentionally detailed to help debugging
2. **First database migration** may take 10-30 seconds as pg-boss creates multiple tables
3. **Test emails** may land in spam for unverified domains
4. **Worker polling interval** is 5 seconds, so jobs may take up to 5 seconds to start processing
5. **Config validate** shows partial secrets (first/last 4 characters) for verification purposes
---
## 6. Troubleshooting Guide
### Common Issues and Solutions
| Symptom | Likely Cause | Solution |
|---------|--------------|----------|
| "Command not found: heimdall" | Build not completed | Run `npm run build` first |
| "Cannot connect to database" | Wrong db_url | Check Supabase connection string format |
| "Invalid API key" | Resend key incorrect | Regenerate key in Resend dashboard |
| "ECONNREFUSED" | Database not accessible | Check Supabase project is running, check IP restrictions |
| Worker crashes on startup | Missing configuration | Run `heimdall config validate` first |
| Email not received | Domain not verified | Verify domain in Resend or check spam folder |
---
## 7. Sign-off Section
### Testing Summary
| Item | Value |
|------|-------|
| **Tester Name** | _________________________ |
| **Test Date** | _________________________ |
| **Environment** | [ ] Local / [ ] Railway / [ ] Both |
| **Node.js Version** | _________________________ |
| **Operating System** | _________________________ |
### Test Results Summary
| Category | Passed | Failed | Skipped |
|----------|--------|--------|---------|
| Required Scenarios (1-8) | ___ / 8 | ___ | ___ |
| Optional Scenarios (9) | ___ / 1 | ___ | ___ |
| Success Criteria (1-14) | ___ / 14 | ___ | ___ |
### Issues Found
| Issue # | Scenario | Description | Severity |
|---------|----------|-------------|----------|
| | | | [ ] Blocker / [ ] Major / [ ] Minor |
| | | | [ ] Blocker / [ ] Major / [ ] Minor |
| | | | [ ] Blocker / [ ] Major / [ ] Minor |
### Final Decision
- [ ] **APPROVED** - All required criteria pass, ready for production
- [ ] **APPROVED WITH CONDITIONS** - Minor issues noted, can proceed
- [ ] **NOT APPROVED** - Blocker issues must be resolved
### Signatures
| Role | Name | Signature | Date |
|------|------|-----------|------|
| **Tester** | | | |
| **Product Owner** | | | |
| **Technical Lead** | | | |
---
## 8. Appendix
### A. CLI Command Reference
| Command | Purpose |
|---------|---------|
| `heimdall --version` | Display version number |
| `heimdall --help` | Show available commands |
| `heimdall config init` | Create configuration file |
| `heimdall config validate` | Validate configuration |
| `heimdall db migrate` | Set up database tables |
| `heimdall db status` | Check database status |
| `heimdall start` | Start worker process |
| `heimdall test-queue` | Enqueue a test job |
| `heimdall test-send --to EMAIL` | Send a test email |
### B. Configuration File Template
```yaml
# heimdall.config.yaml
workspace:
name: "my-workspace"
admin_email: "admin@example.com"
supabase:
url: "https://xxxxx.supabase.co"
anon_key: "eyJ..."
db_url: "postgresql://postgres:password@db.xxxxx.supabase.co:5432/postgres"
resend:
api_key: "re_..."
# Optional AI configuration
ai:
provider: "anthropic"
api_key: "sk-ant-..."
model: "claude-3-haiku-20240307"
```
### C. Environment Variables for Railway
| Variable | Required | Description |
|----------|----------|-------------|
| `DATABASE_URL` | Yes | PostgreSQL connection string |
| `SUPABASE_URL` | Yes | Supabase project URL |
| `SUPABASE_ANON_KEY` | Yes | Supabase anonymous key |
| `RESEND_API_KEY` | Yes | Resend API key |
| `ADMIN_EMAIL` | Yes | Admin notification email |
| `WORKSPACE_NAME` | No | Workspace identifier |
| `PORT` | No | Server port (default: 3000) |
---
*Document generated: January 2, 2026*
*Epic 1: Foundation, CLI & Deployment Infrastructure*

View File

@ -9,15 +9,25 @@
# ./epic-chain.sh 36 37 38 --dry-run --verbose
# ./epic-chain.sh 36 37 38 --analyze-only
# ./epic-chain.sh 36 37 38 --start-from 37
# ./epic-chain.sh 36 37 38 --uat-gate=full --uat-blocking
#
# Options:
# --dry-run Show what would be executed without running
# --analyze-only Run analysis phase only, don't execute
# --verbose Show detailed output
# --start-from ID Start from a specific epic (skip earlier ones)
# --skip-done Skip epics/stories with Status: Done
# --no-handoff Don't generate context handoffs between epics
# --no-combined-uat Skip combined UAT generation at end
# --dry-run Show what would be executed without running
# --analyze-only Run analysis phase only, don't execute
# --verbose Show detailed output
# --start-from ID Start from a specific epic (skip earlier ones)
# --skip-done Skip epics/stories with Status: Done
# --no-handoff Don't generate context handoffs between epics
# --no-combined-uat Skip combined UAT generation at end
#
# UAT Gate Options:
# --uat-gate=MODE UAT validation mode: quick|full|skip (default: quick)
# --uat-blocking Halt chain if UAT fails (default: continue)
# --uat-retries=N Max fix attempts per epic (default: 2)
# --no-uat Disable UAT validation gate entirely
#
# Report Options:
# --no-report Skip chain execution report generation
#
set -e
@ -48,6 +58,19 @@ CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# UAT Gate Configuration
UAT_GATE_ENABLED="${UAT_GATE_ENABLED:-true}"
UAT_GATE_MODE="${UAT_GATE_MODE:-quick}"
UAT_MAX_RETRIES="${UAT_MAX_RETRIES:-2}"
UAT_BLOCKING="${UAT_BLOCKING:-false}"
# Metrics Configuration
METRICS_DIR="$SPRINT_ARTIFACTS_DIR/metrics"
# Report Configuration
GENERATE_REPORT="${GENERATE_REPORT:-true}"
CHAIN_REPORT_FILE="$SPRINT_ARTIFACTS_DIR/chain-execution-report.md"
# =============================================================================
# Helper Functions
# =============================================================================
@ -87,6 +110,80 @@ log_section() {
echo -e "${BOLD}───────────────────────────────────────────────────────────${NC}"
}
# Helper function to create basic report if Claude fails
create_basic_report() {
local end_time_iso=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
local duration_formatted="${DURATION}s"
if [ $DURATION -gt 3600 ]; then
duration_formatted="$((DURATION / 3600))h $((DURATION % 3600 / 60))m"
elif [ $DURATION -gt 60 ]; then
duration_formatted="$((DURATION / 60))m $((DURATION % 60))s"
fi
cat > "$CHAIN_REPORT_FILE" << EOF
# Epic Chain Execution Report
## Executive Summary
**Execution Method:** BMAD Epic Chain (automated AI-driven development)
**Status:** $([ $FAILED_EPICS -eq 0 ] && echo "COMPLETE" || echo "PARTIAL")
| Metric | Value |
|--------|-------|
| Total Epics | ${#EPIC_IDS[@]} |
| Completed | $COMPLETED_EPICS |
| Failed | $FAILED_EPICS |
| Skipped | $SKIPPED_EPICS |
| Duration | $duration_formatted |
---
## Timeline
| Epic | Status |
|------|--------|
EOF
for epic_id in "${EPIC_IDS[@]}"; do
local status="Unknown"
local metrics_file="$METRICS_DIR/epic-${epic_id}-metrics.yaml"
if [ -f "$metrics_file" ]; then
if command -v yq >/dev/null 2>&1; then
local completed=$(yq '.stories.completed // 0' "$metrics_file")
local failed=$(yq '.stories.failed // 0' "$metrics_file")
if [ "$failed" -gt 0 ]; then
status="Partial ($completed completed, $failed failed)"
else
status="Complete ($completed stories)"
fi
fi
fi
echo "| Epic $epic_id | $status |" >> "$CHAIN_REPORT_FILE"
done
cat >> "$CHAIN_REPORT_FILE" << EOF
---
## Artifacts
| Artifact | Location |
|----------|----------|
| Chain Plan | $CHAIN_PLAN_FILE |
| Metrics | $METRICS_DIR/ |
| UAT Documents | $UAT_DIR/ |
| Handoffs | $HANDOFF_DIR/ |
| Log | $LOG_FILE |
---
*Report generated: $end_time_iso*
*BMAD Method Epic Chain*
EOF
log_success "Basic report created: $CHAIN_REPORT_FILE"
}
# =============================================================================
# Argument Parsing
# =============================================================================
@ -130,6 +227,26 @@ while [[ $# -gt 0 ]]; do
NO_COMBINED_UAT=true
shift
;;
--uat-gate=*)
UAT_GATE_MODE="${1#*=}"
shift
;;
--uat-blocking)
UAT_BLOCKING=true
shift
;;
--no-uat)
UAT_GATE_ENABLED=false
shift
;;
--uat-retries=*)
UAT_MAX_RETRIES="${1#*=}"
shift
;;
--no-report)
GENERATE_REPORT=false
shift
;;
-*)
echo "Unknown option: $1"
exit 1
@ -149,15 +266,25 @@ if [ ${#EPIC_IDS[@]} -eq 0 ]; then
echo " $0 36 37 38 --dry-run # Show what would happen"
echo " $0 36 37 38 --analyze-only # Just analyze, don't execute"
echo " $0 36 37 38 --start-from 37 # Resume from epic 37"
echo " $0 36 37 38 --uat-gate=full # Run full UAT validation after each epic"
echo ""
echo "Options:"
echo " --dry-run Show execution plan without running"
echo " --analyze-only Analyze dependencies only"
echo " --verbose Detailed output"
echo " --start-from ID Start from specific epic"
echo " --skip-done Skip completed stories"
echo " --no-handoff Skip context handoffs between epics"
echo " --no-combined-uat Skip combined UAT at end"
echo " --dry-run Show execution plan without running"
echo " --analyze-only Analyze dependencies only"
echo " --verbose Detailed output"
echo " --start-from ID Start from specific epic"
echo " --skip-done Skip completed stories"
echo " --no-handoff Skip context handoffs between epics"
echo " --no-combined-uat Skip combined UAT at end"
echo ""
echo "UAT Gate Options:"
echo " --uat-gate=MODE UAT validation mode: quick|full|skip (default: quick)"
echo " --uat-blocking Halt chain if UAT fails (default: continue)"
echo " --uat-retries=N Max fix attempts per epic (default: 2)"
echo " --no-uat Disable UAT validation gate entirely"
echo ""
echo "Report Options:"
echo " --no-report Skip chain execution report generation"
exit 1
fi
@ -395,6 +522,60 @@ for current_idx in "${!EXECUTION_ORDER[@]}"; do
else
if $exec_cmd; then
log_success "Epic $epic_id completed"
# Run UAT validation if enabled
if [ "$UAT_GATE_ENABLED" = true ]; then
log_section "UAT Validation Gate: Epic $epic_id"
uat_cmd="$SCRIPT_DIR/uat-validate.sh $epic_id --gate-mode=$UAT_GATE_MODE --max-retries=$UAT_MAX_RETRIES"
if [ "$VERBOSE" = true ]; then
uat_cmd="$uat_cmd --verbose"
fi
log "Running: $uat_cmd"
# Capture UAT result
uat_output=""
uat_exit_code=0
uat_output=$($uat_cmd 2>&1) || uat_exit_code=$?
echo "$uat_output" >> "$LOG_FILE"
# Parse result signals
if echo "$uat_output" | grep -q "UAT_GATE_RESULT: PASS"; then
log_success "UAT validation passed for Epic $epic_id"
# Update metrics file if it exists
epic_metrics_file="$METRICS_DIR/epic-${epic_id}-metrics.yaml"
if [ -f "$epic_metrics_file" ] && command -v yq >/dev/null 2>&1; then
yq -i '.validation.gate_executed = true' "$epic_metrics_file"
yq -i '.validation.gate_status = "PASS"' "$epic_metrics_file"
fi
else
log_error "UAT validation failed for Epic $epic_id"
# Update metrics file if it exists
epic_metrics_file="$METRICS_DIR/epic-${epic_id}-metrics.yaml"
if [ -f "$epic_metrics_file" ] && command -v yq >/dev/null 2>&1; then
yq -i '.validation.gate_executed = true' "$epic_metrics_file"
yq -i '.validation.gate_status = "FAIL"' "$epic_metrics_file"
fi
# Extract fix attempts from output
fix_attempts=$(echo "$uat_output" | grep -oE "UAT_FIX_ATTEMPTS: [0-9]+" | grep -oE "[0-9]+" || echo "0")
[ "$VERBOSE" = true ] && log "Fix attempts: $fix_attempts"
if [ "$UAT_BLOCKING" = true ]; then
log_error "UAT blocking enabled - halting chain"
((FAILED_EPICS++))
break
else
log_warn "UAT blocking disabled - continuing to next epic"
fi
fi
fi
((COMPLETED_EPICS++))
# Generate handoff for next epic
@ -408,6 +589,26 @@ for current_idx in "${!EXECUTION_ORDER[@]}"; do
log "Generating context handoff: Epic $epic_id → Epic $next_epic"
story_count=${EPIC_STORIES_LIST[$current_idx]}
# Determine UAT validation status for handoff
uat_status="Not executed"
uat_fix_info=""
if [ "$UAT_GATE_ENABLED" = true ]; then
if echo "$uat_output" | grep -q "UAT_GATE_RESULT: PASS"; then
uat_status="PASS"
local fix_count=$(echo "$uat_output" | grep -oE "UAT_FIX_ATTEMPTS: [0-9]+" | grep -oE "[0-9]+" || echo "0")
if [ "$fix_count" -gt 0 ]; then
uat_status="PASS (after $fix_count fix attempts)"
uat_fix_info="Self-healing fixes were applied. Review fix contexts at:
\`docs/sprint-artifacts/uat-fixes/epic-${epic_id}-fix-context-*.md\`"
fi
else
uat_status="FAIL (non-blocking)"
uat_fix_info="UAT validation failed but chain continued (non-blocking mode).
Review failures at: \`docs/sprint-artifacts/uat-fixes/epic-${epic_id}-fix-context-*.md\`"
fi
fi
cat > "$handoff_file" << EOF
# Epic $epic_id → Epic $next_epic Handoff
@ -418,6 +619,11 @@ $(date '+%Y-%m-%d %H:%M:%S')
Epic $epic_id has been completed. Key context for Epic $next_epic:
### Implementation Status
- **Stories:** Completed via epic-execute workflow
- **UAT Validation:** $uat_status
- **Metrics:** \`$METRICS_DIR/epic-${epic_id}-metrics.yaml\`
### Patterns Established
- Review code changes in Epic $epic_id for established patterns
- Check \`docs/stories/${epic_id}-*\` for implementation details
@ -425,9 +631,17 @@ Epic $epic_id has been completed. Key context for Epic $next_epic:
### Files Modified
$(git diff --name-only HEAD~${story_count} HEAD 2>/dev/null | head -20 || echo "Unable to determine - check git log")
### UAT Document
- Location: \`docs/uat/epic-${epic_id}-uat.md\`
- Contains test scenarios for regression testing
$([ -n "$uat_fix_info" ] && echo "### Fix Context
$uat_fix_info")
### Notes for Next Epic
- Continue following patterns established in this epic
- Reference UAT document at \`docs/uat/epic-${epic_id}-uat.md\` for context
- Ensure changes don't break Epic $epic_id functionality
- Reference UAT document for integration points
EOF
log_success "Handoff saved to: $handoff_file"
@ -508,6 +722,108 @@ EOF
log_success "Combined UAT saved to: $combined_uat_file"
fi
# =============================================================================
# Phase 7: Generate Chain Execution Report
# =============================================================================
if [ "$GENERATE_REPORT" = true ] && [ "$DRY_RUN" = false ]; then
log_section "Generating Chain Execution Report"
# Check if metrics files exist
metrics_found=0
for epic_id in "${EPIC_IDS[@]}"; do
if [ -f "$METRICS_DIR/epic-${epic_id}-metrics.yaml" ]; then
((metrics_found++))
fi
done
if [ $metrics_found -eq 0 ]; then
log_warn "No metrics files found - skipping report generation"
else
log "Found $metrics_found metrics files"
# Determine workflow path (installed vs source)
WORKFLOW_PATH=""
if [ -d "$BMAD_DIR/bmm/workflows/4-implementation/epic-chain" ]; then
WORKFLOW_PATH="$BMAD_DIR/bmm/workflows/4-implementation/epic-chain"
elif [ -d "$PROJECT_ROOT/src/modules/bmm/workflows/4-implementation/epic-chain" ]; then
WORKFLOW_PATH="$PROJECT_ROOT/src/modules/bmm/workflows/4-implementation/epic-chain"
fi
# Build report generation prompt
report_prompt="You are Bob, the Scrum Master, generating a chain execution report.
## Your Task
Generate a comprehensive chain execution report for the completed epic chain.
## Configuration
- Chain Plan: $CHAIN_PLAN_FILE
- Metrics Folder: $METRICS_DIR
- Output File: $CHAIN_REPORT_FILE
- Stories Location: $STORIES_DIR
- UAT Location: $UAT_DIR
- Epics Location: $EPICS_DIR
- Handoffs Location: $HANDOFF_DIR
## Epics in Chain
${EPIC_IDS[*]}
## Process
1. Read the chain plan file to understand the epic sequence
2. For each epic, load the metrics file from: $METRICS_DIR/epic-{id}-metrics.yaml
3. Aggregate metrics across all epics:
- Total duration
- Story counts (total, completed, failed, skipped)
- UAT gate results
- Issues encountered
4. Generate the report following the template structure
## Report Structure
Generate a markdown report with these sections:
- Executive Summary (status, counts, duration)
- Timeline (epic-by-epic execution details)
- What Was Built (brief per-epic summary)
- Issues Encountered (aggregated from metrics)
- UAT Validation Summary (gate results, fix attempts)
- Artifacts Generated (list generated files)
- Conclusion
## Output
Write the report to: $CHAIN_REPORT_FILE
When complete, output exactly:
REPORT_GENERATED: $CHAIN_REPORT_FILE"
log "Invoking report generator..."
# Execute report generation
report_result=$(claude --dangerously-skip-permissions -p "$report_prompt" 2>&1) || true
echo "$report_result" >> "$LOG_FILE"
if echo "$report_result" | grep -q "REPORT_GENERATED"; then
log_success "Report generated: $CHAIN_REPORT_FILE"
# Stage report file
git add "$CHAIN_REPORT_FILE" 2>/dev/null || true
else
log_warn "Report generation may not have completed cleanly"
# If Claude didn't generate it, create a basic report
if [ ! -f "$CHAIN_REPORT_FILE" ]; then
log "Creating basic report from metrics..."
create_basic_report
fi
fi
fi
fi
# =============================================================================
# Summary
# =============================================================================
@ -527,6 +843,10 @@ echo " Artifacts:"
echo " - Chain Plan: $CHAIN_PLAN_FILE"
echo " - Handoffs: $HANDOFF_DIR/"
echo " - UAT Documents: $UAT_DIR/"
echo " - Metrics: $METRICS_DIR/"
if [ -f "$CHAIN_REPORT_FILE" ]; then
echo " - Report: $CHAIN_REPORT_FILE"
fi
echo " - Log: $LOG_FILE"
echo ""
@ -537,5 +857,12 @@ fi
log_success "All epics completed successfully"
echo ""
echo "Next step: Review UAT documents and run manual testing"
if [ -f "$CHAIN_REPORT_FILE" ]; then
echo "Next steps:"
echo " 1. Review execution report: $CHAIN_REPORT_FILE"
echo " 2. Run UAT validation for each epic"
echo " 3. Execute manual test scenarios"
else
echo "Next step: Review UAT documents and run manual testing"
fi
echo ""

View File

@ -63,6 +63,121 @@ log_warn() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $1" >> "$LOG_FILE"
}
# =============================================================================
# Metrics Functions
# =============================================================================
METRICS_DIR=""
METRICS_FILE=""
init_metrics() {
METRICS_DIR="$SPRINT_ARTIFACTS_DIR/metrics"
METRICS_FILE="$METRICS_DIR/epic-${EPIC_ID}-metrics.yaml"
mkdir -p "$METRICS_DIR"
local start_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
cat > "$METRICS_FILE" << EOF
epic_id: "$EPIC_ID"
execution:
start_time: "$start_time"
end_time: ""
duration_seconds: 0
stories:
total: 0
completed: 0
failed: 0
skipped: 0
validation:
gate_executed: false
gate_status: "PENDING"
fix_attempts: 0
issues: []
EOF
log "Metrics initialized: $METRICS_FILE"
}
update_story_metrics() {
local status="$1" # completed|failed|skipped
if [ -z "$METRICS_FILE" ] || [ ! -f "$METRICS_FILE" ]; then
return
fi
# Check if yq is available for YAML manipulation
if command -v yq >/dev/null 2>&1; then
case "$status" in
completed) yq -i '.stories.completed += 1' "$METRICS_FILE" ;;
failed) yq -i '.stories.failed += 1' "$METRICS_FILE" ;;
skipped) yq -i '.stories.skipped += 1' "$METRICS_FILE" ;;
esac
else
# Fallback: log warning (metrics will be finalized at end)
[ "$VERBOSE" = true ] && log_warn "yq not found - metrics update deferred"
fi
}
add_metrics_issue() {
local story_id="$1"
local issue_type="$2"
local message="$3"
if [ -z "$METRICS_FILE" ] || [ ! -f "$METRICS_FILE" ]; then
return
fi
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
if command -v yq >/dev/null 2>&1; then
yq -i ".issues += [{\"story\": \"$story_id\", \"type\": \"$issue_type\", \"message\": \"$message\", \"timestamp\": \"$timestamp\"}]" "$METRICS_FILE"
fi
}
finalize_metrics() {
local total_stories="$1"
local completed="$2"
local failed="$3"
local skipped="$4"
local duration="$5"
if [ -z "$METRICS_FILE" ] || [ ! -f "$METRICS_FILE" ]; then
return
fi
local end_time=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
if command -v yq >/dev/null 2>&1; then
yq -i ".execution.end_time = \"$end_time\"" "$METRICS_FILE"
yq -i ".execution.duration_seconds = $duration" "$METRICS_FILE"
yq -i ".stories.total = $total_stories" "$METRICS_FILE"
yq -i ".stories.completed = $completed" "$METRICS_FILE"
yq -i ".stories.failed = $failed" "$METRICS_FILE"
yq -i ".stories.skipped = $skipped" "$METRICS_FILE"
else
# Fallback: rewrite the file with final values
cat > "$METRICS_FILE" << EOF
epic_id: "$EPIC_ID"
execution:
start_time: "$EPIC_START_TIME"
end_time: "$end_time"
duration_seconds: $duration
stories:
total: $total_stories
completed: $completed
failed: $failed
skipped: $skipped
validation:
gate_executed: false
gate_status: "PENDING"
fix_attempts: 0
issues: []
EOF
fi
log "Metrics finalized: $METRICS_FILE"
}
# =============================================================================
# Argument Parsing
# =============================================================================
@ -142,6 +257,11 @@ log "Project root: $PROJECT_ROOT"
mkdir -p "$UAT_DIR"
mkdir -p "$SPRINTS_DIR"
# Initialize metrics collection
EPIC_START_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
EPIC_START_SECONDS=$(date +%s)
init_metrics
# Find epic file (supports both epic-39-*.md and epic-039-*.md formats)
EPIC_FILE=""
# Pad epic ID with leading zero for 3-digit format (e.g., 40 -> 040)
@ -554,6 +674,7 @@ for story_file in "${STORIES[@]}"; do
else
log_warn "Skipping $story_id (waiting for $START_FROM)"
((SKIPPED++))
update_story_metrics "skipped"
continue
fi
fi
@ -563,6 +684,7 @@ for story_file in "${STORIES[@]}"; do
if grep -q "^Status:.*Done" "$story_file" 2>/dev/null; then
log_warn "Skipping $story_id (Status: Done)"
((SKIPPED++))
update_story_metrics "skipped"
continue
fi
fi
@ -576,22 +698,27 @@ for story_file in "${STORIES[@]}"; do
if ! execute_dev_phase "$story_file"; then
log_error "Dev phase failed for $story_id"
((FAILED++))
update_story_metrics "failed"
add_metrics_issue "$story_id" "dev_phase_failed" "Development phase did not complete"
continue
fi
# REVIEW PHASE (Context 2 - Fresh)
if [ "$SKIP_REVIEW" = false ]; then
if ! execute_review_phase "$story_file"; then
log_error "Review phase failed for $story_id"
((FAILED++))
update_story_metrics "failed"
add_metrics_issue "$story_id" "review_failed" "Code review phase failed"
continue
fi
fi
# COMMIT
commit_story "$story_id"
((COMPLETED++))
update_story_metrics "completed"
log_success "Story complete: $story_id ($COMPLETED/${#STORIES[@]})"
done
@ -613,6 +740,9 @@ generate_uat
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
# Finalize metrics with final counts
finalize_metrics "${#STORIES[@]}" "$COMPLETED" "$FAILED" "$SKIPPED" "$DURATION"
echo ""
log "=========================================="
log "EPIC EXECUTION COMPLETE"
@ -628,6 +758,7 @@ echo ""
echo " Deliverables:"
echo " - Stories: $STORIES_DIR/"
echo " - UAT: $UAT_DIR/epic-${EPIC_ID}-uat.md"
echo " - Metrics: $METRICS_FILE"
echo " - Log: $LOG_FILE"
echo ""

827
scripts/uat-validate.sh Executable file
View File

@ -0,0 +1,827 @@
#!/bin/bash
#
# BMAD UAT Validate - Automated UAT Scenario Execution with Self-Healing Fix Loop
#
# Usage: ./uat-validate.sh <epic-id> [options]
#
# Options:
# --gate-mode=MODE Validation mode: quick|full|skip (default: quick)
# --max-retries=N Max fix attempts before halt (default: 2)
# --skip-manual Skip manual-only scenarios (default: skip)
# --verbose Show detailed output
# --dry-run Show what would be executed without running
# --timeout=SECONDS Timeout per scenario (default: 30)
#
# Exit Codes:
# 0 - UAT PASS (all automatable scenarios passed)
# 1 - UAT FAIL (fixable, retries remain or self-heal succeeded)
# 2 - UAT FAIL (max retries exceeded)
#
set -e
# =============================================================================
# Section 1: Configuration
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
BMAD_DIR="$PROJECT_ROOT/.bmad"
UAT_DIR="$PROJECT_ROOT/docs/uat"
SPRINT_ARTIFACTS_DIR="$PROJECT_ROOT/docs/sprint-artifacts"
METRICS_DIR="$SPRINT_ARTIFACTS_DIR/metrics"
FIX_DIR="$SPRINT_ARTIFACTS_DIR/uat-fixes"
STORIES_DIR="$PROJECT_ROOT/docs/stories"
LOG_FILE="/tmp/bmad-uat-validate-$$.log"
# Default configuration
UAT_GATE_MODE="quick"
MAX_RETRIES=2
SKIP_MANUAL=true
VERBOSE=false
DRY_RUN=false
TIMEOUT_SECONDS=30
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
NC='\033[0m' # No Color
# =============================================================================
# Section 2: Helper Functions
# =============================================================================
log() {
echo -e "${BLUE}[UAT]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
log_success() {
echo -e "${GREEN}[PASS]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [PASS] $1" >> "$LOG_FILE"
}
log_error() {
echo -e "${RED}[FAIL]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [FAIL] $1" >> "$LOG_FILE"
}
log_warn() {
echo -e "${YELLOW}[!]${NC} $1"
echo "[$(date '+%Y-%m-%d %H:%M:%S')] [WARN] $1" >> "$LOG_FILE"
}
log_section() {
echo ""
echo -e "${BOLD}───────────────────────────────────────────────────────────${NC}"
echo -e "${BOLD} $1${NC}"
echo -e "${BOLD}───────────────────────────────────────────────────────────${NC}"
}
log_header() {
echo ""
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN}${BOLD} $1${NC}"
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════════════${NC}"
echo ""
}
# =============================================================================
# Section 3: Argument Parsing
# =============================================================================
EPIC_ID=""
while [[ $# -gt 0 ]]; do
case $1 in
--gate-mode=*)
UAT_GATE_MODE="${1#*=}"
shift
;;
--max-retries=*)
MAX_RETRIES="${1#*=}"
shift
;;
--skip-manual)
SKIP_MANUAL=true
shift
;;
--include-manual)
SKIP_MANUAL=false
shift
;;
--verbose)
VERBOSE=true
shift
;;
--dry-run)
DRY_RUN=true
shift
;;
--timeout=*)
TIMEOUT_SECONDS="${1#*=}"
shift
;;
-*)
echo "Unknown option: $1"
exit 1
;;
*)
EPIC_ID="$1"
shift
;;
esac
done
if [ -z "$EPIC_ID" ]; then
echo "Usage: $0 <epic-id> [options]"
echo ""
echo "Options:"
echo " --gate-mode=MODE Validation mode: quick|full|skip (default: quick)"
echo " --max-retries=N Max fix attempts before halt (default: 2)"
echo " --skip-manual Skip manual-only scenarios (default)"
echo " --include-manual Include manual scenarios in checklist"
echo " --verbose Detailed output"
echo " --dry-run Show what would be executed"
echo " --timeout=SECONDS Timeout per scenario (default: 30)"
echo ""
echo "Exit Codes:"
echo " 0 - UAT PASS"
echo " 1 - UAT FAIL (fixable)"
echo " 2 - UAT FAIL (max retries exceeded)"
exit 1
fi
# Validate gate mode
if [[ ! "$UAT_GATE_MODE" =~ ^(quick|full|skip)$ ]]; then
echo "Invalid gate mode: $UAT_GATE_MODE"
echo "Valid modes: quick, full, skip"
exit 1
fi
# =============================================================================
# Section 4: UAT Document Loading
# =============================================================================
load_uat_document() {
local epic_id="$1"
# Find UAT document (try multiple patterns)
UAT_FILE=""
for pattern in "epic-${epic_id}-uat.md" "epic-0${epic_id}-uat.md" "${epic_id}-uat.md"; do
found=$(find "$UAT_DIR" -name "$pattern" 2>/dev/null | head -1)
if [ -n "$found" ]; then
UAT_FILE="$found"
break
fi
done
if [ -z "$UAT_FILE" ] || [ ! -f "$UAT_FILE" ]; then
log_error "UAT document not found for Epic $epic_id"
log_error "Searched in: $UAT_DIR"
log_error "Expected: epic-${epic_id}-uat.md"
return 1
fi
log "Found UAT document: $UAT_FILE"
# Validate structure - check for scenarios section
if ! grep -qE "^##.*[Ss]cenario|^##.*[Tt]est|^##.*[Cc]riteria" "$UAT_FILE"; then
log_warn "UAT document may not have standard scenario sections"
fi
# Count scenario blocks (lines starting with ### or numbered items under Test Scenarios)
SCENARIO_COUNT=$(grep -cE "^###|^[0-9]+\." "$UAT_FILE" 2>/dev/null || echo "0")
log "Found approximately $SCENARIO_COUNT scenario entries"
return 0
}
# =============================================================================
# Section 5: Scenario Classification
# =============================================================================
# Arrays to store classified scenarios
declare -a AUTOMATABLE_SCENARIOS
declare -a SEMI_AUTO_SCENARIOS
declare -a MANUAL_SCENARIOS
classify_scenarios() {
local uat_file="$1"
# Reset arrays
AUTOMATABLE_SCENARIOS=()
SEMI_AUTO_SCENARIOS=()
MANUAL_SCENARIOS=()
# Read the UAT file and extract scenario blocks
local current_scenario=""
local current_name=""
local in_scenario=false
local scenario_num=0
while IFS= read -r line; do
# Detect scenario headers (### or numbered items)
if [[ "$line" =~ ^###[[:space:]]*(.*) ]] || [[ "$line" =~ ^([0-9]+)\.[[:space:]]+(.*) ]]; then
# Save previous scenario if exists
if [ -n "$current_scenario" ]; then
classify_single_scenario "$scenario_num" "$current_name" "$current_scenario"
fi
# Start new scenario
((scenario_num++))
if [[ "$line" =~ ^###[[:space:]]*(.*) ]]; then
current_name="${BASH_REMATCH[1]}"
else
current_name="${BASH_REMATCH[2]}"
fi
current_scenario="$line"
in_scenario=true
elif [ "$in_scenario" = true ]; then
# Continue accumulating scenario content
current_scenario+=$'\n'"$line"
fi
done < "$uat_file"
# Handle last scenario
if [ -n "$current_scenario" ]; then
classify_single_scenario "$scenario_num" "$current_name" "$current_scenario"
fi
log "Classification complete:"
log " Automatable: ${#AUTOMATABLE_SCENARIOS[@]}"
log " Semi-auto: ${#SEMI_AUTO_SCENARIOS[@]}"
log " Manual: ${#MANUAL_SCENARIOS[@]}"
}
classify_single_scenario() {
local id="$1"
local name="$2"
local content="$3"
# Check for automatable indicators
if echo "$content" | grep -qiE 'npx|npm run|yarn|node |curl |wget |pytest|jest|vitest|--version|/health|/api/|exit code|returns [0-9]|\.sh |bash '; then
# Extract command from code block if present
local cmd=""
cmd=$(echo "$content" | grep -oE '`[^`]+`' | head -1 | tr -d '`')
if [ -z "$cmd" ]; then
cmd=$(echo "$content" | grep -oE 'npx [a-zA-Z0-9_-]+.*|npm run [a-zA-Z0-9_:-]+.*|curl [^[:space:]]+.*' | head -1)
fi
AUTOMATABLE_SCENARIOS+=("$id|$name|$cmd")
[ "$VERBOSE" = true ] && log " [AUTO] Scenario $id: $name"
# Check for semi-automated indicators
elif echo "$content" | grep -qiE 'test-send|email|inbox|check your|verify.*manually|setup.*first|start.*server'; then
SEMI_AUTO_SCENARIOS+=("$id|$name|")
[ "$VERBOSE" = true ] && log " [SEMI] Scenario $id: $name"
# Everything else is manual
else
MANUAL_SCENARIOS+=("$id|$name|")
[ "$VERBOSE" = true ] && log " [MANUAL] Scenario $id: $name"
fi
}
# =============================================================================
# Section 6: Scenario Execution
# =============================================================================
# Arrays to store results
declare -a PASSED_SCENARIOS
declare -a FAILED_SCENARIOS
declare -a FAILED_DETAILS
execute_scenarios() {
local gate_mode="$1"
# Reset results
PASSED_SCENARIOS=()
FAILED_SCENARIOS=()
FAILED_DETAILS=()
# Skip mode - pass automatically
if [ "$gate_mode" = "skip" ]; then
log "Gate mode: skip - bypassing scenario execution"
echo "UAT_GATE_RESULT: PASS"
echo "UAT_SCENARIOS_PASSED: 0/0 (skipped)"
return 0
fi
# Select scenarios based on gate mode
local scenarios_to_run=()
if [ "$gate_mode" = "quick" ]; then
scenarios_to_run=("${AUTOMATABLE_SCENARIOS[@]}")
elif [ "$gate_mode" = "full" ]; then
scenarios_to_run=("${AUTOMATABLE_SCENARIOS[@]}" "${SEMI_AUTO_SCENARIOS[@]}")
fi
if [ ${#scenarios_to_run[@]} -eq 0 ]; then
log_warn "No automatable scenarios found - gate passes by default"
echo "UAT_GATE_RESULT: PASS"
echo "UAT_SCENARIOS_PASSED: 0/0 (none automatable)"
return 0
fi
log_section "Executing ${#scenarios_to_run[@]} Scenarios"
for scenario_entry in "${scenarios_to_run[@]}"; do
IFS='|' read -r scenario_id scenario_name scenario_cmd <<< "$scenario_entry"
execute_single_scenario "$scenario_id" "$scenario_name" "$scenario_cmd"
done
# Report results
local total=${#scenarios_to_run[@]}
local passed=${#PASSED_SCENARIOS[@]}
local failed=${#FAILED_SCENARIOS[@]}
echo ""
log "Results: $passed/$total passed"
if [ $failed -eq 0 ]; then
return 0
else
return 1
fi
}
execute_single_scenario() {
local scenario_id="$1"
local scenario_name="$2"
local scenario_cmd="$3"
echo ""
log "Scenario $scenario_id: $scenario_name"
# If no command extracted, try to infer from name
if [ -z "$scenario_cmd" ]; then
log_warn " No command detected - marking as manual verification needed"
FAILED_SCENARIOS+=("$scenario_id")
FAILED_DETAILS+=("$scenario_id|$scenario_name|No automatable command found|manual|1")
return 1
fi
if [ "$VERBOSE" = true ]; then
log " Command: $scenario_cmd"
fi
if [ "$DRY_RUN" = true ]; then
echo " [DRY RUN] Would execute: $scenario_cmd"
PASSED_SCENARIOS+=("$scenario_id")
return 0
fi
# Execute with timeout
local start_time=$(date +%s%N)
local output=""
local exit_code=0
local stderr_file="/tmp/uat-stderr-$$.txt"
# Run command with timeout
set +e
if command -v timeout >/dev/null 2>&1; then
output=$(timeout "$TIMEOUT_SECONDS" bash -c "$scenario_cmd" 2>"$stderr_file")
exit_code=$?
# timeout returns 124 on timeout
if [ $exit_code -eq 124 ]; then
exit_code=124
fi
else
# macOS fallback using perl
output=$(perl -e 'alarm shift @ARGV; exec @ARGV' "$TIMEOUT_SECONDS" bash -c "$scenario_cmd" 2>"$stderr_file")
exit_code=$?
fi
set -e
local end_time=$(date +%s%N)
local duration_ms=$(( (end_time - start_time) / 1000000 ))
local stderr=""
[ -f "$stderr_file" ] && stderr=$(cat "$stderr_file")
rm -f "$stderr_file"
# Evaluate result
if [ $exit_code -eq 0 ]; then
log_success " Scenario $scenario_id: PASS (${duration_ms}ms)"
PASSED_SCENARIOS+=("$scenario_id")
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Scenario $scenario_id PASS: $scenario_cmd" >> "$LOG_FILE"
elif [ $exit_code -eq 124 ]; then
log_error " Scenario $scenario_id: FAIL (timeout after ${TIMEOUT_SECONDS}s)"
FAILED_SCENARIOS+=("$scenario_id")
FAILED_DETAILS+=("$scenario_id|$scenario_name|$scenario_cmd|timeout|$exit_code|$output|$stderr")
else
log_error " Scenario $scenario_id: FAIL (exit code $exit_code)"
if [ -n "$stderr" ] && [ "$VERBOSE" = true ]; then
echo " Error: $stderr"
fi
FAILED_SCENARIOS+=("$scenario_id")
FAILED_DETAILS+=("$scenario_id|$scenario_name|$scenario_cmd|error|$exit_code|$output|$stderr")
fi
return $exit_code
}
# =============================================================================
# Section 7: Gate Evaluation
# =============================================================================
evaluate_gate() {
local total=${#AUTOMATABLE_SCENARIOS[@]}
local passed=${#PASSED_SCENARIOS[@]}
local failed=${#FAILED_SCENARIOS[@]}
log_section "Gate Evaluation"
if [ $failed -eq 0 ]; then
log_success "All automatable scenarios passed"
return 0
else
log_error "$failed scenario(s) failed"
return 1
fi
}
# =============================================================================
# Section 8: Self-Healing Loop
# =============================================================================
generate_fix_context() {
local epic_id="$1"
local attempt="$2"
mkdir -p "$FIX_DIR"
local fix_file="$FIX_DIR/epic-${epic_id}-fix-context-${attempt}.md"
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Find template
local template="$PROJECT_ROOT/src/modules/bmm/workflows/5-validation/uat-validate/uat-fix-context-template.md"
if [ -f "$template" ]; then
# Render template with basic variable substitution
sed -e "s/{epic_id}/$epic_id/g" \
-e "s/{attempt}/$attempt/g" \
-e "s/{timestamp}/$timestamp/g" \
-e "s/{max_retries}/$MAX_RETRIES/g" \
-e "s/{next_attempt}/$((attempt + 1))/g" \
-e "s/{failure_count}/${#FAILED_SCENARIOS[@]}/g" \
-e "s|{uat_doc_path}|$UAT_FILE|g" \
"$template" > "$fix_file"
else
# Create minimal fix context without template
cat > "$fix_file" << EOF
# UAT Fix Context - Epic $epic_id (Attempt $attempt)
**Generated:** $timestamp
**Epic:** $epic_id
**Gate Result:** FAIL (${#PASSED_SCENARIOS[@]}/${#AUTOMATABLE_SCENARIOS[@]} scenarios passed)
---
## Summary
This document contains the context needed to fix UAT failures for Epic $epic_id.
**Failures to fix:** ${#FAILED_SCENARIOS[@]}
**Fix attempt:** $attempt of $MAX_RETRIES
---
EOF
fi
# Append failed scenarios details
echo "" >> "$fix_file"
echo "## Failed Scenarios" >> "$fix_file"
echo "" >> "$fix_file"
for detail in "${FAILED_DETAILS[@]}"; do
IFS='|' read -r scenario_id scenario_name cmd error_type exit_code output stderr <<< "$detail"
cat >> "$fix_file" << EOF
### Scenario $scenario_id: $scenario_name
**Command Executed:**
\`\`\`bash
$cmd
\`\`\`
**Error Type:** $error_type
**Exit Code:** $exit_code
**Output:**
\`\`\`
$output
\`\`\`
**Error Output:**
\`\`\`
$stderr
\`\`\`
---
EOF
done
# Add context references section
cat >> "$fix_file" << EOF
## Context References
The following files provide additional context for fixing these failures:
| File | Purpose |
|------|---------|
| \`$UAT_FILE\` | Full UAT document with all scenarios |
| \`$STORIES_DIR/${epic_id}-*\` | Story files with acceptance criteria |
| \`$METRICS_DIR/epic-${epic_id}-metrics.yaml\` | Execution metrics |
## Fix Instructions
Address the failures above in priority order. For each fix:
1. **Analyze** - Understand why the scenario failed
2. **Locate** - Find the relevant code files
3. **Fix** - Implement the minimum change to resolve the failure
4. **Verify** - Run the scenario command locally to confirm fix
5. **Commit** - Use message format: \`fix(epic-$epic_id): {description}\`
### Constraints
- Only fix the identified failures - do not refactor unrelated code
- Run the specific failing commands to verify each fix
- Run project tests after all fixes: \`npm test\`
- If a fix requires changes that would break other scenarios, document the tradeoff
## After Fixing
Once all fixes are committed, the UAT validation will automatically re-run.
- **If all pass:** Epic continues to next phase
- **If failures remain:** Another fix context will be generated (attempt $((attempt + 1)))
- **If max retries exceeded:** Chain halts for human intervention
---
*Generated by UAT Validate Workflow*
*BMAD Method - Epic Chain Self-Healing*
*Fix Context: epic-${epic_id}-fix-context-${attempt}.md*
EOF
log "Fix context generated: $fix_file"
echo "$fix_file"
}
run_quick_dev_fix() {
local fix_context_file="$1"
local epic_id="$2"
local attempt="$3"
log "Spawning quick-dev fix session (attempt $attempt/$MAX_RETRIES)"
local fix_prompt="You are Barry, the Quick Flow Solo Dev.
Load and process this fix context document:
$fix_context_file
Your task:
1. Read the failed scenarios and error details from the fix context
2. Analyze root cause for each failure
3. Implement targeted fixes
4. Run the failing commands to verify fixes
5. Stage changes: git add -A
6. Commit with message: fix(epic-${epic_id}): UAT fix #${attempt}
Constraints:
- Only fix the identified failures
- Do not refactor unrelated code
- Run tests after fixes
When done, output exactly:
FIX_COMPLETE: {number_fixed}/${#FAILED_SCENARIOS[@]}"
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] Would spawn Claude for fixes with prompt:"
echo " Fix context: $fix_context_file"
return 0
fi
# Execute in isolated context
local result
result=$(claude --dangerously-skip-permissions -p "$fix_prompt" 2>&1) || true
echo "$result" >> "$LOG_FILE"
if echo "$result" | grep -q "FIX_COMPLETE"; then
log_success "Quick-dev fix session completed"
return 0
else
log_warn "Quick-dev fix session may not have completed cleanly"
return 1
fi
}
self_healing_loop() {
local epic_id="$1"
local attempt=0
while [ $attempt -lt $MAX_RETRIES ]; do
((attempt++))
log_section "Self-Healing Fix Loop (Attempt $attempt/$MAX_RETRIES)"
# Generate fix context
local fix_file
fix_file=$(generate_fix_context "$epic_id" "$attempt")
# Run quick-dev fix
if ! run_quick_dev_fix "$fix_file" "$epic_id" "$attempt"; then
log_warn "Fix attempt $attempt may have issues"
fi
# Re-run validation
log "Re-validating after fix attempt $attempt..."
# Reset and re-execute
PASSED_SCENARIOS=()
FAILED_SCENARIOS=()
FAILED_DETAILS=()
if execute_scenarios "$UAT_GATE_MODE"; then
log_success "UAT passed after fix attempt $attempt"
return 0
fi
log_warn "UAT still failing after attempt $attempt"
done
log_error "Max retries ($MAX_RETRIES) exceeded"
return 2
}
# =============================================================================
# Section 9: Output Signals and Metrics
# =============================================================================
update_metrics() {
local epic_id="$1"
local gate_status="$2"
local fix_attempts="$3"
mkdir -p "$METRICS_DIR"
local metrics_file="$METRICS_DIR/epic-${epic_id}-metrics.yaml"
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# Check if yq is available for YAML manipulation
if command -v yq >/dev/null 2>&1; then
if [ -f "$metrics_file" ]; then
yq -i ".validation.gate_executed = true" "$metrics_file"
yq -i ".validation.gate_status = \"$gate_status\"" "$metrics_file"
yq -i ".validation.fix_attempts = $fix_attempts" "$metrics_file"
yq -i ".validation.scenarios_passed = ${#PASSED_SCENARIOS[@]}" "$metrics_file"
yq -i ".validation.scenarios_failed = ${#FAILED_SCENARIOS[@]}" "$metrics_file"
yq -i ".validation.timestamp = \"$timestamp\"" "$metrics_file"
else
# Create new metrics file
cat > "$metrics_file" << EOF
epic_id: "$epic_id"
validation:
gate_executed: true
gate_status: "$gate_status"
fix_attempts: $fix_attempts
scenarios_passed: ${#PASSED_SCENARIOS[@]}
scenarios_failed: ${#FAILED_SCENARIOS[@]}
timestamp: "$timestamp"
EOF
fi
else
# Fallback: append to file or create new
if [ ! -f "$metrics_file" ]; then
cat > "$metrics_file" << EOF
epic_id: "$epic_id"
validation:
gate_executed: true
gate_status: "$gate_status"
fix_attempts: $fix_attempts
scenarios_passed: ${#PASSED_SCENARIOS[@]}
scenarios_failed: ${#FAILED_SCENARIOS[@]}
timestamp: "$timestamp"
EOF
else
# Simple append for validation section
log_warn "yq not found - metrics update may be incomplete"
fi
fi
log "Metrics updated: $metrics_file"
}
output_signals() {
local gate_status="$1"
local fix_attempts="$2"
local total=${#AUTOMATABLE_SCENARIOS[@]}
local passed=${#PASSED_SCENARIOS[@]}
echo ""
echo "UAT_GATE_RESULT: $gate_status"
echo "UAT_FIX_ATTEMPTS: $fix_attempts"
echo "UAT_SCENARIOS_PASSED: $passed/$total"
}
print_summary() {
local gate_status="$1"
local fix_attempts="$2"
log_header "UAT VALIDATION COMPLETE"
echo " Epic: $EPIC_ID"
echo " Gate Mode: $UAT_GATE_MODE"
echo " Gate Result: $gate_status"
echo ""
echo " Scenarios:"
echo " Automatable: ${#AUTOMATABLE_SCENARIOS[@]}"
echo " Semi-automated: ${#SEMI_AUTO_SCENARIOS[@]}"
echo " Manual: ${#MANUAL_SCENARIOS[@]}"
echo ""
echo " Results:"
echo " Passed: ${#PASSED_SCENARIOS[@]}"
echo " Failed: ${#FAILED_SCENARIOS[@]}"
echo " Fix Attempts: $fix_attempts"
echo ""
echo " Artifacts:"
echo " Log: $LOG_FILE"
echo " UAT Document: $UAT_FILE"
if [ ${#FAILED_SCENARIOS[@]} -gt 0 ] && [ -d "$FIX_DIR" ]; then
echo " Fix Contexts: $FIX_DIR/"
fi
echo ""
}
# =============================================================================
# Main Execution
# =============================================================================
log_header "UAT VALIDATION: Epic $EPIC_ID"
log "Gate mode: $UAT_GATE_MODE"
log "Max retries: $MAX_RETRIES"
log "Timeout: ${TIMEOUT_SECONDS}s"
# Ensure directories exist
mkdir -p "$METRICS_DIR"
mkdir -p "$FIX_DIR"
# Step 1: Load UAT document
log_section "Loading UAT Document"
if ! load_uat_document "$EPIC_ID"; then
echo "UAT_GATE_RESULT: FAIL"
echo "UAT_FIX_ATTEMPTS: 0"
echo "UAT_SCENARIOS_PASSED: 0/0"
exit 1
fi
# Step 2: Classify scenarios
log_section "Classifying Scenarios"
classify_scenarios "$UAT_FILE"
# Step 3: Execute scenarios
if ! execute_scenarios "$UAT_GATE_MODE"; then
# Gate failed - check if we should try self-healing
if [ "$DRY_RUN" = false ] && [ $MAX_RETRIES -gt 0 ]; then
if ! self_healing_loop "$EPIC_ID"; then
# Max retries exceeded
update_metrics "$EPIC_ID" "FAIL" "$MAX_RETRIES"
output_signals "FAIL" "$MAX_RETRIES"
print_summary "FAIL" "$MAX_RETRIES"
exit 2
fi
else
# No self-healing or dry-run
update_metrics "$EPIC_ID" "FAIL" "0"
output_signals "FAIL" "0"
print_summary "FAIL" "0"
exit 1
fi
fi
# Step 4: Gate passed
FINAL_ATTEMPTS=0
if [ ${#FAILED_SCENARIOS[@]} -gt 0 ]; then
# Passed after retries
FINAL_ATTEMPTS=$((MAX_RETRIES - $(ls -1 "$FIX_DIR"/epic-${EPIC_ID}-fix-context-*.md 2>/dev/null | wc -l) + 1))
fi
update_metrics "$EPIC_ID" "PASS" "$FINAL_ATTEMPTS"
output_signals "PASS" "$FINAL_ATTEMPTS"
print_summary "PASS" "$FINAL_ATTEMPTS"
log_success "UAT validation passed for Epic $EPIC_ID"
exit 0

View File

@ -9,10 +9,10 @@ epic_file: "{epic_file_path}"
# Execution timing
execution:
start_time: "{start_timestamp}" # ISO 8601 format
end_time: "{end_timestamp}" # ISO 8601 format
duration_seconds: 0 # Calculated from start/end
duration_formatted: "" # Human readable, e.g., "1.5 hours"
start_time: "{start_timestamp}" # ISO 8601 format
end_time: "{end_timestamp}" # ISO 8601 format
duration_seconds: 0 # Calculated from start/end
duration_formatted: "" # Human readable, e.g., "1.5 hours"
# Story counts
stories:
@ -44,13 +44,13 @@ uat:
# Validation gate results
validation:
gate_executed: false
gate_mode: "quick" # quick | full | skip
gate_mode: "quick" # quick | full | skip
timestamp: ""
results:
passed: 0
failed: 0
skipped: 0
gate_status: "PENDING" # PASS | FAIL | PENDING | SKIPPED
gate_status: "PENDING" # PASS | FAIL | PENDING | SKIPPED
blocking_issues: []
# Self-healing fix loop tracking
@ -76,19 +76,19 @@ issues: []
# Dependencies (from chain plan)
dependencies:
requires: [] # Epic IDs this epic depends on
enables: [] # Epic IDs that depend on this epic
requires: [] # Epic IDs this epic depends on
enables: [] # Epic IDs that depend on this epic
# Git information
git:
commits: 0 # Number of commits for this epic
branch: "" # Branch used (if feature branch)
first_commit: "" # SHA of first commit
last_commit: "" # SHA of last commit
commits: 0 # Number of commits for this epic
branch: "" # Branch used (if feature branch)
first_commit: "" # SHA of first commit
last_commit: "" # SHA of last commit
# Estimated token usage
tokens:
estimated_calls: 0 # stories * 2 (dev + review)
estimated_input: 0 # estimated_calls * 8000
estimated_output: 0 # estimated_calls * 4000
estimated_total: 0 # input + output
estimated_calls: 0 # stories * 2 (dev + review)
estimated_input: 0 # estimated_calls * 8000
estimated_output: 0 # estimated_calls * 4000
estimated_total: 0 # input + output

View File

@ -7,13 +7,13 @@
execution:
# Automatically commit after each story completes
auto_commit: true
# Run tests before transitioning to review phase
run_tests_before_review: true
# Maximum retries for failed phases
max_retries: 2
# Timeout per phase in seconds (0 = no timeout)
phase_timeout: 0
@ -24,21 +24,21 @@ review:
# standard - Block on acceptance criteria, security, major quality
# strict - Block on any quality issue, no auto-fixing
mode: standard
# Allow reviewer to auto-fix issues
auto_fix_enabled: true
# Require all tests pass before approval
require_passing_tests: true
# Issue fix policy thresholds
fix_policy:
# Always fix HIGH severity issues
always_fix_high: true
# Fix MEDIUM severity if total issues exceed this threshold
medium_fix_threshold: 5
# Never auto-fix LOW severity (just document)
fix_low: false
@ -46,13 +46,13 @@ review:
context:
# Clear context between phases (recommended)
isolate_phases: true
# Include architecture doc in dev context
include_architecture: true
# Include PRD in dev context
include_prd: false
# Max lines of code context to include
max_code_context: 500
@ -60,16 +60,16 @@ context:
uat:
# Generate UAT after all stories complete
enabled: true
# UAT output location (relative to project root)
output_dir: docs/uat
# Include edge case scenarios
include_edge_cases: true
# Minimum scenarios to generate
min_scenarios: 3
# Maximum scenarios to generate
max_scenarios: 10
@ -77,13 +77,13 @@ uat:
git:
# Commit message prefix
commit_prefix: "feat"
# Include epic ID in commit message
include_epic_in_commit: true
# Push after each commit
auto_push: false
# Branch naming pattern (use {epic_id} and {story_id} as placeholders)
branch_pattern: "feature/epic-{epic_id}"
@ -91,10 +91,10 @@ git:
parallel:
# Enable parallel story execution
enabled: false
# Maximum concurrent stories
max_concurrent: 3
# Only parallelize independent stories (no dependencies)
respect_dependencies: true
@ -102,13 +102,13 @@ parallel:
logging:
# Log level: debug | info | warn | error
level: info
# Save execution log
save_log: true
# Log output location
log_dir: docs/sprints
# Include Claude responses in log (verbose)
log_responses: false
@ -116,9 +116,9 @@ logging:
notifications:
# Notify on completion
on_complete: false
# Notify on failure
on_failure: false
# Notification method: slack | email | none
method: none

View File

@ -124,8 +124,8 @@ This epic is **successful** when a user can:
| Metric | Value |
|--------|-------|
| Scenarios Tested | __ / __ |
| Scenarios Passed | __ / __ |
| Scenarios Tested | \_\_ / \_\_ |
| Scenarios Passed | \_\_ / \_\_ |
| Critical Issues | |
| Major Issues | |
| Minor Issues | |

View File

@ -0,0 +1,102 @@
# Step 1: Load UAT Document
## Purpose
Load and validate the UAT document for the specified epic, extracting all test scenarios for classification and execution.
## Inputs
| Input | Source | Required |
|-------|--------|----------|
| epic_id | CLI argument | Yes |
| uat_dir | Configuration | Yes (default: `docs/uat`) |
## Process
### 1.1 Locate UAT Document
Search for UAT document using these patterns in order:
1. `{uat_dir}/epic-{epic_id}-uat.md`
2. `{uat_dir}/epic-0{epic_id}-uat.md` (zero-padded)
3. `{uat_dir}/{epic_id}-uat.md`
**If not found:** Exit with error code 1 and message:
```
UAT document not found for Epic {epic_id}
Searched in: {uat_dir}
Expected: epic-{epic_id}-uat.md
```
### 1.2 Validate Document Structure
Confirm document contains at least one of these sections:
- `## Test Scenarios`
- `## Acceptance Criteria`
- `## Scenarios`
- `## Success Criteria`
**Warning if missing:** Log warning but continue (scenarios may be inline)
### 1.3 Parse Scenarios
Extract scenario blocks by detecting:
- Headers starting with `###` (individual scenario titles)
- Numbered items `1.`, `2.`, etc. under scenario sections
- Checkbox items `- [ ]` in criteria sections
For each scenario, extract:
- **Scenario ID**: Numeric index or explicit ID
- **Scenario Name**: Title text
- **Steps**: Given/When/Then or numbered steps
- **Verification Command**: Code block or CLI reference (if present)
- **Expected Result**: Success criteria text
### 1.4 Build Scenario List
Create structured list of scenarios:
```yaml
scenarios:
- id: 1
name: "Project Initialization"
steps:
- "Run npx heimdall init"
- "Verify config file created"
verification_command: "npx heimdall --version"
expected_result: "displays a version number"
raw_content: |
### 1. Project Initialization
...
```
## Outputs
| Output | Location | Description |
|--------|----------|-------------|
| scenario_list | Memory/State | Array of parsed scenario objects |
| scenario_count | Console | Total number of scenarios found |
| uat_file_path | State | Path to loaded UAT document |
## Completion Signal
```
UAT_LOADED: {scenario_count} scenarios from {uat_file_path}
```
## Error Handling
| Error | Action |
|-------|--------|
| File not found | Exit 1 with clear error message |
| Empty file | Exit 1 with "UAT document is empty" |
| No scenarios detected | Log warning, return empty list (gate passes by default) |
| Parse error | Log warning for specific section, continue with partial results |
## Example Output
```
[UAT] Loading UAT document for Epic 1
[UAT] Found: docs/uat/epic-1-uat.md
[UAT] Parsed 9 scenarios
UAT_LOADED: 9 scenarios from docs/uat/epic-1-uat.md
```

View File

@ -0,0 +1,156 @@
# Step 2: Classify Scenarios
## Purpose
Categorize each scenario by its executability level to determine which can be automated, which need partial automation, and which require manual verification.
## Inputs
| Input | Source | Required |
|-------|--------|----------|
| scenario_list | Step 1 | Yes |
## Process
### 2.1 Classification Categories
| Classification | Description | Gate Behavior |
|----------------|-------------|---------------|
| **Automatable** | Can be fully executed via shell command | Execute and verify |
| **Semi-automated** | Requires setup, then automated verification | Execute with warning |
| **Manual** | Requires human interaction or visual verification | Skip, add to checklist |
### 2.2 Detect Automatable Scenarios
Check scenario content for these indicators (case-insensitive):
**CLI/Command indicators:**
- `npx`, `npm run`, `yarn`, `pnpm`
- `node `, `python `, `ruby `
- `curl `, `wget `, `http`
- `bash `, `sh `, `./`
**Test framework indicators:**
- `pytest`, `jest`, `vitest`, `mocha`
- `npm test`, `yarn test`
**Verification indicators:**
- `--version`, `--help`
- `/health`, `/api/`, `/status`
- `exit code`, `returns 0`, `returns 1`
- `outputs`, `prints`, `displays`
**Database/Config indicators:**
- `db migrate`, `db status`
- `config validate`, `config check`
### 2.3 Detect Semi-Automated Scenarios
Scenarios with commands that require prior setup:
**Setup-required indicators:**
- "Start the server first"
- "Ensure database is running"
- "In a separate terminal"
- "After deploying"
**Partial automation indicators:**
- `test-send`, `send test`
- "check your email/inbox"
- "verify in browser"
- Manual setup + automated verification
### 2.4 Classify as Manual
Scenarios without detectable automation path:
**Manual-only indicators:**
- "Railway dashboard", "Vercel dashboard"
- "Open browser", "Navigate to"
- "Visual inspection", "Visually verify"
- "Two terminals", "Side by side"
- "User should see", "Observe that"
- No code blocks or CLI references
### 2.5 Extract Commands
For automatable/semi-automated scenarios, extract the verification command:
1. Look for inline code: `` `command here` ``
2. Look for code blocks: ```bash ... ```
3. Look for CLI patterns: `npx ...`, `npm run ...`, `curl ...`
4. Look for expected patterns after "Run:" or "Execute:"
### 2.6 Build Classification Result
```yaml
classification:
total: 9
automatable: 6
semi_automated: 2
manual: 1
automatable_scenarios:
- id: 1
name: "Project Initialization"
command: "npx heimdall --version"
expected: "displays a version number"
- id: 3
name: "Database Migration"
command: "npx heimdall db migrate"
expected: "success message"
semi_automated_scenarios:
- id: 7
name: "Email Notification"
command: "curl -X POST localhost:3000/test-send"
expected: "email received"
note: "Requires manual inbox verification"
manual_scenarios:
- id: 9
name: "Dashboard Visual Check"
note: "Requires browser inspection"
```
## Outputs
| Output | Location | Description |
|--------|----------|-------------|
| automatable | Array | Scenarios to execute automatically |
| semi_automated | Array | Scenarios needing setup + execution |
| manual | Array | Scenarios requiring human verification |
| classification_summary | Console | Counts per category |
## Completion Signal
```
SCENARIOS_CLASSIFIED: {automatable}/{semi_automated}/{manual}
```
## Example Output
```
[UAT] Classifying 9 scenarios...
[AUTO] Scenario 1: Project Initialization
[AUTO] Scenario 2: Configuration Setup
[AUTO] Scenario 3: Database Migration
[AUTO] Scenario 4: Connection Validation
[AUTO] Scenario 5: Worker Process Startup
[AUTO] Scenario 6: Job Queue Testing
[SEMI] Scenario 7: Email Notification
[SEMI] Scenario 8: Webhook Delivery
[MANUAL] Scenario 9: Dashboard Visual Check
SCENARIOS_CLASSIFIED: 6/2/1
```
## Classification Confidence
For edge cases, use this priority:
1. If command is clearly present → Automatable
2. If command present but requires setup → Semi-automated
3. If no command detected → Manual
When in doubt, classify as semi-automated (attempts execution, flags for review).

View File

@ -0,0 +1,202 @@
# Step 3: Execute Scenarios
## Purpose
Run automatable scenarios via shell commands, capture results, and determine pass/fail status for each.
## Inputs
| Input | Source | Required |
|-------|--------|----------|
| automatable | Step 2 | Yes |
| semi_automated | Step 2 | For full gate mode |
| gate_mode | CLI | Yes (quick/full/skip) |
| timeout | Config | No (default: 30s) |
## Process
### 3.1 Filter by Gate Mode
| Mode | Scenarios Executed |
|------|-------------------|
| `quick` | Automatable only |
| `full` | Automatable + Semi-automated |
| `skip` | None (gate passes automatically) |
**Skip mode behavior:**
```
[UAT] Gate mode: skip - bypassing scenario execution
UAT_GATE_RESULT: PASS
UAT_SCENARIOS_PASSED: 0/0 (skipped)
```
### 3.2 Handle Empty Scenario List
If no scenarios to execute:
```
[UAT] No automatable scenarios found - gate passes by default
UAT_GATE_RESULT: PASS
UAT_SCENARIOS_PASSED: 0/0 (none automatable)
```
### 3.3 Execute Each Scenario
For each scenario in the execution list:
#### 3.3.1 Extract Command
```bash
# From scenario object
command="${scenario.command}"
# If no command extracted, mark as failed
if [ -z "$command" ]; then
status="FAIL"
error="No automatable command found"
fi
```
#### 3.3.2 Execute with Timeout
```bash
# Using GNU timeout
output=$(timeout $TIMEOUT_SECONDS bash -c "$command" 2>&1)
exit_code=$?
# macOS fallback (perl-based timeout)
output=$(perl -e 'alarm shift @ARGV; exec @ARGV' $TIMEOUT_SECONDS bash -c "$command" 2>&1)
exit_code=$?
```
#### 3.3.3 Capture Results
Record for each execution:
- **stdout**: Command output
- **stderr**: Error output (captured via 2>&1)
- **exit_code**: Process exit code
- **duration_ms**: Execution time in milliseconds
- **status**: PASS or FAIL
### 3.4 Result Evaluation Rules
| Condition | Result | Notes |
|-----------|--------|-------|
| Exit code 0 | PASS | Command succeeded |
| Exit code 124 | FAIL (timeout) | Exceeded timeout limit |
| Exit code 127 | FAIL (not found) | Command not found |
| Exit code non-zero | FAIL (error) | Command failed |
### 3.5 Expected Output Matching (Optional)
If scenario has `expected_result`, apply flexible matching:
**Contains match** (default):
```bash
if echo "$output" | grep -qi "$expected"; then
match="true"
fi
```
**Exit code only match:**
```bash
# If expected contains "exit 0" or "returns 0"
if [ $exit_code -eq 0 ]; then
match="true"
fi
```
**Note:** Output matching is advisory. Primary pass/fail is based on exit code.
### 3.6 Record Execution Results
```yaml
execution_results:
- scenario_id: 1
name: "Project Initialization"
command: "npx heimdall --version"
status: "PASS"
exit_code: 0
output: "1.0.0"
duration_ms: 1250
- scenario_id: 3
name: "Database Migration"
command: "npx heimdall db migrate"
status: "FAIL"
exit_code: 1
output: ""
stderr: "Error: relation 'events' does not exist"
duration_ms: 3400
```
### 3.7 Progress Output
During execution, output progress:
```
[UAT] Executing 6 scenarios...
Scenario 1: Project Initialization
Command: npx heimdall --version
[PASS] (1250ms)
Scenario 2: Configuration Setup
Command: npx heimdall config validate
[PASS] (890ms)
Scenario 3: Database Migration
Command: npx heimdall db migrate
[FAIL] (3400ms)
Error: relation 'events' does not exist
```
## Outputs
| Output | Location | Description |
|--------|----------|-------------|
| results | Array | Per-scenario execution results |
| passed_count | State | Number of passed scenarios |
| failed_count | State | Number of failed scenarios |
| failed_details | Array | Details for failed scenarios |
## Completion Signal
```
SCENARIOS_EXECUTED: {passed}/{total}
```
## Error Handling
| Error | Action |
|-------|--------|
| Command not found | Record as FAIL with "command not found" message |
| Timeout exceeded | Record as FAIL with "timeout after Xs" message |
| Permission denied | Record as FAIL with permission error |
| Shell syntax error | Record as FAIL with syntax error details |
## Example Complete Output
```
[UAT] Executing 6 scenarios (gate mode: quick)
Scenario 1: Project Initialization
[PASS] npx heimdall --version (1250ms)
Scenario 2: Configuration Setup
[PASS] npx heimdall config validate (890ms)
Scenario 3: Database Migration
[FAIL] npx heimdall db migrate (3400ms)
Error: relation 'events' does not exist
Scenario 4: Connection Validation
[PASS] npx heimdall db status (450ms)
Scenario 5: Worker Process Startup
[PASS] npx heimdall worker --check (2100ms)
Scenario 6: Job Queue Testing
[PASS] npx heimdall queue test (1800ms)
SCENARIOS_EXECUTED: 5/6
```

View File

@ -0,0 +1,237 @@
# Step 4: Evaluate Gate
## Purpose
Determine overall pass/fail status based on execution results. If failed and self-healing is enabled, generate fix context and trigger the quick-dev fix loop.
## Inputs
| Input | Source | Required |
|-------|--------|----------|
| results | Step 3 | Yes |
| passed_count | Step 3 | Yes |
| failed_count | Step 3 | Yes |
| failed_details | Step 3 | Yes |
| max_retries | CLI | Yes (default: 2) |
| current_attempt | State | Yes (starts at 0) |
| self_heal_enabled | Config | Yes (default: true) |
## Process
### 4.1 Check All Results
```
if failed_count == 0:
gate_status = PASS
→ Skip to Step 5 (Report Results)
else:
gate_status = FAIL
→ Continue to 4.2
```
### 4.2 Evaluate Retry Eligibility
```
if current_attempt >= max_retries:
→ Max retries exceeded, halt
exit_code = 2
if self_heal_enabled == false:
→ Self-healing disabled, report failure
exit_code = 1
→ Continue to 4.3 (Generate Fix Context)
```
### 4.3 Generate Fix Context Document
Create fix context document at:
```
{sprint_artifacts}/uat-fixes/epic-{epic_id}-fix-context-{attempt}.md
```
#### 4.3.1 Load Template
Load from: `{workflow_path}/uat-fix-context-template.md`
If template not found, use inline default structure.
#### 4.3.2 Populate Template Variables
| Variable | Value |
|----------|-------|
| `{epic_id}` | Epic ID being validated |
| `{attempt}` | Current fix attempt number |
| `{timestamp}` | ISO 8601 timestamp |
| `{max_retries}` | Maximum retry limit |
| `{next_attempt}` | attempt + 1 |
| `{failure_count}` | Number of failed scenarios |
| `{passed}` | Number of passed scenarios |
| `{total}` | Total scenarios executed |
#### 4.3.3 Append Failed Scenario Details
For each failed scenario:
```markdown
### Scenario {id}: {name}
**Command Executed:**
```bash
{command}
```
**Expected Result:**
{expected_result}
**Actual Result:**
```
{actual_output}
```
**Error Output:**
```
{stderr}
```
**Exit Code:** {exit_code}
**Related Story:** {story_id} (if determinable)
**Root Cause Hint:**
{analyze_error_for_hints}
---
```
#### 4.3.4 Add Fix Instructions
```markdown
## Fix Instructions
Address the failures above in priority order. For each fix:
1. **Analyze** - Understand why the scenario failed
2. **Locate** - Find the relevant code files
3. **Fix** - Implement the minimum change to resolve
4. **Verify** - Run the scenario command locally
5. **Commit** - Use: `fix(epic-{epic_id}): {description}`
### Constraints
- Only fix identified failures
- Do not refactor unrelated code
- Run tests after fixes: `npm test`
```
### 4.4 Trigger Quick-Dev Fix
Spawn a fresh Claude session for fixes:
```bash
fix_prompt="You are Barry, the Quick Flow Solo Dev.
Load and process this fix context document:
{fix_context_file}
Your task:
1. Read the failed scenarios and error details
2. Analyze root cause for each failure
3. Implement targeted fixes
4. Run the failing commands to verify fixes
5. Stage changes: git add -A
6. Commit: fix(epic-{epic_id}): UAT fix #{attempt}
Constraints:
- Only fix identified failures
- Do not refactor unrelated code
- Run tests after fixes
When done, output:
FIX_COMPLETE: {fixed_count}/{failure_count}"
claude --dangerously-skip-permissions -p "$fix_prompt"
```
### 4.5 Increment and Re-validate
After fix session completes:
1. Increment `current_attempt`
2. Return to Step 3 (Execute Scenarios)
3. Re-run validation with fresh results
```
current_attempt += 1
→ Go to Step 3: Execute Scenarios
→ Then return to Step 4: Evaluate Gate
```
### 4.6 Gate Decision Flowchart
```
┌─────────────────┐
│ Execute Results │
└────────┬────────┘
┌─────────┐
│All Pass?│──Yes──▶ GATE_PASS (exit 0)
└────┬────┘
│No
┌──────────────┐
│ Retries Left?│──No──▶ GATE_FAIL (exit 2)
└──────┬───────┘ Max retries exceeded
│Yes
┌──────────────┐
│Self-Heal On? │──No──▶ GATE_FAIL (exit 1)
└──────┬───────┘ Fixable but disabled
│Yes
┌──────────────┐
│Generate Fix │
│Context Doc │
└──────┬───────┘
┌──────────────┐
│Spawn Quick- │
│Dev Fix │
└──────┬───────┘
┌──────────────┐
│Re-validate │──────▶ (Back to Execute)
└──────────────┘
```
## Outputs
| Output | Location | Description |
|--------|----------|-------------|
| gate_status | State | PASS or FAIL |
| fix_context_file | Path | Generated fix context (if failed) |
| fix_attempt | State | Current attempt number |
## Completion Signal
```
GATE_EVALUATED: PASS
```
or
```
GATE_EVALUATED: FAIL
FIX_CONTEXT: {fix_context_file}
FIX_ATTEMPT: {attempt}/{max_retries}
```
## Error Handling
| Error | Action |
|-------|--------|
| Template not found | Use inline default template |
| Fix context write fails | Log error, continue without self-healing |
| Claude session fails | Log output, count as failed attempt |
| Fix didn't resolve issue | Increment attempt, retry if allowed |

View File

@ -0,0 +1,235 @@
# Step 5: Report Results
## Purpose
Update metrics files with validation results and output parseable signals for orchestration scripts (epic-chain.sh) to consume.
## Inputs
| Input | Source | Required |
|-------|--------|----------|
| gate_status | Step 4 | Yes |
| results | Step 3 | Yes |
| passed_count | Step 3 | Yes |
| failed_count | Step 3 | Yes |
| fix_attempts | State | Yes |
| epic_id | CLI | Yes |
| metrics_dir | Config | Yes |
## Process
### 5.1 Update Metrics File
Update or create metrics file at:
```
{metrics_dir}/epic-{epic_id}-metrics.yaml
```
#### 5.1.1 Using yq (if available)
```bash
yq -i '.validation.gate_executed = true' "$metrics_file"
yq -i '.validation.gate_status = "PASS"' "$metrics_file"
yq -i '.validation.gate_mode = "quick"' "$metrics_file"
yq -i '.validation.fix_attempts = 0' "$metrics_file"
yq -i '.validation.scenarios_passed = 5' "$metrics_file"
yq -i '.validation.scenarios_failed = 1' "$metrics_file"
yq -i '.validation.timestamp = "2026-01-05T17:30:00Z"' "$metrics_file"
```
#### 5.1.2 Fallback (no yq)
Create or append validation section:
```yaml
validation:
gate_executed: true
gate_mode: "quick"
timestamp: "2026-01-05T17:30:00Z"
results:
passed: 5
failed: 1
skipped: 3
gate_status: "PASS"
fix_attempts: 0
```
#### 5.1.3 Record Blocking Issues (if failed)
```yaml
validation:
# ... other fields ...
blocking_issues:
- scenario_id: 3
name: "Database Migration"
error: "relation 'events' does not exist"
command: "npx heimdall db migrate"
```
#### 5.1.4 Record Fix History (if self-healing occurred)
```yaml
validation:
# ... other fields ...
fix_history:
- attempt: 1
timestamp: "2026-01-05T17:35:00Z"
failed_scenarios: ["scenario-3"]
fix_context: "docs/sprint-artifacts/uat-fixes/epic-1-fix-context-1.md"
result: "partial"
- attempt: 2
timestamp: "2026-01-05T17:42:00Z"
failed_scenarios: []
result: "success"
```
### 5.2 Output Parseable Signals
Print to stdout in format that epic-chain.sh can parse:
```bash
# Always output these signals
echo "UAT_GATE_RESULT: $gate_status"
echo "UAT_FIX_ATTEMPTS: $fix_attempts"
echo "UAT_SCENARIOS_PASSED: $passed_count/$total_count"
```
**Additional signals on failure:**
```bash
echo "UAT_FIX_REQUIRED: true"
echo "UAT_FIX_CONTEXT: $fix_context_file"
```
**Additional signals on max retries:**
```bash
echo "UAT_MAX_RETRIES: true"
echo "UAT_HALT_CHAIN: true"
```
### 5.3 Set Exit Code
| Status | Exit Code | Meaning |
|--------|-----------|---------|
| PASS | 0 | All scenarios passed |
| FAIL (fixable) | 1 | Failed but retries remain or self-heal disabled |
| FAIL (max retries) | 2 | Max retries exceeded, chain should halt |
### 5.4 Print Human-Readable Summary
```
═══════════════════════════════════════════════════════════
UAT VALIDATION COMPLETE
═══════════════════════════════════════════════════════════
Epic: 1
Gate Mode: quick
Gate Result: PASS
Scenarios:
Automatable: 6
Semi-automated: 2
Manual: 1
Results:
Passed: 6
Failed: 0
Fix Attempts: 0
Artifacts:
Log: /tmp/bmad-uat-validate-12345.log
UAT Document: docs/uat/epic-1-uat.md
Metrics: docs/sprint-artifacts/metrics/epic-1-metrics.yaml
═══════════════════════════════════════════════════════════
```
### 5.5 Failure Summary (if applicable)
```
═══════════════════════════════════════════════════════════
UAT VALIDATION FAILED
═══════════════════════════════════════════════════════════
Epic: 1
Gate Mode: quick
Gate Result: FAIL
Scenarios:
Automatable: 6
Semi-automated: 2
Manual: 1
Results:
Passed: 5
Failed: 1
Fix Attempts: 2
Blocking Issues:
- Scenario 3: Database Migration
Error: relation 'events' does not exist
Fix Context:
docs/sprint-artifacts/uat-fixes/epic-1-fix-context-2.md
Action Required:
Manual intervention needed - max retries exceeded
═══════════════════════════════════════════════════════════
```
## Outputs
| Output | Location | Description |
|--------|----------|-------------|
| Updated metrics | YAML file | Validation results persisted |
| Signals | stdout | Parseable output for orchestration |
| Summary | stdout | Human-readable summary |
| Exit code | Process | 0=pass, 1=fail-fixable, 2=fail-max-retries |
## Completion Signal
```
RESULTS_REPORTED: {metrics_file_path}
```
## Signal Reference
### For Orchestration Scripts
| Signal | Values | Description |
|--------|--------|-------------|
| `UAT_GATE_RESULT` | PASS, FAIL | Overall gate status |
| `UAT_FIX_ATTEMPTS` | 0-N | Number of fix attempts made |
| `UAT_SCENARIOS_PASSED` | X/Y | Passed/Total ratio |
| `UAT_FIX_REQUIRED` | true | Indicates fixes were needed |
| `UAT_FIX_CONTEXT` | path | Location of fix context doc |
| `UAT_MAX_RETRIES` | true | Max retries exceeded |
| `UAT_HALT_CHAIN` | true | Chain should stop |
### Parsing in Shell
```bash
# In epic-chain.sh
uat_output=$("$SCRIPT_DIR/uat-validate.sh" "$epic_id" 2>&1)
if echo "$uat_output" | grep -q "UAT_GATE_RESULT: PASS"; then
log_success "UAT passed"
else
log_error "UAT failed"
if echo "$uat_output" | grep -q "UAT_HALT_CHAIN: true"; then
log_error "Halting chain - max retries exceeded"
exit 2
fi
fi
```
## Error Handling
| Error | Action |
|-------|--------|
| Metrics file write fails | Log warning, continue with signal output |
| yq not available | Use fallback append method |
| Invalid metrics file | Create new file with validation section |

View File

@ -1,7 +1,7 @@
# UAT Fix Context - Epic {epic_id} (Attempt {attempt})
**Generated:** {timestamp}
**Epic:** {epic_name}
**Epic:** {epic_id}
**Gate Result:** FAIL ({passed}/{total} scenarios passed)
---
@ -12,101 +12,16 @@ This document contains the context needed to fix UAT failures for Epic {epic_id}
**Failures to fix:** {failure_count}
**Fix attempt:** {attempt} of {max_retries}
**UAT Document:** `{uat_doc_path}`
---
## Failed Scenarios
## Quick Start
{#each failed_scenario}
### Scenario {scenario_id}: {scenario_name}
**Command Executed:**
```bash
{command}
```
**Expected Result:**
{expected_result}
**Actual Result:**
```
{actual_output}
```
**Error Output:**
```
{stderr}
```
**Exit Code:** {exit_code}
**Related Story:** {story_id}
**Acceptance Criteria:**
{#each acceptance_criteria}
- {criterion}
{/each}
**Root Cause Hint:**
{root_cause_hint}
**Files Likely Involved:**
{#each likely_files}
- `{file_path}`
{/each}
1. Read each failed scenario below
2. Identify root cause from error output
3. Locate and fix the relevant code
4. Verify fix: run the failing command
5. Commit: `fix(epic-{epic_id}): {description}`
---
{/each}
## Fix Instructions
Address the failures above in priority order. For each fix:
1. **Analyze** - Understand why the scenario failed
2. **Locate** - Find the relevant code files
3. **Fix** - Implement the minimum change to resolve the failure
4. **Verify** - Run the scenario command locally to confirm fix
5. **Commit** - Use message format: `fix(epic-{epic_id}): {description}`
### Priority Order
{#each failed_scenario}
{priority}. **Scenario {scenario_id}**: {one_line_description}
{/each}
---
## Constraints
- Only fix the identified failures - do not refactor unrelated code
- Run the specific failing commands to verify each fix
- Run project tests after all fixes: `npm test`
- Commit with conventional format: `fix(epic-{epic_id}): {description}`
- If a fix requires changes that would break other scenarios, document the tradeoff
---
## Context Files
The following files may provide additional context:
| File | Purpose |
|------|---------|
| `{uat_doc_path}` | Full UAT document with all scenarios |
| `{story_files}` | Story files with complete acceptance criteria |
| `{architecture_doc}` | System architecture reference |
---
## After Fixing
Once all fixes are committed, the UAT validation will automatically re-run.
- **If all pass:** Epic continues to next phase
- **If failures remain:** Another fix context will be generated (attempt {next_attempt})
- **If max retries exceeded:** Chain halts for human intervention
---
*Generated by UAT Validate Workflow*
*BMAD Method - Epic Chain Self-Healing*