Run a sprint health review
Table of Contents
This page documents a runbook — a named, repeatable composition of recipes and skills for a complete multi-step procedure. Each step references a recipe or skill by id-link.
Goal
Produce a System 2 sprint health review: scaffold the review task,
gather data, run the analysis, write the full findings into the task's
* Result, add a one-row summary to sprint.org, and land it as a
merged PR.
Preconditions
- You are on, or can branch from, the latest
main. - The current sprint's
sprint.orgexists and has a* Health Reviewsection (placeholder or existing summary table). - The sprint has a
sprint_health_review/subdirectory containingstory.org. ghis authenticated;compass.shis available.python3,gnuplotare available onPATH(required for chart generation in step 2).
Steps
In execution order:
Create a feature branch from latest main.
git fetch origin && git checkout -b feature/sprint-<N>-health-review-<number> origin/main
Generate the sprint charts. Do this for every review, mid-sprint and at close — the charts visualise the same load/velocity data the review analyses, and a sprint page with broken
file:image links is a defect. Generate the four chart PNGs into the sprint directory:./projects/ores.compass/compass.sh sprint charts --sprint <N>
One command extracts the data and renders all four PNGs. Requires
gnuplotonPATH. See How do I generate sprint health charts? for full details including the CMake target.Verify the charts rendered — check
doc/agile/versions/v0/sprint_<N>/forprs_commits.png,line_churn.png,pr_cycle.png, andstories_done.png. A flat-linestories_done.pngwhen the Stories table has several DONE entries is a data bug worth investigating before proceeding. The four PNGs are committed with the review (step 12), matching the convention used by every prior sprint.Regenerate the sprint timeline. Fill in all 20-minute buckets from the end of the last bucket up to now. Buckets covering the gap keep the board timeline chart current and give the review data it can reference. Run the generation loop and commit the new files together with the rest of the review (step 13).
Find the last existing bucket timestamp:
ls doc/agile/versions/v0/sprint_<N>/timeline/ | sort | tail -1
Then generate buckets from that end-time to now (example; adjust
STARTandENDto the actual gap):import subprocess, json, uuid, os from datetime import datetime, timedelta, timezone SPRINT = "sprint_<N>" TIMELINE_DIR = f"doc/agile/versions/v0/{SPRINT}/timeline" START = datetime(<YYYY>, <MM>, <DD>, <HH>, <MM>, tzinfo=timezone.utc) END = datetime(<YYYY>, <MM>, <DD>, <HH>, <MM>, tzinfo=timezone.utc) BUCKET = timedelta(minutes=20) existing = set(os.listdir(TIMELINE_DIR)) written = 0 t = START while t < END: t_end = t + BUCKET slug_from = t.strftime("%Y%m%dT%H%M") slug_to = t_end.strftime("%Y%m%dT%H%M") filename = f"{slug_from}-{slug_to}.org" if filename in existing: t = t_end; continue iso_from = t.strftime("%Y-%m-%dT%H:%M") iso_to = t_end.strftime("%Y-%m-%dT%H:%M") result = subprocess.run( ["./compass.sh", "timeline", "generate", "--from", iso_from, "--to", iso_to, "--format", "json"], capture_output=True, text=True) data = json.loads(result.stdout) docs = data.get("documents", []) prs = data.get("prs", []) local_from = t + timedelta(hours=1) local_to = t_end + timedelta(hours=1) title_from = local_from.strftime("%Y-%m-%d %H:%M") title_to = local_to.strftime("%H:%M") pr_merged = [p for p in prs if p.get("event") == "merged"] pr_opened = [p for p in prs if p.get("event") == "opened"] summary_parts = [] if pr_merged: summary_parts.append(f"{len(pr_merged)} PR(s) merged") if pr_opened: summary_parts.append(f"{len(pr_opened)} opened") if docs: summary_parts.append(f"{len(docs)} doc change(s)") summary = ("; ".join(summary_parts) + ".") if summary_parts else "No activity." stories = [d for d in docs if d.get("doc_type") == "story"] tasks = [d for d in docs if d.get("doc_type") == "task"] captures = [d for d in docs if d.get("doc_type") == "capture"] def row(d, prefix=""): title = d.get("title","").replace(prefix,"") uid = d.get("id","") event = d.get("event","updated") link = f"[[id:{uid}][{title}]]" if uid else title return f"| {link} | {event} | |" def pr_row(p): return f"| #{p.get('number','')} | {p.get('event','')} | {p.get('title','')} |" lines = [":PROPERTIES:", f":ID: {str(uuid.uuid4()).upper()}", ":END:", f"#+title: Timeline: {title_from} → {title_to}", "#+description: 20-minute timeline snapshot of agile activity in this window, from the consistent substrate (origin/main + GitHub).", "#+type: timeline", "#+level: cross", f"#+filetags: :timeline:{SPRINT}:v0:", f"#+created: {local_from.strftime('%Y-%m-%d')}", f"#+updated: {local_from.strftime('%Y-%m-%d')}", "", "* Summary", "", f"- {summary}", "", "* Stories", ""] if stories: lines += ["| Story | Event | Notes |","|-------+-------+-------|"] + [row(d,"Story: ") for d in stories] else: lines.append("- None.") lines += ["","* Tasks",""] if tasks: lines += ["| Task | Event | Notes |","|------+-------+-------|"] + [row(d,"Task: ") for d in tasks] else: lines.append("- None.") lines += ["","* Captures",""] lines += ([f"- {c.get('title','')}" for c in captures] if captures else ["- None."]) lines += ["","* Pull requests",""] if prs: lines += ["| PR | Event | Title |","|----+-------+-------|"] + [pr_row(p) for p in prs] else: lines.append("- None.") lines += ["","* Problems and suspicious decisions","","- None observed.","","* Audit","","- Auto-generated bucket; environment stamps unavailable.",""] with open(os.path.join(TIMELINE_DIR, filename),"w") as f: f.write("\n".join(lines)) written += 1 t = t_end print(f"Written {written} new buckets.")
Commit the new buckets with the review commit in step 13.
Determine the review number. Count existing
task_health_review_*.orgfiles in the sprint'ssprint_health_review/directory and increment.ls doc/agile/versions/v0/sprint_<N>/sprint_health_review/task_health_review_*.org 2>/dev/null | wc -l
Scaffold the health review task via compass. (If not already scaffolded — skip if the task was created before the review started.)
./projects/ores.compass/compass.sh add task \ --parent-dir doc/agile/versions/v0/sprint_<N>/sprint_health_review \ --slug health_review_<number> \ --title "Health review <number> — sprint <N> <mid-sprint|close> analysis" \ --description "System 2 health review <number> of sprint <N>."
Set
#+branch:to the feature branch. Wire the task intosprint_health_review/story.org* Tasks.Run the agile-review-sprint skill. Load the skill and follow its steps (Steps 1–3 of the skill: activate System 2, gather data, analyse). The skill's data-gathering commands:
# Sprint start date from sprint.org #+created: field git log --oneline --since=<sprint-start-date> | wc -l gh pr list --state merged --search "merged:>=<sprint-start-date>" --limit 100 | wc -l gh pr list --state open find doc/agile/versions/v0/sprint_<N> -name "story.org" | sort
- Write the full review into the task's
* Result. Follow skill Step 4b: all six sub-sections (goal alignment, sprint load, PR velocity, story and task balance, focus signal, overall verdict) go under* Resultin the task file, not insprint.org. Add a summary row to
sprint.org. Follow skill Step 4c: append one row to the* Health Reviewtable using the task's:ID::| <number> | <date> | day <N> | <OVERALL> | [[id:<task-uuid>][Health review <number> — sprint <N> analysis]] |
Perform shape completeness fixes. For every DONE task in the sprint, verify the three required shape fields and fill any that are missing:
#+branch:— look up the branch viagh pr view <PR#> --json headRefNameor git log. If the task was written directly onto main with no dedicated PR, notenone (committed directly)rather than leaving it blank.#+pr:— set to the PR number that merged this task's work. If there was no PR, notenonewith a reason.* PRstable — ensure it contains a[[url][#NNN]]row matching the#+pr:value. Empty rows in DONE tasks are always wrong.Waiting on/Next— set toNothing./None.for DONE tasks; stale prose here is a common artefact of status fields updated mid-task but not cleared on close.
Use:
# Find all DONE tasks missing #+branch: or #+pr: grep -rn '#+branch:$\|#+pr:$' doc/agile/versions/v0/sprint_<N>/ \ --include="task_*.org"
Apply fixes in the same commit as the review (step 12) or in a follow-up commit on the same branch.
- Deduplicate stories. Scan the sprint's
* Storiestable for duplicate or near-duplicate entries — two stories with the same or very similar titles, overlapping descriptions, or goals that would naturally be one story. If any are found, apply How do I deduplicate two documents? before closing the sprint: nominate the richer story as survivor, merge any unique acceptance criteria or tasks into it, delete the weaker story, and update all id-links. Note deduplication decisions in the health review task's* Resultunder a "Duplicates resolved" sub-heading. - Mark the task DONE. Set
State: DONEandNow: Complete.in the task's* Statustable. - Update the story if all tasks are DONE. If every task in
sprint_health_review/story.orgis DONE, set the story to DONE. Commit. Include the timeline buckets, the four chart PNGs, and all shape-completeness fixes in the same commit:
git add doc/agile/versions/v0/sprint_<N>/ git commit -m "[agile] Sprint <N> health review <number> — <overall-verdict>"Raise a PR targeting
main—compass pr createpushes the branch and records the PR on the task:./compass.sh pr create --title "[agile] Sprint <N> health review <number>" \ --summary "..." --change "..."
- Handle review comments per the Handle a PR review round runbook.
Postconditions
- A new
task_health_review_<number>.orgexists under the sprint'ssprint_health_review/directory with the full analysis in* Result. sprint.org* Health Reviewtable has one new summary row.- Every DONE task in the sprint has
#+branch:,#+pr:, and a populated* PRstable;Waiting onandNextare cleared. - The sprint timeline is current: 20-minute buckets from sprint open to
now exist in
doc/agile/versions/v0/sprint_<N>/timeline/. - The task is DONE; the story is DONE if all tasks complete.
- A merged PR carries the review on
main.
See also
- Sprint Reviewer — the skill that drives steps 6–8.
- How do I generate sprint health charts? — cmake recipe for step 2.
- Sprint health charts — what each chart measures and how to read it when forming the verdict.
- How do I deduplicate two documents? — step 9; resolves duplicate or near-duplicate stories.
- Handle a PR review round — step 14.
- Runbooks catalogue — all runbooks.
- Open a new sprint — the runbook that follows at sprint close.