Sprint health charts
Table of Contents
Summary
Every sprint page carries four charts — PRs & Commits per Day,
Daily Line Churn, PR Cycle Time, and Cumulative Stories Done —
rendered by compass sprint charts from git history, the GitHub API,
and the sprint's own * Stories tables. They are activity
instruments: each measures one observable facet of sprint execution
so that drift (stalls, review bottlenecks, batched closures, scope
creep) becomes visible mid-sprint rather than at the retrospective.
This page defines each chart, what it measures, why we generate it,
and the patterns to read it by — and it is the literate home of the
gnuplot sources, which tangle to scripts/sprint_*.gnuplot where
compass sprint charts expects them.
Detail
The pipeline
compass sprint charts (see How do I generate sprint health
charts?) does everything in one step:
- Extract metrics into tab-separated CSVs under
build/output/sprint_NN/:sprint_activity.csv—day,merges,commits,added,deleted. One row per calendar day in the sprint window, fromgit logonorigin/main:mergescounts merge commits (one per merged PR),commitscounts all commits,added/deletedsum the--numstatline counts.pr_cycle_times.csv—pr_number,title,day,cycle_hours. From the GitHub API (gh pr list --state merged): hours from PR creation to merge, the twenty longest cycles with non-zero duration.sprint_progress.csv—day,cumulative_done. Parsed from the sprint page's own* Storiestables: each row whose End date falls inside the sprint counts as one story transitioning toDONE(see TODO state).
- Render each CSV through the four gnuplot scripts below, writing
the PNGs into the sprint directory
(
doc/agile/versions/v0/sprint_NN/), where the sprint page's* Chartssection displays them.
The PNGs are committed with the sprint page; every sprint follows this convention.
Reading the charts
The charts measure activity, not value — they are proxies. A quiet day may be a blocker or a design day; a churn spike may be progress or vendored code. They earn their keep by making questions specific ("why did nothing merge for three days?"), not by answering them. Interpretation belongs to the sprint health review, which reads them alongside goal alignment and the sprint mission.
PRs & Commits per Day
- Measures
- Delivery cadence. PRs merged into
mainper day (bars, left axis) against commits landing per day (line, right axis). - Purpose
- Shows the rhythm and batch size of sprint execution —
whether work is flowing to
mainin small, regular increments. - How to read it
- Healthy is a steady drumbeat of PRs across the sprint with a modest commits-to-PR ratio. A high ratio means large batches — many commits accumulating per merge — which often signals scope creep inside a task. Consecutive zero-PR days mean work in progress is piling up unmerged: a blocker, an oversized task, or review starvation.
Daily Line Churn
- Measures
- Lines added (green) and deleted (red) per day, from
git log --numstatonmain. - Purpose
- Characterises the kind of work the sprint is doing, not just its volume.
- How to read it
- Building work produces mostly additions; refactoring produces balanced add/delete; cleanup is deletion-heavy (deletions are usually good news — see the simplification themes in the agile process). Days with no churn at all may indicate blockers. Outsized spikes deserve a question: generated code, vendored dependencies, or a genuinely big landing?
PR Cycle Time
- Measures
- Hours from PR open to merge, one bar per PR — the twenty longest non-zero cycles in the sprint.
- Purpose
- Surfaces review latency. In this project's loop, review is where System 2 checks System 1 output (see sprint execution), so long cycles mean the checking function is the constraint.
- How to read it
- Most PRs should merge within hours. Long bars are review bottlenecks: a PR too large to review comfortably, a contentious design, or a PR parked while other work took priority. A few long bars among short ones is normal; a wall of long bars means the sprint is queueing on review. The chart only renders when PR data is available from the GitHub API.
Cumulative Stories Done
- Measures
- Running total of stories transitioning to
DONEduring the sprint, by the End date recorded in the sprint's* Storiestables. - Purpose
- The sprint's burn-up: story-level throughput against time, the closest thing to "are we going to land the mission?".
- How to read it
- A steady upward slope is healthy. A plateau
signals a stall — stories too large, a blocker, or effort spread
across many stories none of which is finishing. A hockey stick at
sprint end means closures were batched — stories were really done
earlier but only marked
DONEat closure, which makes the chart (and mid-sprint health reviews) lie; record End dates when the story's acceptance is actually met.
Chart sources
The gnuplot sources live here and tangle to
scripts/sprint_*.gnuplot, which is where compass sprint charts
invokes them. To regenerate the scripts after editing a block:
emacs -Q --batch -l org --eval '(org-babel-tangle-file "doc/agile/sprint_charts.org")'
Each script receives the sprint number via gnuplot -e "sprint=NN"
and derives its input CSV and output PNG paths from it. All four
share the same conventions: pngcairo at 800×400, tab-separated
input, solid fill, light y-grid, legend outside right.
PRs & Commits per Day
# GENERATED FILE — tangled from doc/agile/sprint_charts.org.
# Edit the source block there and re-tangle; do not edit directly.
#
# PRs & Commits per Day — standalone chart
sprint_str = sprintf("%02d", sprint)
set terminal pngcairo size 800, 400 font "sans,11"
set output sprintf("doc/agile/versions/v0/sprint_%s/prs_commits.png", sprint_str)
set datafile separator "\t"
set style fill solid 0.7
set grid ytics lc rgb "#cccccc" lw 1
set border 3
set tics nomirror
set key outside right
set xlabel "Day"
set ylabel "PRs"
set y2label "Commits"
set ytics nomirror
set y2tics
set title sprintf("Sprint %d — PRs & Commits per Day", sprint)
set xtics rotate by -45 font "sans,8"
infile_act = sprintf("build/output/sprint_%s/sprint_activity.csv", sprint_str)
plot infile_act using 0:2:xticlabels(stringcolumn(1)) every ::1 title "PRs" with boxes lc rgb "#4A90D9", \
"" using 0:3 every ::1 axes x1y2 title "Commits" with linespoints lc rgb "#E67E22" lw 2 pt 7
Daily Line Churn
# GENERATED FILE — tangled from doc/agile/sprint_charts.org.
# Edit the source block there and re-tangle; do not edit directly.
#
# Line Churn — standalone chart
sprint_str = sprintf("%02d", sprint)
set terminal pngcairo size 800, 400 font "sans,11"
set output sprintf("doc/agile/versions/v0/sprint_%s/line_churn.png", sprint_str)
set datafile separator "\t"
set style fill solid 0.7
set grid ytics lc rgb "#cccccc" lw 1
set border 3
set tics nomirror
set key outside right
set xlabel "Day"
set ylabel "Lines"
set title sprintf("Sprint %d — Daily Line Churn", sprint)
set xtics rotate by -45 font "sans,8"
infile_act = sprintf("build/output/sprint_%s/sprint_activity.csv", sprint_str)
plot infile_act using 0:4:xticlabels(stringcolumn(1)) every ::1 with boxes lc rgb "#27AE60" title "Added", \
"" using 0:5 every ::1 with boxes lc rgb "#E74C3C" title "Deleted"
PR Cycle Time
# GENERATED FILE — tangled from doc/agile/sprint_charts.org.
# Edit the source block there and re-tangle; do not edit directly.
#
# PR Cycle Time — standalone chart, only run when PR data exists
sprint_str = sprintf("%02d", sprint)
set terminal pngcairo size 800, 400 font "sans,10"
set output sprintf("doc/agile/versions/v0/sprint_%s/pr_cycle.png", sprint_str)
set datafile separator "\t"
set style fill solid 0.7
set grid ytics lc rgb "#cccccc" lw 1
set border 3
set tics nomirror
set key outside right
set xlabel "PR number"
set ylabel "Hours"
set title sprintf("Sprint %d — PR Cycle Time (open → close)", sprint)
set xtics rotate by -45 font "sans,8"
infile_cycle = sprintf("build/output/sprint_%s/pr_cycle_times.csv", sprint_str)
plot infile_cycle using 0:4:xticlabels(stringcolumn(1)) every ::1 with boxes lc rgb "#E74C3C" title "Cycle Hrs"
Cumulative Stories Done
# GENERATED FILE — tangled from doc/agile/sprint_charts.org.
# Edit the source block there and re-tangle; do not edit directly.
#
# Cumulative Stories Done — standalone chart, only run when story data exists
sprint_str = sprintf("%02d", sprint)
set terminal pngcairo size 800, 400 font "sans,11"
set output sprintf("doc/agile/versions/v0/sprint_%s/stories_done.png", sprint_str)
set datafile separator "\t"
set style fill solid 0.7
set grid ytics lc rgb "#cccccc" lw 1
set border 3
set tics nomirror
set key outside right
set xlabel "Day"
set ylabel "Stories"
set title sprintf("Sprint %d — Cumulative Stories Done", sprint)
set xtics rotate by -45 font "sans,8"
infile_prog = sprintf("build/output/sprint_%s/sprint_progress.csv", sprint_str)
plot infile_prog using 0:2:xticlabels(stringcolumn(1)) every ::1 with linespoints lc rgb "#2C3E50" lw 3 pt 9 title "Done"
See also
- How do I generate sprint health charts? — the compass recipe; one command extracts the CSVs and renders all four PNGs.
- Run a sprint health review — the runbook that interprets these charts as part of the System 2 health check.
- Sprint Reviewer — the skill the health review runs.
- Agile process — the loop these instruments observe.
- Glossary — definitions of sprint, story, task, and TODO state.