BMAD-METHOD/scripts/epic-chain.sh

542 lines
16 KiB
Bash
Executable File

#!/bin/bash
#
# BMAD Epic Chain - Execute Multiple Epics with Analysis and Context Sharing
#
# Usage: ./epic-chain.sh <epic-ids...> [options]
#
# Examples:
# ./epic-chain.sh 36 37 38
# ./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
#
# 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
#
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"
EPICS_DIR="$PROJECT_ROOT/docs/epics"
UAT_DIR="$PROJECT_ROOT/docs/uat"
HANDOFF_DIR="$PROJECT_ROOT/docs/handoffs"
LOG_FILE="/tmp/bmad-epic-chain-$$.log"
CHAIN_PLAN_FILE="$SPRINT_ARTIFACTS_DIR/chain-plan.yaml"
# 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
# =============================================================================
# Helper Functions
# =============================================================================
log() {
echo -e "${BLUE}[CHAIN]${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"
}
log_header() {
echo ""
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN}${BOLD} $1${NC}"
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════════════${NC}"
echo ""
}
log_section() {
echo ""
echo -e "${BOLD}───────────────────────────────────────────────────────────${NC}"
echo -e "${BOLD} $1${NC}"
echo -e "${BOLD}───────────────────────────────────────────────────────────${NC}"
}
# =============================================================================
# Argument Parsing
# =============================================================================
EPIC_IDS=()
DRY_RUN=false
ANALYZE_ONLY=false
VERBOSE=false
START_FROM=""
SKIP_DONE=false
NO_HANDOFF=false
NO_COMBINED_UAT=false
while [[ $# -gt 0 ]]; do
case $1 in
--dry-run)
DRY_RUN=true
shift
;;
--analyze-only)
ANALYZE_ONLY=true
shift
;;
--verbose)
VERBOSE=true
shift
;;
--start-from)
START_FROM="$2"
shift 2
;;
--skip-done)
SKIP_DONE=true
shift
;;
--no-handoff)
NO_HANDOFF=true
shift
;;
--no-combined-uat)
NO_COMBINED_UAT=true
shift
;;
-*)
echo "Unknown option: $1"
exit 1
;;
*)
EPIC_IDS+=("$1")
shift
;;
esac
done
if [ ${#EPIC_IDS[@]} -eq 0 ]; then
echo "Usage: $0 <epic-id> [epic-id...] [options]"
echo ""
echo "Examples:"
echo " $0 36 37 38 # Execute epics 36, 37, 38 in order"
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 ""
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"
exit 1
fi
# =============================================================================
# Setup
# =============================================================================
log_header "EPIC CHAIN EXECUTION"
log "Epics to chain: ${EPIC_IDS[*]}"
log "Project root: $PROJECT_ROOT"
# Ensure directories exist
mkdir -p "$UAT_DIR"
mkdir -p "$HANDOFF_DIR"
mkdir -p "$SPRINT_ARTIFACTS_DIR"
# =============================================================================
# Phase 1: Validate Epics
# =============================================================================
log_section "Phase 1: Validating Epics"
# Bash 3.2 compatible: use indexed arrays with matching indices
EPIC_FILES_LIST=()
EPIC_STORIES_LIST=()
EPIC_DEPS_LIST=()
for i in "${!EPIC_IDS[@]}"; do
epic_id="${EPIC_IDS[$i]}"
# Find epic file
epic_file=""
for pattern in "epic-${epic_id}.md" "epic-${epic_id}-"*.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 $epic_id: File not found in $EPICS_DIR"
exit 1
fi
EPIC_FILES_LIST[$i]="$epic_file"
log_success "Epic $epic_id: Found $(basename "$epic_file")"
# Find stories for this epic
story_count=0
for search_dir in "$STORIES_DIR" "$SPRINT_ARTIFACTS_DIR"; do
if [ -d "$search_dir" ]; then
count=$(find "$search_dir" -name "${epic_id}-*-*.md" 2>/dev/null | wc -l)
story_count=$((story_count + count))
fi
done
if [ "$story_count" -eq 0 ]; then
log_warn "Epic $epic_id: No story files found (will check epic file for story definitions)"
else
log "Epic $epic_id: Found $story_count story files"
fi
EPIC_STORIES_LIST[$i]=$story_count
done
log_success "All ${#EPIC_IDS[@]} epics validated"
# =============================================================================
# Phase 2: Analyze Dependencies
# =============================================================================
log_section "Phase 2: Analyzing Dependencies"
# Simple dependency detection: check for ## Dependencies section in epic files
for i in "${!EPIC_IDS[@]}"; do
epic_id="${EPIC_IDS[$i]}"
epic_file="${EPIC_FILES_LIST[$i]}"
# Look for Dependencies section
deps=$(grep -A 10 "^## Dependencies" "$epic_file" 2>/dev/null | grep -oE "Epic [0-9]+" | grep -oE "[0-9]+" || true)
if [ -n "$deps" ]; then
EPIC_DEPS_LIST[$i]="$deps"
log "Epic $epic_id depends on: $deps"
else
EPIC_DEPS_LIST[$i]=""
log "Epic $epic_id: No explicit dependencies"
fi
done
# =============================================================================
# Phase 3: Determine Execution Order
# =============================================================================
log_section "Phase 3: Determining Execution Order"
# For now, use order as provided (user presumably knows the right order)
# Future enhancement: topological sort based on dependencies
EXECUTION_ORDER=("${EPIC_IDS[@]}")
log "Execution order: ${EXECUTION_ORDER[*]}"
# =============================================================================
# Phase 4: Generate Chain Plan
# =============================================================================
log_section "Phase 4: Generating Chain Plan"
cat > "$CHAIN_PLAN_FILE" << EOF
# Epic Chain Execution Plan
# Generated: $(date '+%Y-%m-%d %H:%M:%S')
epics: [${EPIC_IDS[*]}]
total_epics: ${#EPIC_IDS[@]}
execution_order:
EOF
total_stories=0
for i in "${!EXECUTION_ORDER[@]}"; do
epic_id="${EXECUTION_ORDER[$i]}"
story_count=${EPIC_STORIES_LIST[$i]}
total_stories=$((total_stories + story_count))
deps="${EPIC_DEPS_LIST[$i]}"
cat >> "$CHAIN_PLAN_FILE" << EOF
- epic: $epic_id
file: $(basename "${EPIC_FILES_LIST[$i]}")
stories: $story_count
dependencies: [$deps]
EOF
done
cat >> "$CHAIN_PLAN_FILE" << EOF
total_stories: $total_stories
options:
dry_run: $DRY_RUN
skip_done: $SKIP_DONE
context_handoff: $([ "$NO_HANDOFF" = true ] && echo "false" || echo "true")
combined_uat: $([ "$NO_COMBINED_UAT" = true ] && echo "false" || echo "true")
EOF
log_success "Chain plan saved to: $CHAIN_PLAN_FILE"
# =============================================================================
# Display Summary
# =============================================================================
log_header "CHAIN EXECUTION PLAN"
echo " Epics: ${EPIC_IDS[*]}"
echo " Total Stories: $total_stories"
echo " Dry Run: $DRY_RUN"
echo " Skip Done: $SKIP_DONE"
echo ""
echo " Execution Order:"
for i in "${!EXECUTION_ORDER[@]}"; do
epic_id="${EXECUTION_ORDER[$i]}"
deps="${EPIC_DEPS_LIST[$i]}"
echo " $((i+1)). Epic $epic_id (${EPIC_STORIES_LIST[$i]} stories) ${deps:+← depends on: $deps}"
done
echo ""
if [ "$ANALYZE_ONLY" = true ]; then
log_success "Analysis complete (--analyze-only specified)"
echo ""
echo "To execute this chain, run:"
echo " $0 ${EPIC_IDS[*]}"
echo ""
exit 0
fi
# =============================================================================
# Phase 5: Execute Chain
# =============================================================================
log_header "EXECUTING EPIC CHAIN"
COMPLETED_EPICS=0
FAILED_EPICS=0
SKIPPED_EPICS=0
START_TIME=$(date +%s)
STARTED=false
PREVIOUS_EPIC=""
PREVIOUS_IDX=-1
for current_idx in "${!EXECUTION_ORDER[@]}"; do
epic_id="${EXECUTION_ORDER[$current_idx]}"
# Handle --start-from
if [ -n "$START_FROM" ] && [ "$STARTED" = false ]; then
if [ "$epic_id" = "$START_FROM" ]; then
STARTED=true
else
log_warn "Skipping Epic $epic_id (waiting for --start-from $START_FROM)"
((SKIPPED_EPICS++))
continue
fi
fi
log_section "Executing Epic $epic_id"
# Generate context handoff from previous epic
if [ -n "$PREVIOUS_EPIC" ] && [ "$NO_HANDOFF" = false ]; then
handoff_file="$HANDOFF_DIR/epic-${PREVIOUS_EPIC}-to-${epic_id}-handoff.md"
if [ -f "$handoff_file" ]; then
log "Loading context handoff from Epic $PREVIOUS_EPIC"
fi
fi
# Build epic-execute command
exec_cmd="$SCRIPT_DIR/epic-execute.sh $epic_id"
if [ "$DRY_RUN" = true ]; then
exec_cmd="$exec_cmd --dry-run"
fi
if [ "$SKIP_DONE" = true ]; then
exec_cmd="$exec_cmd --skip-done"
fi
if [ "$VERBOSE" = true ]; then
exec_cmd="$exec_cmd --verbose"
fi
log "Running: $exec_cmd"
# Execute epic
if [ "$DRY_RUN" = true ]; then
echo "[DRY RUN] Would execute: $exec_cmd"
((COMPLETED_EPICS++))
else
if $exec_cmd; then
log_success "Epic $epic_id completed"
((COMPLETED_EPICS++))
# Generate handoff for next epic
if [ "$NO_HANDOFF" = false ]; then
next_idx=$((current_idx + 1))
if [ $next_idx -lt ${#EXECUTION_ORDER[@]} ]; then
next_epic="${EXECUTION_ORDER[$next_idx]}"
handoff_file="$HANDOFF_DIR/epic-${epic_id}-to-${next_epic}-handoff.md"
log "Generating context handoff: Epic $epic_id → Epic $next_epic"
story_count=${EPIC_STORIES_LIST[$current_idx]}
cat > "$handoff_file" << EOF
# Epic $epic_id → Epic $next_epic Handoff
## Generated
$(date '+%Y-%m-%d %H:%M:%S')
## Epic $epic_id Completion Summary
Epic $epic_id has been completed. Key context for Epic $next_epic:
### Patterns Established
- Review code changes in Epic $epic_id for established patterns
- Check \`docs/stories/${epic_id}-*\` for implementation details
### Files Modified
$(git diff --name-only HEAD~${story_count} HEAD 2>/dev/null | head -20 || echo "Unable to determine - check git log")
### Notes for Next Epic
- Continue following patterns established in this epic
- Reference UAT document at \`docs/uat/epic-${epic_id}-uat.md\` for context
EOF
log_success "Handoff saved to: $handoff_file"
fi
fi
else
log_error "Epic $epic_id failed"
((FAILED_EPICS++))
# Ask whether to continue or abort
echo ""
echo "Epic $epic_id failed. Continue with remaining epics? (y/n)"
read -r continue_choice
if [ "$continue_choice" != "y" ]; then
log_error "Chain execution aborted by user"
break
fi
fi
fi
PREVIOUS_EPIC="$epic_id"
PREVIOUS_IDX=$current_idx
done
# =============================================================================
# Phase 6: Generate Combined UAT
# =============================================================================
if [ "$NO_COMBINED_UAT" = false ] && [ "$DRY_RUN" = false ] && [ $COMPLETED_EPICS -gt 1 ]; then
log_section "Generating Combined UAT Document"
combined_uat_file="$UAT_DIR/chain-${EPIC_IDS[*]// /-}-uat.md"
cat > "$combined_uat_file" << EOF
# Combined UAT: Epics ${EPIC_IDS[*]}
## Generated
$(date '+%Y-%m-%d %H:%M:%S')
## Overview
This document combines User Acceptance Testing for epics: ${EPIC_IDS[*]}
## Individual Epic UATs
EOF
for epic_id in "${EPIC_IDS[@]}"; do
uat_file="$UAT_DIR/epic-${epic_id}-uat.md"
if [ -f "$uat_file" ]; then
echo "### Epic $epic_id" >> "$combined_uat_file"
echo "" >> "$combined_uat_file"
echo "See: [epic-${epic_id}-uat.md](epic-${epic_id}-uat.md)" >> "$combined_uat_file"
echo "" >> "$combined_uat_file"
fi
done
cat >> "$combined_uat_file" << EOF
## Cross-Epic Integration Testing
After individual epic testing, verify these cross-epic scenarios:
1. [ ] Features from earlier epics still work after later epic changes
2. [ ] Data flows correctly between features from different epics
3. [ ] No regression in previously tested functionality
## Sign-off
| Epic | Tester | Date | Status |
|------|--------|------|--------|
EOF
for epic_id in "${EPIC_IDS[@]}"; do
echo "| $epic_id | | | Pending |" >> "$combined_uat_file"
done
log_success "Combined UAT saved to: $combined_uat_file"
fi
# =============================================================================
# Summary
# =============================================================================
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
log_header "EPIC CHAIN COMPLETE"
echo " Epics in Chain: ${#EPIC_IDS[@]}"
echo " Completed: $COMPLETED_EPICS"
echo " Failed: $FAILED_EPICS"
echo " Skipped: $SKIPPED_EPICS"
echo " Duration: ${DURATION}s"
echo ""
echo " Artifacts:"
echo " - Chain Plan: $CHAIN_PLAN_FILE"
echo " - Handoffs: $HANDOFF_DIR/"
echo " - UAT Documents: $UAT_DIR/"
echo " - Log: $LOG_FILE"
echo ""
if [ $FAILED_EPICS -gt 0 ]; then
log_warn "$FAILED_EPICS epic(s) failed - check log for details"
exit 1
fi
log_success "All epics completed successfully"
echo ""
echo "Next step: Review UAT documents and run manual testing"
echo ""