#!/bin/bash # # BMAD Epic Execute - Automated Story Execution with Context Isolation # # Usage: ./epic-execute.sh [options] # # Options: # --dry-run Show what would be executed without running # --skip-review Skip code review phase (not recommended) # --no-commit Stage changes but don't commit # --parallel Run independent stories in parallel (experimental) # --verbose Show detailed output # --start-from ID Start from a specific story (e.g., 31-2) # --skip-done Skip stories with Status: Done # set -e # ============================================================================= # Configuration # ============================================================================= SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" BMAD_DIR="$PROJECT_ROOT/bmad" STORIES_DIR="$PROJECT_ROOT/docs/stories" SPRINT_ARTIFACTS_DIR="$PROJECT_ROOT/docs/sprint-artifacts" SPRINTS_DIR="$PROJECT_ROOT/docs/sprints" EPICS_DIR="$PROJECT_ROOT/docs/epics" UAT_DIR="$PROJECT_ROOT/docs/uat" LOG_FILE="/tmp/bmad-epic-execute-$$.log" # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # ============================================================================= # Helper Functions # ============================================================================= log() { echo -e "${BLUE}[BMAD]${NC} $1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE" } log_success() { echo -e "${GREEN}[✓]${NC} $1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [SUCCESS] $1" >> "$LOG_FILE" } log_error() { echo -e "${RED}[✗]${NC} $1" echo "[$(date '+%Y-%m-%d %H:%M:%S')] [ERROR] $1" >> "$LOG_FILE" } log_warn() { echo -e "${YELLOW}[!]${NC} $1" 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 # ============================================================================= EPIC_ID="" DRY_RUN=false SKIP_REVIEW=false NO_COMMIT=false PARALLEL=false VERBOSE=false START_FROM="" SKIP_DONE=false while [[ $# -gt 0 ]]; do case $1 in --dry-run) DRY_RUN=true shift ;; --skip-review) SKIP_REVIEW=true shift ;; --no-commit) NO_COMMIT=true shift ;; --parallel) PARALLEL=true shift ;; --verbose) VERBOSE=true shift ;; --start-from) START_FROM="$2" shift 2 ;; --skip-done) SKIP_DONE=true shift ;; -*) echo "Unknown option: $1" exit 1 ;; *) EPIC_ID="$1" shift ;; esac done if [ -z "$EPIC_ID" ]; then echo "Usage: $0 [options]" echo "" echo "Options:" echo " --dry-run Show what would be executed" echo " --skip-review Skip code review phase" echo " --no-commit Don't commit after stories" echo " --parallel Parallel execution (experimental)" echo " --verbose Detailed output" echo " --start-from ID Start from a specific story (e.g., 31-2)" echo " --skip-done Skip stories with Status: Done" exit 1 fi # ============================================================================= # Setup # ============================================================================= log "Starting epic execution for: $EPIC_ID" log "Project root: $PROJECT_ROOT" # Ensure directories exist 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) EPIC_ID_PADDED=$(printf "%03d" "$EPIC_ID" 2>/dev/null || echo "$EPIC_ID") for pattern in "epic-${EPIC_ID}.md" "epic-${EPIC_ID}-"*.md "epic-${EPIC_ID_PADDED}-"*.md "epic-0${EPIC_ID}-"*.md "${EPIC_ID}.md"; do found=$(find "$EPICS_DIR" -name "$pattern" 2>/dev/null | head -1) if [ -n "$found" ]; then EPIC_FILE="$found" break fi done if [ -z "$EPIC_FILE" ] || [ ! -f "$EPIC_FILE" ]; then log_error "Epic file not found for: $EPIC_ID" log_error "Searched in: $EPICS_DIR" exit 1 fi log "Found epic file: $EPIC_FILE" # ============================================================================= # Discover Stories # ============================================================================= log "Discovering stories..." # Search multiple locations for story files STORY_LOCATIONS=("$STORIES_DIR" "$SPRINT_ARTIFACTS_DIR" "$SPRINTS_DIR") STORIES=() for search_dir in "${STORY_LOCATIONS[@]}"; do if [ ! -d "$search_dir" ]; then continue fi # Method 1: Stories that reference this epic in content while IFS= read -r -d '' file; do if [[ ! " ${STORIES[*]} " =~ " ${file} " ]]; then STORIES+=("$file") fi done < <(grep -l -Z "Epic.*:.*${EPIC_ID}\|epic-${EPIC_ID}\|Epic.*${EPIC_ID}" "$search_dir"/*.md 2>/dev/null || true) # Method 2: {EpicNumber}-{StoryNumber}-{description}.md (e.g., 1-1-user-registration.md) while IFS= read -r -d '' file; do if [[ ! " ${STORIES[*]} " =~ " ${file} " ]]; then STORIES+=("$file") fi done < <(find "$search_dir" -name "${EPIC_ID}-*-*.md" -print0 2>/dev/null || true) # Method 3: story-{epic}.{seq}-*.md (BMAD standard) while IFS= read -r -d '' file; do if [[ ! " ${STORIES[*]} " =~ " ${file} " ]]; then STORIES+=("$file") fi done < <(find "$search_dir" -name "story-${EPIC_ID}.*-*.md" -print0 2>/dev/null || true) # Method 4: story-{epic}-*.md (BMAD alternate) while IFS= read -r -d '' file; do if [[ ! " ${STORIES[*]} " =~ " ${file} " ]]; then STORIES+=("$file") fi done < <(find "$search_dir" -name "story-${EPIC_ID}-*.md" -print0 2>/dev/null || true) done if [ ${#STORIES[@]} -eq 0 ]; then log_error "No stories found for epic: $EPIC_ID" log_error "Searched in: ${STORY_LOCATIONS[*]}" log_error "Looking for:" log_error " - Files containing 'Epic: $EPIC_ID'" log_error " - Files named: ${EPIC_ID}-*-*.md (e.g., ${EPIC_ID}-1-description.md)" log_error " - Files named: story-${EPIC_ID}.*.md or story-${EPIC_ID}-*.md" exit 1 fi log "Found ${#STORIES[@]} stories" # Sort stories for consistent execution order IFS=$'\n' STORIES=($(sort -V <<<"${STORIES[*]}")); unset IFS # Show which directories stories came from if [ "$VERBOSE" = true ]; then for story in "${STORIES[@]}"; do echo " - $story" done fi # ============================================================================= # Execution Functions # ============================================================================= execute_dev_phase() { local story_file="$1" local story_id=$(basename "$story_file" .md) log ">>> DEV PHASE: $story_id" local story_contents=$(cat "$story_file") # Build the dev prompt local dev_prompt="You are the Dev agent executing a BMAD story implementation. ## Your Task Implement story: $story_id ## Story Specification $story_contents ## Implementation Requirements 1. Read the story file completely before writing any code 2. Follow existing patterns in the codebase 3. Implement ALL acceptance criteria 4. Write tests for each criterion 5. Run tests and fix any failures 6. Update documentation as needed ## When Complete 1. Update the story file: - Change Status to: In Review - Fill in the Dev Agent Record section with: - Implementation Summary - Files Created/Modified - Key Decisions - Tests Added - Test Results (summary of test run) - Notes for Reviewer - Acceptance Criteria Status (checklist with file references) 2. Stage changes: git add -A 3. Output exactly: IMPLEMENTATION COMPLETE: $story_id If blocked, output: IMPLEMENTATION BLOCKED: $story_id - [reason]" if [ "$DRY_RUN" = true ]; then echo "[DRY RUN] Would execute dev phase for $story_id" return 0 fi # Execute in isolated context local result result=$(claude --dangerously-skip-permissions -p "$dev_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" if echo "$result" | grep -q "IMPLEMENTATION COMPLETE"; then log_success "Dev phase complete: $story_id" return 0 elif echo "$result" | grep -q "IMPLEMENTATION BLOCKED"; then log_error "Dev phase blocked: $story_id" echo "$result" | grep "IMPLEMENTATION BLOCKED" return 1 else log_error "Dev phase did not complete cleanly: $story_id" return 1 fi } execute_review_phase() { local story_file="$1" local story_id=$(basename "$story_file" .md) log ">>> REVIEW PHASE: $story_id (fresh context)" local story_contents=$(cat "$story_file") # Build the review prompt with severity-based fix logic local review_prompt="You are a Senior Code Reviewer performing a BMAD code review. ## Your Task Review the implementation of story: $story_id You are seeing this code for the first time. You have no knowledge of the implementation process. ## Story Specification and Dev Context $story_contents The story file contains: - Acceptance criteria (what must be verified) - Dev Agent Record (implementation notes from the developer) - Notes for Reviewer (areas of concern flagged by dev) ## Review Process 1. Run: git diff --staged 2. Verify each acceptance criterion is implemented and tested 3. Check code quality, security, and patterns 4. Collect and categorize all issues by severity ## Issue Severity Definitions - **HIGH**: Security vulnerabilities, missing error handling, no tests for new code, N+1 queries, exposed credentials - **MEDIUM**: Pattern violations, missing edge cases, hardcoded config values, code duplication - **LOW**: Naming inconsistencies, minor style issues, missing comments ## Issue Fix Policy (IMPORTANT) Apply this logic after collecting all issues: \`\`\` 1. Always fix ALL HIGH severity issues 2. If TOTAL issues > 5, also fix ALL MEDIUM severity issues 3. LOW severity issues: document only, do NOT fix \`\`\` ## Review Checklist ### Acceptance Criteria For each criterion: implemented? tested? matches requirement? ### Code Quality - Follows existing patterns (MEDIUM) - No security issues (HIGH) - Error handling appropriate (HIGH) - Tests exist and meaningful (HIGH) - No hardcoded secrets (HIGH) ## After Review 1. Compile issues found with severity 2. Count: HIGH=?, MEDIUM=?, LOW=?, TOTAL=? 3. Apply fix policy: fix HIGH always, fix MEDIUM if total > 5 4. For each fix: make change, run tests, verify 5. Stage any fixes: git add -A ## Update Story File Add Code Review Record section: \`\`\`markdown ## Code Review Record **Reviewer**: Code Review Agent **Date**: $(date '+%Y-%m-%d %H:%M') ### Issues Found | # | Description | Severity | Status | |---|-------------|----------|--------| **Totals**: X HIGH, Y MEDIUM, Z LOW ### Fixes Applied [List what was fixed] ### Remaining Issues [Low severity items for future cleanup] ### Final Status Approved / Approved with fixes / Rejected \`\`\` ## Completion If PASSED (no unfixed HIGH/MEDIUM issues): 1. Update story Status to: Done 2. Output: REVIEW PASSED: $story_id or: REVIEW PASSED WITH FIXES: $story_id - Fixed N issues If FAILED (unfixable issues or missing acceptance criteria): 1. Update story Status to: Blocked 2. Output: REVIEW FAILED: $story_id - [reason]" if [ "$DRY_RUN" = true ]; then echo "[DRY RUN] Would execute review phase for $story_id" return 0 fi # Execute in isolated context local result result=$(claude --dangerously-skip-permissions -p "$review_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" if echo "$result" | grep -q "REVIEW PASSED"; then log_success "Review passed: $story_id" return 0 elif echo "$result" | grep -q "REVIEW FAILED"; then log_error "Review failed: $story_id" echo "$result" | grep "REVIEW FAILED" return 1 else log_warn "Review did not complete cleanly: $story_id" return 1 fi } commit_story() { local story_id="$1" if [ "$NO_COMMIT" = true ]; then log "Skipping commit (--no-commit)" return 0 fi if [ "$DRY_RUN" = true ]; then echo "[DRY RUN] Would commit: feat(epic-$EPIC_ID): complete $story_id" return 0 fi git add -A git commit -m "feat(epic-$EPIC_ID): complete $story_id" || { log_warn "Nothing to commit for $story_id" } log_success "Committed: $story_id" } generate_uat() { log ">>> GENERATING UAT DOCUMENT (fresh context)" local epic_contents=$(cat "$EPIC_FILE") local all_stories="" for story_file in "${STORIES[@]}"; do local story_id=$(basename "$story_file" .md) all_stories+=" $(cat "$story_file") " done local uat_prompt="You are a QA Specialist creating a User Acceptance Testing document. ## Your Task Generate a UAT document for Epic: $EPIC_ID ## Epic Definition $epic_contents ## Completed Stories $all_stories ## Requirements Create a UAT document for NON-TECHNICAL users with: 1. **Overview**: What was built (plain language) 2. **Prerequisites**: Test environment, accounts, setup 3. **Test Scenarios**: Step-by-step instructions with expected results 4. **Success Criteria**: Checklist of what must work 5. **Sign-off Section**: For human approval Write for someone who can use the software but doesn't know how it's built. ## Output Save to: $UAT_DIR/epic-${EPIC_ID}-uat.md When complete, output: UAT GENERATED: $UAT_DIR/epic-${EPIC_ID}-uat.md" if [ "$DRY_RUN" = true ]; then echo "[DRY RUN] Would generate UAT document" return 0 fi local result result=$(claude --dangerously-skip-permissions -p "$uat_prompt" 2>&1) || true echo "$result" >> "$LOG_FILE" if echo "$result" | grep -q "UAT GENERATED"; then log_success "UAT document generated" else log_warn "UAT generation may not have completed cleanly" fi # Commit UAT document if [ "$NO_COMMIT" = false ]; then git add "$UAT_DIR/epic-${EPIC_ID}-uat.md" 2>/dev/null || true git commit -m "docs(epic-$EPIC_ID): add UAT document" 2>/dev/null || true fi } # ============================================================================= # Main Execution Loop # ============================================================================= log "==========================================" log "Starting execution of ${#STORIES[@]} stories" log "==========================================" COMPLETED=0 FAILED=0 SKIPPED=0 START_TIME=$(date +%s) STARTED=false for story_file in "${STORIES[@]}"; do story_id=$(basename "$story_file" .md) # --start-from: Skip stories until we reach the specified one if [ -n "$START_FROM" ] && [ "$STARTED" = false ]; then if [[ "$story_id" == *"$START_FROM"* ]]; then STARTED=true else log_warn "Skipping $story_id (waiting for $START_FROM)" ((SKIPPED++)) update_story_metrics "skipped" continue fi fi # --skip-done: Skip stories with Status: Done if [ "$SKIP_DONE" = true ]; then 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 echo "" log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" log "Story: $story_id" log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" # DEV PHASE (Context 1) 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 # ============================================================================= # UAT Generation (Context 3 - Fresh) # ============================================================================= echo "" log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" log "UAT Document Generation" log "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" generate_uat # ============================================================================= # Summary # ============================================================================= 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" log "==========================================" echo "" echo " Epic: $EPIC_ID" echo " Duration: ${DURATION}s" echo " Stories: ${#STORIES[@]}" echo " Skipped: $SKIPPED" echo " Completed: $COMPLETED" echo " Failed: $FAILED" 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 "" if [ $FAILED -gt 0 ]; then log_warn "$FAILED stories failed - check log for details" exit 1 fi log_success "All stories completed successfully" echo "" echo "Next step: Run UAT document with a human tester" echo ""