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

444 lines
14 KiB
Markdown

# calculate-sprint-metrics
## Purpose
Calculate and display key sprint metrics with optimized performance, enhanced accuracy, and shell compatibility.
## Task Execution
### Step 0: Initialize Environment
```bash
# 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
```bash
# 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:
```bash
# 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
```bash
# 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
```bash
# 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
```bash
# 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
```bash
# 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)
```bash
# 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