BMAD-METHOD/bmad-core/tasks/calculate-sprint-metrics.md

14 KiB

calculate-sprint-metrics

Purpose

Calculate and display key sprint metrics with optimized performance, enhanced accuracy, and shell compatibility.

Task Execution

Step 0: Initialize Environment

# Source shared library for utilities
if [ -f ".bmad-core/utils/bmad-lib.sh" ]; then
  source .bmad-core/utils/bmad-lib.sh
elif [ -f "BMAD-METHOD/bmad-core/utils/bmad-lib.sh" ]; then
  source BMAD-METHOD/bmad-core/utils/bmad-lib.sh
else
  echo "Warning: bmad-lib.sh not found, using fallback mode"
  # Define minimal fallback functions
  get_yaml_field() {
    grep "^$2:" "$1" 2>/dev/null | cut -d: -f2- | sed 's/^ //' | head -1
  }
  progress_bar() {
    echo "[$1/$2]"
  }
fi

# Validate project exists
if ! validate_bmad_project 2>/dev/null; then
  echo "📊 SPRINT METRICS"
  echo "═════════════════"
  echo ""
  echo "⚠️ No BMAD project found"
  exit 0
fi

Step 1: Gather Sprint Information

# Determine current sprint (from most recent story or default)
current_sprint="Sprint 2"  # Default
latest_sprint=$(grep "^sprint:" .bmad/stories/*.yaml 2>/dev/null | 
                cut -d: -f2- | sed 's/^ //' | sort -u | tail -1)
[ -n "$latest_sprint" ] && current_sprint="$latest_sprint"

# Get today's date for calculations
today=$(date +%Y-%m-%d)

Step 2: Optimized Metrics Calculation (Single Pass)

Use AWK for efficient single-pass processing:

# Single pass through all story files for multiple metrics
eval $(awk '
BEGIN {
  # Initialize all counters
  total_stories = 0
  total_points = 0
  completed_stories = 0
  completed_points = 0
  wip_stories = 0
  wip_points = 0
  blocked_stories = 0
  ready_stories = 0
  ready_points = 0
}
FILENAME != prevfile {
  # Process previous file data
  if (prevfile != "") {
    total_stories++
    total_points += story_points
    
    if (story_status == "completed" || story_status == "done") {
      completed_stories++
      completed_points += story_points
    } else if (story_status == "in_progress" || story_status == "code_review" || story_status == "qa_testing") {
      wip_stories++
      wip_points += story_points
    } else if (story_status == "ready") {
      ready_stories++
      ready_points += story_points
    }
    
    if (is_blocked == "true") {
      blocked_stories++
    }
    
    # Track epics
    if (story_epic != "") {
      epic_total[story_epic]++
      epic_points[story_epic] += story_points
      if (story_status == "completed" || story_status == "done") {
        epic_completed[story_epic]++
        epic_completed_points[story_epic] += story_points
      }
    }
    
    # Track assignees
    if (story_assignee != "" && story_assignee != "null") {
      assignee_stories[story_assignee]++
      if (story_status == "in_progress" || story_status == "code_review") {
        assignee_active[story_assignee]++
      }
    }
  }
  
  # Reset for new file
  prevfile = FILENAME
  story_points = 0
  story_status = ""
  story_epic = ""
  story_assignee = ""
  is_blocked = "false"
}
/^status:/ { story_status = $2 }
/^points:/ { story_points = $2 }
/^epic:/ { story_epic = $2; for(i=3;i<=NF;i++) story_epic = story_epic " " $i }
/^assignee:/ { story_assignee = $2; for(i=3;i<=NF;i++) story_assignee = story_assignee " " $i }
/^blocked: true/ { is_blocked = "true" }
/^days_in_progress: [3-9]/ { aging_stories++ }
/^days_in_progress: [0-9][0-9]/ { aging_stories++ }
END {
  # Process last file
  if (prevfile != "") {
    total_stories++
    total_points += story_points
    
    if (story_status == "completed" || story_status == "done") {
      completed_stories++
      completed_points += story_points
    } else if (story_status == "in_progress" || story_status == "code_review" || story_status == "qa_testing") {
      wip_stories++
      wip_points += story_points
    } else if (story_status == "ready") {
      ready_stories++
      ready_points += story_points
    }
    
    if (is_blocked == "true") {
      blocked_stories++
    }
    
    if (story_epic != "") {
      epic_total[story_epic]++
      epic_points[story_epic] += story_points
      if (story_status == "completed" || story_status == "done") {
        epic_completed[story_epic]++
        epic_completed_points[story_epic] += story_points
      }
    }
  }
  
  # Output all metrics as shell variables
  print "total_stories=" total_stories
  print "total_points=" total_points
  print "completed_stories=" completed_stories
  print "completed_points=" completed_points
  print "wip_stories=" wip_stories
  print "wip_points=" wip_points
  print "blocked_stories=" blocked_stories
  print "ready_stories=" ready_stories
  print "ready_points=" ready_points
  print "aging_stories=" aging_stories+0
  
  # Calculate velocity metrics
  if (total_stories > 0) {
    completion_rate = (completed_stories * 100) / total_stories
    print "completion_rate=" int(completion_rate)
  } else {
    print "completion_rate=0"
  }
  
  # Output epic data for further processing
  for (epic in epic_total) {
    gsub(" ", "_", epic)  # Replace spaces with underscores for shell compatibility
    print "epic_" epic "_total=" epic_total[epic]
    print "epic_" epic "_completed=" epic_completed[epic]+0
    print "epic_" epic "_points=" epic_points[epic]+0
    print "epic_" epic "_completed_points=" epic_completed_points[epic]+0
  }
}
' .bmad/stories/*.yaml 2>/dev/null)

Step 3: Calculate Derived Metrics

# Calculate additional metrics
throughput_weekly=0
if [ "$completed_stories" -gt 0 ]; then
  # Assuming 2-week sprint
  throughput_weekly=$((completed_stories / 2))
fi

# Calculate average cycle time (simplified - would need timestamps in production)
avg_cycle_time="N/A"
if [ -f ".bmad/.metrics/cycle_times.log" ]; then
  avg_cycle_time=$(awk '{sum+=$1; count++} END {if(count>0) printf "%.1f", sum/count}' \
                   .bmad/.metrics/cycle_times.log 2>/dev/null || echo "N/A")
fi

# WIP limit analysis
wip_limit=8
wip_status="✅"
if [ "$wip_stories" -gt "$wip_limit" ]; then
  wip_status="🔴"
elif [ "$wip_stories" -ge $((wip_limit - 1)) ]; then
  wip_status="🟡"
fi

# Sprint health calculation
sprint_health="Healthy"
health_emoji="✅"
if [ "$blocked_stories" -gt 2 ] || [ "$aging_stories" -gt 3 ]; then
  sprint_health="At Risk"
  health_emoji="⚠️"
elif [ "$blocked_stories" -gt 0 ] || [ "$aging_stories" -gt 1 ]; then
  sprint_health="Needs Attention"
  health_emoji="🟡"
fi

Step 4: Gather Epic Progress Data

# Get unique epics and calculate progress
epic_progress=""
for epic_file in .bmad/stories/*.yaml; do
  epic_name=$(get_yaml_field "$epic_file" "epic" "")
  if [ -n "$epic_name" ] && [ "$epic_name" != "null" ]; then
    echo "$epic_name"
  fi
done | sort -u | while read -r epic; do
  [ -z "$epic" ] && continue
  
  # Count stories for this epic
  epic_total=$(grep -l "^epic: $epic$" .bmad/stories/*.yaml 2>/dev/null | wc -l)
  epic_done=$(grep -l "^epic: $epic$" .bmad/stories/*.yaml 2>/dev/null | \
              xargs grep -l "^status: completed\|^status: done" 2>/dev/null | wc -l)
  
  if [ "$epic_total" -gt 0 ]; then
    percentage=$((epic_done * 100 / epic_total))
    bar=$(progress_bar "$epic_done" "$epic_total" 10)
    check_mark=""
    [ "$percentage" -eq 100 ] && check_mark=" ✅"
    
    epic_progress="${epic_progress}$(printf "%-20s %s %3d%% (%d/%d)%s\n" \
                   "$epic" "$bar" "$percentage" "$epic_done" "$epic_total" "$check_mark")\n"
  fi
done

Step 5: Team Performance Analysis

# Analyze team performance
team_metrics=""
active_devs=0
total_capacity=0

# Get developer metrics
grep "^assignee:" .bmad/stories/*.yaml 2>/dev/null | \
  cut -d: -f2- | sed 's/^ //' | grep -v "null" | sort | uniq -c | \
  while read count dev; do
    [ -z "$dev" ] && continue
    active_devs=$((active_devs + 1))
    
    # Count active work for this developer
    active_count=$(grep -l "assignee: $dev" .bmad/stories/*.yaml 2>/dev/null | \
                   xargs grep -l "status: in_progress\|status: code_review" 2>/dev/null | wc -l)
    
    # Simple capacity calculation (2 stories optimal per dev)
    capacity_used=$((active_count * 50))  # Each story uses ~50% capacity
    [ "$capacity_used" -gt 100 ] && capacity_used=100
    
    team_metrics="${team_metrics}$dev: $active_count active ($capacity_used% capacity)\n"
    total_capacity=$((total_capacity + capacity_used))
  done

# Calculate average team capacity
avg_capacity=0
[ "$active_devs" -gt 0 ] && avg_capacity=$((total_capacity / active_devs))

Step 6: Display Comprehensive Metrics Dashboard

# Display header
echo ""
echo "📊 SPRINT METRICS"
echo "═════════════════"
echo ""
echo "SPRINT: ${current_sprint}"
echo "Date: $(date '+%B %d, %Y')"
echo ""

# Velocity metrics
echo "VELOCITY METRICS:"
echo "━━━━━━━━━━━━━━━━━"
echo "📈 Completed: ${completed_stories} stories (${completed_points} points)"
echo "📊 Work in Progress: ${wip_stories} stories (${wip_points} points)"
echo "📦 Ready Backlog: ${ready_stories} stories (${ready_points} points)"
echo "🎯 Sprint Progress: ${completion_rate}% complete"
echo ""

# Flow metrics
echo "FLOW METRICS:"
echo "━━━━━━━━━━━━"
if [ "$avg_cycle_time" != "N/A" ]; then
  echo "⏱️ Avg Cycle Time: ${avg_cycle_time} days"
else
  echo "⏱️ Avg Cycle Time: Calculating..."
fi
echo "🔄 Throughput: ~${throughput_weekly} stories/week"
echo "📊 Total Velocity: ${completed_points} points delivered"
echo ""

# Health indicators
echo "HEALTH INDICATORS:"
echo "━━━━━━━━━━━━━━━━━━"
echo "${wip_status} WIP Limit: ${wip_stories}/${wip_limit} stories"
if [ "$blocked_stories" -gt 0 ]; then
  echo "⚠️ Blocked Items: ${blocked_stories}"
fi
if [ "$aging_stories" -gt 0 ]; then
  echo "⏰ Aging Items: ${aging_stories} (>3 days in state)"
fi
echo "${health_emoji} Sprint Health: ${sprint_health}"
echo ""

# Epic progress
if [ -n "$epic_progress" ]; then
  echo "EPIC PROGRESS:"
  echo "━━━━━━━━━━━━━"
  echo -e "$epic_progress"
fi

# Team performance
if [ -n "$team_metrics" ]; then
  echo "TEAM PERFORMANCE:"
  echo "━━━━━━━━━━━━━━━━━"
  echo -e "$team_metrics"
  echo "📊 Avg Team Capacity: ${avg_capacity}%"
  echo ""
fi

# Sprint burndown visualization (simplified)
echo "SPRINT BURNDOWN:"
echo "━━━━━━━━━━━━━━━━"
echo "Start: ${total_stories} stories (${total_points} points)"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

# Visual burndown
remaining=$((total_stories - completed_stories))
burndown_bar=$(progress_bar "$completed_stories" "$total_stories" 20)
echo "Progress: $burndown_bar"
echo "Remaining: ${remaining} stories"

if [ "$remaining" -gt 0 ] && [ "$wip_stories" -gt 0 ]; then
  projected_spillover=$((remaining - wip_stories))
  [ "$projected_spillover" -lt 0 ] && projected_spillover=0
  echo "Projected spillover: ~${projected_spillover} stories"
fi
echo ""

# Recommendations
echo "RECOMMENDATIONS:"
echo "━━━━━━━━━━━━━━━━"
priority=1

# Generate prioritized recommendations
if [ "$blocked_stories" -gt 0 ]; then
  echo "${priority}. 🚨 Unblock ${blocked_stories} blocked story(ies) - Critical path items"
  priority=$((priority + 1))
fi

if [ "$aging_stories" -gt 2 ]; then
  echo "${priority}. ⚠️ Review ${aging_stories} aging stories - May need assistance"
  priority=$((priority + 1))
fi

if [ "$wip_stories" -gt "$wip_limit" ]; then
  echo "${priority}. 📝 Reduce WIP - Focus on completing before starting new work"
  priority=$((priority + 1))
fi

if [ "$ready_stories" -gt 5 ]; then
  echo "${priority}. 🎯 Large backlog (${ready_stories} stories) - Consider grooming session"
  priority=$((priority + 1))
fi

if [ "$avg_capacity" -gt 80 ]; then
  echo "${priority}. ⚡ Team at high capacity (${avg_capacity}%) - Monitor for burnout"
  priority=$((priority + 1))
fi

[ "$priority" -eq 1 ] && echo "✅ Sprint running smoothly - no immediate actions needed"

Step 7: Trend Analysis (Optional)

# If historical data exists, show trends
if [ -f ".bmad/.metrics/historical.log" ]; then
  echo ""
  echo "TREND vs LAST SPRINT:"
  echo "━━━━━━━━━━━━━━━━━━━━"
  
  # Read last sprint metrics
  last_velocity=$(tail -1 .bmad/.metrics/historical.log | cut -d',' -f2)
  last_completed=$(tail -1 .bmad/.metrics/historical.log | cut -d',' -f3)
  
  # Calculate trends
  velocity_trend="→"
  [ "$completed_points" -gt "$last_velocity" ] && velocity_trend="↑"
  [ "$completed_points" -lt "$last_velocity" ] && velocity_trend="↓"
  
  echo "Velocity: ${velocity_trend} $([ "$velocity_trend" = "↑" ] && echo "+")$((completed_points - last_velocity)) points"
  echo "Throughput: ${velocity_trend} from ${last_completed} to ${completed_stories} stories"
fi

Success Criteria

  • Metrics calculate in under 500ms
  • Single-pass processing for efficiency
  • Accurate calculations with error handling
  • Shell-agnostic implementation
  • Clear, actionable insights

Data Sources

  • Story YAML files in .bmad/stories/
  • Optional historical metrics in .bmad/.metrics/
  • Real-time calculation, no stale data

Error Handling

  • Graceful handling of missing data
  • Default values for all metrics
  • Continue with partial data
  • Clear error messages

Performance Optimizations

  • Single AWK pass for all metrics
  • Process substitution over subshells
  • Efficient file operations
  • Optional caching for large datasets

Notes

  • Leverages bmad-lib.sh utilities
  • Compatible with bash/zsh/sh
  • Supports projects of any size
  • Maintains read-only operations