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.org exists and has a * Health Review section (placeholder or existing summary table).
  • The sprint has a sprint_health_review/ subdirectory containing story.org.
  • gh is authenticated; compass.sh is available.
  • python3, gnuplot are available on PATH (required for chart generation in step 2).

Steps

In execution order:

  1. Create a feature branch from latest main.

    git fetch origin && git checkout -b feature/sprint-<N>-health-review-<number> origin/main
    
  2. 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 gnuplot on PATH. 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>/ for prs_commits.png, line_churn.png, pr_cycle.png, and stories_done.png. A flat-line stories_done.png when 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.

  3. 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 START and END to 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.

  4. Determine the review number. Count existing task_health_review_*.org files in the sprint's sprint_health_review/ directory and increment.

    ls doc/agile/versions/v0/sprint_<N>/sprint_health_review/task_health_review_*.org 2>/dev/null | wc -l
    
  5. 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 into sprint_health_review/story.org * Tasks.

  6. 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
    
  7. 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 * Result in the task file, not in sprint.org.
  8. Add a summary row to sprint.org. Follow skill Step 4c: append one row to the * Health Review table using the task's :ID::

    | <number> | <date> | day <N> | <OVERALL> | [[id:<task-uuid>][Health review <number> — sprint <N> analysis]] |
    
  9. 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 via gh pr view <PR#> --json headRefName or git log. If the task was written directly onto main with no dedicated PR, note none (committed directly) rather than leaving it blank.
    • #+pr: — set to the PR number that merged this task's work. If there was no PR, note none with a reason.
    • * PRs table — ensure it contains a [[url][#NNN]] row matching the #+pr: value. Empty rows in DONE tasks are always wrong.
    • Waiting on / Next — set to Nothing. / 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.

  10. Deduplicate stories. Scan the sprint's * Stories table 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 * Result under a "Duplicates resolved" sub-heading.
  11. Mark the task DONE. Set State: DONE and Now: Complete. in the task's * Status table.
  12. Update the story if all tasks are DONE. If every task in sprint_health_review/story.org is DONE, set the story to DONE.
  13. 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>"
    
  14. Raise a PR targeting maincompass pr create pushes the branch and records the PR on the task:

    ./compass.sh pr create --title "[agile] Sprint <N> health review <number>" \
      --summary "..." --change "..."
    
  15. Handle review comments per the Handle a PR review round runbook.

Postconditions

  • A new task_health_review_<number>.org exists under the sprint's sprint_health_review/ directory with the full analysis in * Result.
  • sprint.org * Health Review table has one new summary row.
  • Every DONE task in the sprint has #+branch:, #+pr:, and a populated * PRs table; Waiting on and Next are 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

Emacs 29.1 (Org mode 9.6.6)