---
name: gh-metrics
description: Use when asked how often a GitHub repo merges pull requests, average or median time to merge, PR throughput, review turnaround, open-PR backlog, or how often RFC/proposal issues get accepted and how long they take. Computes repository PR and issue/RFC metrics consistently via the gh CLI.
---

# PR & Issue/RFC Metrics

## Overview

Compute repository health metrics consistently instead of hand-rolling `gh` +
`jq` each time. One Python script wraps the GitHub CLI and reports merge rate,
cycle time, throughput, review timing, open-PR backlog, and RFC/issue
acceptance — with the same definitions and bot filtering every run.

**Prerequisite:** the GitHub CLI must be installed and authenticated
(`gh auth status`). The script shells out to `gh`; it stores no token itself.
It works on any repository the `gh` login can access — including private or
company/SSO-gated repos (with the `repo` token scope). Repos the user cannot
reach fail gracefully with a clear message.

## When to Use

- "How often does <repo> merge PRs?" / "What's the merge rate?"
- "Average / median time to merge a PR?"
- "How long until a PR gets its first review?" / review turnaround
- "How big is the open PR backlog and how old is it?"
- "How often do RFCs / proposals get accepted, and how long do they take?"

## Quick Reference

```bash
SCRIPT=scripts/gh_metrics.py   # relative to this skill directory

# Tier 1 — fast: merge rate, cycle time, throughput, open backlog
python3 $SCRIPT owner/repo

# Tier 2 — add review timing, PR size, review counts (batched GraphQL)
python3 $SCRIPT owner/repo --deep --sample 100

# Issue/RFC mode — acceptance, time to first response, time to close
python3 $SCRIPT owner/repo --issues --limit 800

# Filter RFCs by label instead of title, or analyze all issues
python3 $SCRIPT owner/repo --issues --label proposal
python3 $SCRIPT owner/repo --issues --all-issues

# Include bots/backports, raw JSON, skip backlog
python3 $SCRIPT owner/repo --include-bots
python3 $SCRIPT owner/repo --json
python3 $SCRIPT owner/repo --no-backlog
```

Run `python3 scripts/gh_metrics.py --help` for all flags.

## What Each Metric Means

| Metric | Definition |
|--------|------------|
| Merge rate | merged ÷ closed PRs (open PRs excluded — they aren't decided yet) |
| Cycle time | open → merge, for merged PRs; report **median** (mean is skewed by a long tail) |
| Throughput | merged PRs per 30 days across the sampled window |
| Open backlog | count and age (open → now) of currently open PRs |
| Time to first review | PR open → first submitted review (pickup time) |
| Review time | first review → merge |
| RFC acceptance | closed issue **with a linked merged PR** = accepted; closed without = declined/withdrawn |
| Time to first response | issue open → first comment by someone other than the author |

## Reading the Numbers Honestly

- **Report median first.** Cycle time has a long tail; the mean overstates the
  typical PR. Always show median, mean, and p90 together.
- **Survivorship bias.** Sampling recently *closed* PRs undercounts long-lived
  ones. The open-backlog age section exposes this — a fast merge-time median
  alongside a large backlog age means "PRs that merge, merge fast; many others
  sit open." Report both.
- **Bots default OFF.** dependabot/CI/backport PRs merge fast and at high rates,
  skewing merge rate and cycle time. Excluded by default; `--include-bots` to
  compare.
- **RFC acceptance is a heuristic, not ground truth.** "Closed + linked merged
  PR" misses RFCs implemented by an unlinked PR, and counts still-open RFCs as
  neither. State it as an estimate.

## Common Mistakes

- **Including open PRs in merge rate.** Merge rate is merged ÷ *closed*. Open
  PRs are undecided; including them corrupts the ratio.
- **Too small a `--limit` for RFCs.** RFCs are rare; the default window may not
  reach older ones. Raise `--limit` (issues are fetched newest-first) to cover
  more history.
- **Trusting `timelineItems.totalCount` for review counts.** It ignores the
  type filter. Use the `reviews` connection's own `totalCount` (the script
  already does).
- **Quoting the mean as "typical."** A 100-day outlier drags the mean; lead with
  the median.
