#!/bin/bash
# ==============================================================================
# run-pipeline.sh — CalcPad Epic Development Pipeline
# ==============================================================================
# Runs dev → review → commit for each story. Auto-discovers stories and skips
# already-committed ones via git history. Supports parallel execution via
# git worktrees.
#
# Usage:
#   ./run-pipeline.sh                              # All pending stories
#   ./run-pipeline.sh --phase1                     # Phase 1 stories (Epics 1-5, 10)
#   ./run-pipeline.sh --phase1 --epic 1            # Only Epic 1 stories
#   ./run-pipeline.sh --parallel 3                 # 3 stories in parallel
#   ./run-pipeline.sh --from 3-1                   # Start from story 3-1
#   ./run-pipeline.sh --dry-run                    # Show plan only
#
# Options:
#   --phase1              Phase 1: Engine + MVP (Epics 1-5, 10)
#   --phase2              Phase 2: Power features (Epics 6, 7.1-7.6, 8.1-8.6, 9.1-9.3)
#   --phase3              Phase 3: System integration (Epics 7.7-7.14, 8.7-8.10, 9.4-9.8, 11)
#   --phase4              Phase 4: Ecosystem (Epics 12, 13, 14, 15, 16)
#   --parallel <N>        Run N stories in parallel via git worktrees (default: 1)
#   --epic <N>            Filter to epic N (e.g. --epic 1)
#   --from <prefix>       Start from story matching prefix (e.g. --from 3-1)
#   --stories <paths...>  Explicit story paths (skip auto-discovery)
#   --no-review           Skip code-review phase
#   --force               Ignore git-based skip detection
#   --cooldown <seconds>  Pause between stories (default: 10, ignored in parallel)
#   --dry-run             Show execution plan without running
#   --log-dir <path>      Directory for logs (default: .logs/epic-pipeline/)
#   --help                Show this help message
# ==============================================================================

set -euo pipefail

# ── Portable relpath (macOS lacks realpath --relative-to) ────────────────────
relpath() {
  python3 -c "import os; print(os.path.relpath('$1', '${2:-$PWD}'))"
}

# ── Configuration ─────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$SCRIPT_DIR"
STORY_DIR="${PROJECT_ROOT}/_bmad-output/implementation-artifacts"
LOG_DIR="${PROJECT_ROOT}/.logs/epic-pipeline"
WORKTREE_BASE="${PROJECT_ROOT}/.worktrees"
COOLDOWN=10
PARALLEL=1
DRY_RUN=false
NO_REVIEW=false
FORCE=false
MERGE_ONLY=false
PHASE_FILTER=""
EPIC_FILTER=""
FROM_PREFIX=""
STORIES=()
TIMESTAMP=$(date +"%Y%m%d-%H%M%S")

# Phase 1: Engine + MVP (Epics 1-5, 10)
PHASE1_STORIES=(
  1-1-lexer-and-tokenizer
  1-2-parser-and-ast
  1-3-interpreter-evaluator
  1-4-arbitrary-precision-arithmetic
  1-5-c-ffi-layer-for-swift
  1-6-wasm-bindings-for-web
  1-7-error-handling-and-graceful-degradation
  1-8-sheet-context
  2-1-unit-registry-and-base-conversion
  2-2-si-prefix-support
  2-3-css-and-screen-units
  2-4-data-units-binary-vs-decimal
  2-5-natural-language-unit-expressions
  2-6-custom-user-defined-units
  3-1-fiat-currency-provider
  3-2-cryptocurrency-provider
  3-3-historical-rates
  3-4-currency-symbol-and-code-recognition
  3-5-multi-currency-arithmetic
  4-1-date-math
  4-2-time-math
  4-3-time-zone-conversions
  4-4-business-day-calculations
  4-5-unix-timestamp-conversions
  4-6-relative-time-expressions
  5-1-variable-declaration-and-usage
  5-2-line-references
  5-3-previous-line-reference
  5-4-section-aggregators
  5-5-subtotals-and-grand-total
  5-6-autocomplete
  10-1-headers-comments-and-labels
  10-2-click-to-copy-answer
  10-3-drag-to-resize-columns
  10-4-answer-column-formatting
  10-5-line-numbers
  10-6-find-and-replace
)

# Phase 2: Power features (Epics 6, 7.1-7.6, 8.1-8.6, 9.1-9.3)
PHASE2_STORIES=()

# Phase 3: System integration (Epics 7.7-7.14, 8.7-8.10, 9.4-9.8, 11)
PHASE3_STORIES=()

# Phase 4: Ecosystem (Epics 12, 13, 14, 15, 16)
PHASE4_STORIES=()

# ── Colors ────────────────────────────────────────────────────────────────────
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'

# ── Logging ───────────────────────────────────────────────────────────────────
log() {
  local level="$1"; shift
  local ts; ts=$(date +"%H:%M:%S")
  case "$level" in
    INFO)  echo -e "${BLUE}[$ts]${NC} ${BOLD}INFO${NC}  $*" ;;
    OK)    echo -e "${GREEN}[$ts]${NC} ${GREEN}${BOLD}OK${NC}    $*" ;;
    WARN)  echo -e "${YELLOW}[$ts]${NC} ${YELLOW}${BOLD}WARN${NC}  $*" ;;
    ERROR) echo -e "${RED}[$ts]${NC} ${RED}${BOLD}ERROR${NC} $*" ;;
    STEP)  echo -e "${CYAN}[$ts]${NC} ${CYAN}${BOLD}STEP${NC}  $*" ;;
  esac
  echo "[$ts] $level $*" >> "${LOG_DIR}/run-${TIMESTAMP}.log" 2>/dev/null || true
}

separator() {
  echo ""
  echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  echo -e "${BOLD}  $1${NC}"
  echo -e "${BOLD}═══════════════════════════════════════════════════════════════${NC}"
  echo ""
}

usage() {
  head -n 32 "$0" | tail -n +2 | sed 's/^# //' | sed 's/^#//'
  exit 0
}

# ── Prerequisites ─────────────────────────────────────────────────────────────
check_prerequisites() {
  if ! command -v claude &> /dev/null; then
    log ERROR "claude CLI not found. Install: https://docs.anthropic.com/en/docs/claude-code"
    exit 1
  fi
  if [ ! -d "${PROJECT_ROOT}/_bmad" ]; then
    log ERROR "Not in a valid BMAD project root (_bmad/ not found)"
    exit 1
  fi
  if [ ! -d "$STORY_DIR" ]; then
    log ERROR "Story directory not found: $STORY_DIR"
    exit 1
  fi
  # Parallel mode requires at least one commit on main (for branching)
  if [ "$PARALLEL" -gt 1 ]; then
    if ! git -C "$PROJECT_ROOT" rev-parse HEAD &>/dev/null; then
      log ERROR "No commits on main. Parallel mode needs an initial commit."
      log ERROR "Run: git add -A && git commit -m 'initial commit'"
      exit 1
    fi
  fi
}

# ── Git-based completion detection ────────────────────────────────────────────
is_story_committed() {
  local story_path="$1"
  if git log --all --grep="Story: $story_path" --oneline -1 2>/dev/null | grep -q .; then
    return 0
  fi
  return 1
}

# ── Auto-discover stories ────────────────────────────────────────────────────
discover_stories() {
  local all_stories=()

  while IFS= read -r story; do
    local rel_path
    rel_path=$(relpath "$story" "$PROJECT_ROOT")
    all_stories+=("$rel_path")
  done < <(find "$STORY_DIR" -maxdepth 1 -name "[0-9]*-[0-9]*-*.md" -type f | sort -V)

  if [ ${#all_stories[@]} -eq 0 ]; then
    log ERROR "No story files found in $STORY_DIR"
    exit 1
  fi

  # Apply phase filter
  if [ -n "$PHASE_FILTER" ]; then
    local phase_stories_ref="PHASE${PHASE_FILTER}_STORIES[@]"
    local phase_stories=("${!phase_stories_ref}")
    if [ ${#phase_stories[@]} -eq 0 ]; then
      log ERROR "No stories defined for phase $PHASE_FILTER"
      exit 1
    fi
    local filtered=()
    for s in "${all_stories[@]}"; do
      local base; base=$(basename "$s" .md)
      for ps in "${phase_stories[@]}"; do
        if [ "$base" = "$ps" ]; then
          filtered+=("$s")
          break
        fi
      done
    done
    all_stories=("${filtered[@]}")
    if [ ${#all_stories[@]} -eq 0 ]; then
      log ERROR "No phase $PHASE_FILTER stories found on disk"
      exit 1
    fi
  fi

  # Apply --epic filter
  if [ -n "$EPIC_FILTER" ]; then
    local filtered=()
    for s in "${all_stories[@]}"; do
      local base; base=$(basename "$s")
      if [[ "$base" =~ ^${EPIC_FILTER}- ]]; then
        filtered+=("$s")
      fi
    done
    all_stories=("${filtered[@]}")
    if [ ${#all_stories[@]} -eq 0 ]; then
      log ERROR "No stories found for epic $EPIC_FILTER"
      exit 1
    fi
  fi

  # Apply --from filter
  if [ -n "$FROM_PREFIX" ]; then
    local filtered=()
    local found=false
    for s in "${all_stories[@]}"; do
      local base; base=$(basename "$s" .md)
      if [ "$found" = true ] || [[ "$base" =~ ^${FROM_PREFIX} ]]; then
        found=true
        filtered+=("$s")
      fi
    done
    if [ "$found" = false ]; then
      log ERROR "No story matching prefix '$FROM_PREFIX' found"
      exit 1
    fi
    all_stories=("${filtered[@]}")
  fi

  STORIES=("${all_stories[@]}")
}

# ── Filter out already-committed stories ──────────────────────────────────────
filter_committed() {
  if [ "$FORCE" = true ]; then
    log INFO "Force mode: skipping git-based filtering"
    return
  fi

  local pending=()
  local skipped=0

  for story in "${STORIES[@]}"; do
    if is_story_committed "$story"; then
      ((skipped++)) || true
    else
      pending+=("$story")
    fi
  done

  if [ $skipped -gt 0 ]; then
    log INFO "Skipped $skipped already-committed stories"
  fi

  if [ ${#pending[@]} -eq 0 ]; then
    log OK "All ${#STORIES[@]} stories already committed. Nothing to do."
    exit 0
  fi

  STORIES=("${pending[@]}")
}

# ── Extract story title from file ─────────────────────────────────────────────
story_title() {
  local story_path="$1"
  local root="${2:-$PROJECT_ROOT}"
  grep -m1 '^# ' "${root}/${story_path}" 2>/dev/null \
    | sed 's/^# Story [A-Z0-9.]*: //' \
    | sed 's/^# //' \
    || basename "$story_path" .md
}

# ==============================================================================
# Sequential mode (--parallel 1, default)
# ==============================================================================

# ── Run Claude with clean context ─────────────────────────────────────────────
run_claude() {
  local description="$1"
  local prompt="$2"
  local log_file="$3"
  local work_dir="${4:-$PROJECT_ROOT}"

  if [ "$DRY_RUN" = true ]; then
    log INFO "[DRY RUN] Would execute: claude -p \"${prompt:0:80}...\""
    return 0
  fi

  log STEP "$description"
  log INFO "Log: $log_file"

  if (cd "$work_dir" && env -u CLAUDECODE claude --dangerously-skip-permissions -p "$prompt" < /dev/null > "$log_file" 2>&1); then
    if grep -q "Unknown skill" "$log_file"; then
      log ERROR "$description — skill not found (check skill name)"
      log ERROR "Log contains: $(grep 'Unknown skill' "$log_file" | head -1)"
      return 1
    fi
    log OK "$description — completed"
    return 0
  else
    local exit_code=$?
    log ERROR "$description — failed (exit code: $exit_code)"
    log ERROR "Check log: $log_file"
    return $exit_code
  fi
}

phase_dev() {
  local story_path="$1"
  local work_dir="${2:-$PROJECT_ROOT}"
  local story_name; story_name=$(basename "$story_path" .md)

  local dev_log="${LOG_DIR}/${TIMESTAMP}-dev-${story_name}.log"
  run_claude \
    "Developing: $story_name" \
    "You are running in a fully autonomous pipeline — do NOT ask questions, do NOT wait for input. If the story file is missing task breakdowns, create them yourself and proceed immediately. Always choose option 1 (do it yourself). /bmad-dev-story $story_path" \
    "$dev_log" \
    "$work_dir" || return 1

  if [ "$DRY_RUN" = false ]; then
    if (cd "$work_dir" && git diff --quiet && git diff --staged --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]); then
      log ERROR "Dev produced no file changes for: $story_name"
      if grep -q "Unknown skill" "$dev_log"; then
        log ERROR "Cause: Unknown skill in log"
      fi
      return 1
    fi
    log OK "Dev produced changes for: $story_name"
  fi
}

phase_review() {
  local story_path="$1"
  local work_dir="${2:-$PROJECT_ROOT}"
  local story_name; story_name=$(basename "$story_path" .md)

  local review_log="${LOG_DIR}/${TIMESTAMP}-review-${story_name}.log"
  run_claude \
    "Code review: $story_name" \
    "/bmad-code-review $story_path" \
    "$review_log" \
    "$work_dir" || return 1

  log OK "Code review complete: $story_name"
}

phase_commit() {
  local story_path="$1"
  local work_dir="${2:-$PROJECT_ROOT}"
  local story_name; story_name=$(basename "$story_path" .md)

  if [ "$DRY_RUN" = true ]; then
    log INFO "[DRY RUN] Would commit changes for: $story_name"
    return 0
  fi

  if (cd "$work_dir" && git diff --quiet && git diff --staged --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]); then
    log WARN "No changes to commit for: $story_name"
    return 0
  fi

  local title
  title=$(story_title "$story_path" "$work_dir")

  (cd "$work_dir" && git add -A && git commit -m "$(cat <<EOF
feat(epic): implement $title

Story: $story_path
EOF
)")
  log OK "Committed: $story_name"
}

run_sequential() {
  local succeeded=0 failed=0
  local total=${#STORIES[@]}

  for i in "${!STORIES[@]}"; do
    local story="${STORIES[$i]}"
    local num=$((i + 1))
    local story_name; story_name=$(basename "$story" .md)

    separator "[$num/$total] $story_name"

    if ! phase_dev "$story"; then
      log ERROR "Dev failed: $story_name"
      ((failed++)) || true
      continue
    fi

    if [ "$NO_REVIEW" = false ]; then
      phase_review "$story" || log WARN "Review had issues: $story_name (continuing)"
    fi

    phase_commit "$story" || log WARN "Commit failed: $story_name"

    ((succeeded++)) || true

    if [ $num -lt $total ] && [ "$DRY_RUN" = false ]; then
      log INFO "Cooldown: ${COOLDOWN}s..."
      sleep "$COOLDOWN"
    fi
  done

  print_summary $succeeded $failed $total
}

# ==============================================================================
# Parallel mode (--parallel N, N > 1) — uses git worktrees
# ==============================================================================

# Track results per story: .worktrees/<name>.result = 0 (ok) or 1 (fail)
RESULTS_DIR=""

# Cleanup worktrees on exit (keeps unmerged branches safe)
cleanup_worktrees() {
  if [ -d "$WORKTREE_BASE" ]; then
    log INFO "Cleaning up worktrees..."
    for wt in "$WORKTREE_BASE"/*/; do
      [ -d "$wt" ] || continue
      local name; name=$(basename "$wt")
      # Remove worktree directory only (branch stays for manual merge)
      git -C "$PROJECT_ROOT" worktree remove --force "$wt" 2>/dev/null || true
    done
    rmdir "$WORKTREE_BASE" 2>/dev/null || true
  fi
  # Show unmerged branches if any remain
  local unmerged
  unmerged=$(git -C "$PROJECT_ROOT" branch --list 'epic/*' --no-merged main 2>/dev/null || true)
  if [ -n "$unmerged" ]; then
    log WARN "Unmerged branches (work preserved):"
    echo "$unmerged" | while read -r b; do echo "  $b"; done
    log INFO "Re-run to merge, or: git merge <branch>"
  fi
  if [ -n "$RESULTS_DIR" ] && [ -d "$RESULTS_DIR" ]; then
    rm -rf "$RESULTS_DIR"
  fi
}

# Process one story in an isolated worktree
process_story_worktree() {
  local story="$1"
  local story_name; story_name=$(basename "$story" .md)
  local branch="epic/${story_name}"
  local wt_path="${WORKTREE_BASE}/${story_name}"

  # Create branch + worktree
  if [ "$DRY_RUN" = true ]; then
    log INFO "[DRY RUN] [parallel] Would create worktree for: $story_name"
    log INFO "[DRY RUN] [parallel] Would develop: $story_name"
    [ "$NO_REVIEW" = false ] && log INFO "[DRY RUN] [parallel] Would review: $story_name"
    log INFO "[DRY RUN] [parallel] Would commit: $story_name"
    echo "0" > "${RESULTS_DIR}/${story_name}.result"
    return 0
  fi

  log STEP "Creating worktree: $story_name → $branch"
  # Reset branch to main if it already exists, or create fresh
  if git -C "$PROJECT_ROOT" rev-parse --verify "$branch" &>/dev/null; then
    git -C "$PROJECT_ROOT" branch -f "$branch" main 2>/dev/null || true
    log INFO "Reset existing branch: $branch"
  else
    if ! git -C "$PROJECT_ROOT" branch "$branch" main 2>&1; then
      log ERROR "[parallel] Failed to create branch: $branch"
      echo "1" > "${RESULTS_DIR}/${story_name}.result"
      return 1
    fi
  fi
  # Remove stale worktree if path exists
  if [ -d "$wt_path" ]; then
    git -C "$PROJECT_ROOT" worktree remove --force "$wt_path" 2>/dev/null || rm -rf "$wt_path"
  fi
  if ! git -C "$PROJECT_ROOT" worktree add "$wt_path" "$branch" 2>&1; then
    log ERROR "[parallel] Failed to create worktree: $wt_path"
    echo "1" > "${RESULTS_DIR}/${story_name}.result"
    return 1
  fi

  # Symlink .claude/ so skills are available in worktree
  if [ -d "${PROJECT_ROOT}/.claude" ] && [ ! -e "${wt_path}/.claude" ]; then
    ln -s "${PROJECT_ROOT}/.claude" "${wt_path}/.claude"
    # Exclude symlink from git so it doesn't get committed
    echo ".claude" >> "${wt_path}/.git/info/exclude"
  fi

  # Dev
  if ! phase_dev "$story" "$wt_path"; then
    log ERROR "[parallel] Dev failed: $story_name"
    echo "1" > "${RESULTS_DIR}/${story_name}.result"
    return 1
  fi

  # Review
  if [ "$NO_REVIEW" = false ]; then
    phase_review "$story" "$wt_path" || log WARN "[parallel] Review had issues: $story_name"
  fi

  # Commit in worktree branch
  phase_commit "$story" "$wt_path" || log WARN "[parallel] Commit failed: $story_name"

  echo "0" > "${RESULTS_DIR}/${story_name}.result"
  log OK "[parallel] Story complete: $story_name (branch: $branch)"
}

# Merge completed branches back to main in order
merge_branches() {
  local merged=0 merge_failed=0

  separator "Merging branches into main"
  cd "$PROJECT_ROOT"

  for story in "${STORIES[@]}"; do
    local story_name; story_name=$(basename "$story" .md)
    local branch="epic/${story_name}"
    local result_file="${RESULTS_DIR}/${story_name}.result"

    # Skip failed stories
    if [ -f "$result_file" ] && [ "$(cat "$result_file")" != "0" ]; then
      log WARN "Skipping merge (dev failed): $story_name"
      continue
    fi

    # Skip if branch has no new commits
    if ! git rev-parse --verify "$branch" &>/dev/null; then
      continue
    fi

    local title
    title=$(story_title "$story")

    log STEP "Merging: $branch → main"
    if git merge --no-ff "$branch" -m "$(cat <<EOF
feat(epic): implement $title

Story: $story
EOF
)"; then
      # Remove .claude symlink if it sneaked in
      if [ -L "${PROJECT_ROOT}/.claude" ] || git ls-files --error-unmatch .claude &>/dev/null; then
        git rm -f .claude &>/dev/null && git commit --amend --no-edit &>/dev/null || true
      fi
      log OK "Merged: $story_name"
      ((merged++)) || true
    else
      log ERROR "Merge conflict: $story_name — aborting merge"
      git merge --abort 2>/dev/null || true
      ((merge_failed++)) || true
      log WARN "Remaining stories not merged. Resolve conflict and re-run."
      break
    fi

    # Cleanup branch + worktree
    local wt_path="${WORKTREE_BASE}/${story_name}"
    git worktree remove --force "$wt_path" 2>/dev/null || true
    git branch -d "$branch" 2>/dev/null || true
  done

  log INFO "Merged: $merged, Failed: $merge_failed"
  return $merge_failed
}

run_parallel() {
  local total=${#STORIES[@]}
  RESULTS_DIR=$(mktemp -d)
  mkdir -p "$WORKTREE_BASE"

  # Register cleanup trap
  trap cleanup_worktrees EXIT

  log INFO "Parallel mode: $PARALLEL workers, $total stories"

  if [ "$DRY_RUN" = true ]; then
    for story in "${STORIES[@]}"; do
      process_story_worktree "$story"
    done
    print_summary "$total" 0 "$total"
    return 0
  fi

  # Launch workers with semaphore using a slot-based approach
  local pids=()
  local slots=()
  # Initialize slots
  for ((s=0; s<PARALLEL; s++)); do slots[$s]=0; done

  for i in "${!STORIES[@]}"; do
    local story="${STORIES[$i]}"
    local num=$((i + 1))
    local story_name; story_name=$(basename "$story" .md)

    # Find a free slot (wait for one if all busy)
    local found_slot=false
    while [ "$found_slot" = false ]; do
      for ((s=0; s<PARALLEL; s++)); do
        if [ "${slots[$s]}" -eq 0 ]; then
          found_slot=true
          break
        fi
        # Check if the process in this slot has finished
        if ! kill -0 "${slots[$s]}" 2>/dev/null; then
          wait "${slots[$s]}" 2>/dev/null || true
          slots[$s]=0
          found_slot=true
          break
        fi
      done
      if [ "$found_slot" = false ]; then
        sleep 1
      fi
    done

    log INFO "Launching [$num/$total]: $story_name (slot $s)"
    process_story_worktree "$story" &
    slots[$s]=$!
    pids+=($!)
  done

  # Wait for all remaining
  local remaining=0
  for ((s=0; s<PARALLEL; s++)); do
    [ "${slots[$s]}" -ne 0 ] && ((remaining++)) || true
  done
  log INFO "Waiting for $remaining remaining workers..."
  for pid in "${pids[@]}"; do
    wait "$pid" 2>/dev/null || true
  done

  # Count results
  local succeeded=0 failed=0
  for story in "${STORIES[@]}"; do
    local story_name; story_name=$(basename "$story" .md)
    local result_file="${RESULTS_DIR}/${story_name}.result"
    if [ -f "$result_file" ] && [ "$(cat "$result_file")" = "0" ]; then
      ((succeeded++)) || true
    else
      ((failed++)) || true
    fi
  done

  log OK "All workers done. Succeeded: $succeeded, Failed: $failed"

  # Merge branches sequentially into main
  if [ $succeeded -gt 0 ]; then
    merge_branches
  fi

  print_summary $succeeded $failed $total
}

# ==============================================================================
# Common
# ==============================================================================

print_summary() {
  local succeeded=$1 failed=$2 total=$3
  separator "Pipeline Complete"
  echo -e "  ${GREEN}Succeeded:${NC} $succeeded"
  echo -e "  ${RED}Failed:${NC}    $failed"
  echo -e "  ${BOLD}Total:${NC}     $total"
  [ "$PARALLEL" -gt 1 ] && echo -e "  ${CYAN}Workers:${NC}   $PARALLEL"
  echo ""
  log INFO "Logs: $LOG_DIR/"
  [ "$failed" -gt 0 ] && return 1
  return 0
}

# ── Parse Arguments ───────────────────────────────────────────────────────────
parse_args() {
  while [[ $# -gt 0 ]]; do
    case "$1" in
      --phase1)
        PHASE_FILTER="1"
        shift
        ;;
      --phase2)
        PHASE_FILTER="2"
        shift
        ;;
      --phase3)
        PHASE_FILTER="3"
        shift
        ;;
      --phase4)
        PHASE_FILTER="4"
        shift
        ;;
      --parallel)
        PARALLEL="$2"
        if ! [[ "$PARALLEL" =~ ^[0-9]+$ ]] || [ "$PARALLEL" -lt 1 ]; then
          log ERROR "--parallel must be a positive integer"
          exit 1
        fi
        shift 2
        ;;
      --epic)
        EPIC_FILTER="$2"
        shift 2
        ;;
      --from)
        FROM_PREFIX="$2"
        shift 2
        ;;
      --stories)
        shift
        while [[ $# -gt 0 && ! "$1" =~ ^-- ]]; do
          STORIES+=("$1")
          shift
        done
        ;;
      --no-review)
        NO_REVIEW=true
        shift
        ;;
      --force)
        FORCE=true
        shift
        ;;
      --merge-only)
        MERGE_ONLY=true
        shift
        ;;
      --cooldown)
        COOLDOWN="$2"
        shift 2
        ;;
      --dry-run)
        DRY_RUN=true
        shift
        ;;
      --log-dir)
        LOG_DIR="$2"
        shift 2
        ;;
      --help|-h)
        usage
        ;;
      *)
        log ERROR "Unknown option: $1"
        usage
        ;;
    esac
  done
}

# ── Main ──────────────────────────────────────────────────────────────────────
main() {
  parse_args "$@"

  mkdir -p "$LOG_DIR"
  check_prerequisites
  cd "$PROJECT_ROOT"

  # Merge-only mode: merge existing epic/* branches into main
  if [ "$MERGE_ONLY" = true ]; then
    separator "Merge-only mode"
    # Discover stories from existing epic/* branches
    local branches
    branches=$(git branch --list 'epic/*' 2>/dev/null | sed 's/^[* ]*//')
    if [ -z "$branches" ]; then
      log OK "No epic/* branches to merge."
      exit 0
    fi
    # Build STORIES array from branches
    STORIES=()
    while IFS= read -r branch; do
      local story_name="${branch#epic/}"
      local story_path="_bmad-output/implementation-artifacts/${story_name}.md"
      STORIES+=("$story_path")
      echo -e "  ${BOLD}-${NC} $story_name"
    done <<< "$branches"
    echo ""
    RESULTS_DIR=$(mktemp -d)
    # Mark all as succeeded (they were already developed)
    for story in "${STORIES[@]}"; do
      local sn; sn=$(basename "$story" .md)
      echo "0" > "${RESULTS_DIR}/${sn}.result"
    done
    merge_branches
    rm -rf "$RESULTS_DIR"
    # Cleanup merged branches
    local remaining
    remaining=$(git branch --list 'epic/*' 2>/dev/null | wc -l)
    log INFO "Branches remaining: $remaining"
    exit 0
  fi

  # Discover stories if not provided explicitly
  if [ ${#STORIES[@]} -eq 0 ]; then
    discover_stories
  fi

  # Filter out already-committed
  local total_before=${#STORIES[@]}
  filter_committed
  local total_pending=${#STORIES[@]}

  # Show execution plan
  local mode_label="sequential"
  [ "$PARALLEL" -gt 1 ] && mode_label="parallel ($PARALLEL workers)"
  separator "Pipeline: $total_pending pending / $total_before total [$mode_label]"
  log INFO "Timestamp:  $TIMESTAMP"
  log INFO "Mode:       $mode_label"
  log INFO "Review:     $([ "$NO_REVIEW" = true ] && echo 'off' || echo 'on')"
  [ "$PARALLEL" -eq 1 ] && log INFO "Cooldown:   ${COOLDOWN}s"
  log INFO "Dry Run:    $DRY_RUN"
  [ -n "$EPIC_FILTER" ] && log INFO "Epic:       $EPIC_FILTER"
  [ -n "$FROM_PREFIX" ] && log INFO "From:       $FROM_PREFIX"
  echo ""

  for i in "${!STORIES[@]}"; do
    echo -e "  ${BOLD}$((i + 1)).${NC} $(basename "${STORIES[$i]}" .md)"
  done
  echo ""

  # Run
  if [ "$PARALLEL" -gt 1 ]; then
    run_parallel
  else
    run_sequential
  fi

  local exit_code=$?
  exit $exit_code
}

main "$@"
