# Pr Stats

> Summarize a GitHub user's pull-request activity over a time window into one markdown report — per-PR metadata, lines/files/commits, human-reviewer-comment counts (bots filtered), a summary of each linked GitHub issue, and (when configured) a link to each PR's external-tracker ticket. Use when asked for "PR stats", a "PR report", "summarize my GitHub work", "what did I ship in the last X", or given a PR number/URL for a single-PR writeup. Defaults the author to the authed gh account and the window to the last 14 days.

- Skill: `samuelbostic29/pr-stats` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add samuelbostic29/pr-stats`
- Raw SKILL.md: https://api.skillmd.com/api/skills/samuelbostic29/pr-stats/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Docs & Writing
- Author: SamuelBostic29 (https://skillmd.com/u/samuelbostic29)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/samuelbostic29/pr-stats

---


# PR Stats: summarize a user's GitHub PR activity into one report

You are producing a single markdown report of the pull requests one GitHub user authored in a time window — aggregate totals plus a per-PR breakdown, including a short "what was asked" summary of each linked GitHub issue and, when the adopter configures a tracker, a link to each PR's external ticket. You gather read-only from GitHub via `gh` and write exactly one local file. You never post, comment, push, or open anything.

Two failure modes to guard against: **(1) silently running as the wrong account** when several are authed — resolve the user up front (Step 1) and switch at most once; and **(2) baking in anything personal** — user, scope, output path, tracker, and automation accounts are all resolved at runtime or configurable, never hardcoded.

## Configuration (optional — set per adopter)

| Key | Default | Used for |
| --- | --- | --- |
| Output dir | first non-comment line of `output-dir.txt` in this skill's folder, else `~/pr-stats/` | Where reports are written (`output-dir.txt` is git-ignored; ships as `output-dir.txt.example`) |
| `TICKET_KEY_PATTERN` | `[A-Z][A-Z0-9]+-\d+` | Recognizing an external-tracker ticket key in a PR's head branch or title |
| `TRACKER_URL_BASE` | unset | Rendering a per-PR ticket link (`<TRACKER_URL_BASE>/<KEY>`). Unset = external-tracker linking off entirely |

## When to use this skill

- "PR stats", "PR report", "PR tracking", "summarize my GitHub work"
- "What did I ship in the last X days/weeks?", "all my PRs since `<date>`"
- A PR number, `owner/repo#N`, or PR URL → a single-PR writeup

## When NOT to use this skill

- Posting, commenting on, or editing anything on GitHub — this skill is read-only and produces one local file.
- Reporting on issues, commits, or CI not tied to PR authorship.
- A trivial "is this PR merged?" status check — just `gh pr view` it; this skill writes a full report.

## Steps

### Step 1 — Resolve the GitHub user (the ONLY place this skill touches accounts)

1. Enumerate authed accounts: run `gh auth status` and read the login from every `Logged in to github.com` line — accept both the current `account <login>` and the older `as <login>` wordings (grab the login token either way); note which shows `Active account: true`.
2. Determine the **account to report on** — always one of the authed accounts (this skill reports on an account it is logged in as):
   - If the request named a username **matching an authed account**, use it (skips the prompt).
   - If the request named a username that is **not** an authed account, stop and explain that this skill reports only on an account it's authed as — have the user pick an authed account or `gh auth login` as that user.
   - Else if exactly **one** account is authed, use it — no prompt.
   - Else (**two or more**), ask via `AskUserQuestion` which authed account to run as (one option per login, the active one marked).
3. **Switch once, here:** if the target isn't already the active account, run `gh auth switch --user <target>`. This is the *only* `gh auth switch` the skill performs.
4. If **zero** accounts are authed, stop and tell the user to run `gh auth login`.

The resolved login is the report's `--author` and the account every later `gh` call runs under. **Do not switch accounts again anywhere below.**

### Step 2 — Resolve scope, window, output, and mode

- **Scope (optional):** if the user named an owner/org, capture it as `<OWNER>`; otherwise scope is *all repos* the search returns for the user.
- **Window:** convert the user's date phrasing to absolute `YYYY-MM-DD` for `--since`/`--until`. Default: the last 14 days ending today (use the date from the system context as today). If only a start is given, end = today.
- **Output dir:** an explicit directory named in the request always wins; otherwise resolved in Step 3 per Configuration.
- **Extra automation accounts (optional):** any non-bot automation logins the user wants excluded (e.g. an org's security scanner) — fold these into the exclusion list in Step 6.
- **Mode:** a PR number / `owner/repo#N` / PR URL → **single-PR mode** (skip the Step 4 search; one-PR report, no aggregates). Otherwise **bulk mode**.

### Step 3 — Resolve the output directory and ensure it exists

Skip the config read if the request named an explicit directory. Otherwise read the configured default from `output-dir.txt` (first non-comment, non-empty line), falling back to `~/pr-stats/`. Quote the path on every use — a configured default may contain spaces.

```bash
CFG="<skill-base>/output-dir.txt"   # <skill-base> = this skill's base directory, printed at invocation
OUTPUT_DIR="${OUTPUT_DIR:-$(grep -vE '^[[:space:]]*(#|$)' "$CFG" 2>/dev/null | head -1)}"
[ -z "$OUTPUT_DIR" ] && OUTPUT_DIR="$HOME/pr-stats"
mkdir -p "$OUTPUT_DIR"
```

### Step 4 — Find the PRs (bulk mode)

```
gh search prs --author <USER> --created "<SINCE>..<UNTIL>" --limit 1000 \
  --json number,title,repository,state,createdAt,closedAt,url
```

- If a scope `<OWNER>` was given, filter client-side to entries whose `repository.nameWithOwner` starts with `<OWNER>/`. (Combining `--owner` with `--author` on `gh search prs` is unreliable — filter in code instead.)
- 0 results → write a minimal "no PRs in window" report and stop at Step 10.
- If the result count reaches the `--limit` cap (1000 — `gh search`'s maximum), add a **"results may be incomplete"** note to the report header; don't do a second pass.

**Single-PR mode:** skip the search; take the given PR. If only a number was given, derive the repo from `git remote get-url origin`; if that fails (not in a git clone, or origin isn't a GitHub URL), **ask for `owner/repo` via `AskUserQuestion`** instead of guessing. Then go straight to Step 5 for that one PR.

### Step 5 — Gather per-PR detail (parallelize)

For each PR `<owner>/<repo>#<num>`, issue these **in parallel within one tool-use block**; with many PRs, fan out across PRs too. Token cost is not a concern here; latency is — never serialize independent calls.

```
a. gh pr view <num> --repo <owner>/<repo> \
     --json number,title,state,createdAt,closedAt,mergedAt,url,body,headRefName,baseRefName,additions,deletions,changedFiles,commits,isDraft,author,labels
b. gh api 'repos/<owner>/<repo>/pulls/<num>/comments' --paginate \
     --jq '[.[] | {login:.user.login, user_type:.user.type, body, path, line, created_at}]'
c. gh api 'repos/<owner>/<repo>/issues/<num>/comments' --paginate \
     --jq '[.[] | {login:.user.login, user_type:.user.type, body, created_at}]'
d. gh api 'repos/<owner>/<repo>/pulls/<num>/files' --paginate \
     --jq '[.[] | {filename, additions, deletions, changes, status}]'
e. gh api 'repos/<owner>/<repo>/pulls/<num>/reviews' --paginate \
     --jq '[.[] | {login:.user.login, user_type:.user.type, body, state}]'
```

`--paginate` is required so high-activity PRs don't lose comments or files past the first page.

### Step 6 — Tally human-reviewer comments

Combine three sources per PR: inline review comments (5b), conversation comments (5c), and non-empty review bodies from the reviews API (5e). **Exclude** an entry if ANY of these holds:

- `user_type == "Bot"`
- login ends in `[bot]` or `-bot` (case-insensitive)
- login is in the generic automation list (case-insensitive): `github-actions`, `dependabot`, `codecov`, `codecov-commenter`, `mergify`, `renovate`, `snyk-bot`
- login is in the user-supplied extra-automation list from Step 2 — **some automation posts as `user.type == "User"`**, so this explicit list matters
- login == the resolved author (the author's own comments don't count)

Remaining entries are **human reviewer comments**. Group by login and count.

### Step 7 — Link work items: GitHub issues always, an external tracker only when configured

**Track A — linked GitHub issues (always on).** Run (case-insensitive, multiline) over `body`:

```
(?i)(?:closes|fixes|resolves):?\s+#(\d+)
```

Process **every** match (a PR can close several). The capture is the issue number; the issue is always in the PR's own repo — this skill doesn't handle cross-repo `owner/repo#N` references. For each:

```
gh issue view <num> --repo <owner>/<repo> \
  --json number,title,body,createdAt,closedAt,url,labels
```

On a failed lookup, write `_Linked issue #N could not be fetched_` and continue — never fail the whole report.

**Track B — external-tracker ticket link (only when `TRACKER_URL_BASE` is configured).** Many teams keep tickets outside GitHub; the key is usually structural on the PR:

1. **Branch first:** if the head branch's segment after the last `_` (or `/` or `-` prefix segment) matches `TICKET_KEY_PATTERN`, take it (e.g. `main_PROJ-17385` → `PROJ-17385`).
2. **Title fallback:** take a leading `TICKET_KEY_PATTERN` token from the title.
3. **Neither matches** → the PR has no external ticket; omit its ticket line (never fabricate one).

Dedupe keys across PRs (stacked PRs can share one) and render each as a **link line only** — `<TRACKER_URL_BASE>/<KEY>`. **Never fetch the external tracker** — fetching would couple this skill to a specific tracker's API and credentials; the link is the deliverable. With `TRACKER_URL_BASE` unset, skip Track B entirely.

### Step 8 — Summarize each linked GitHub issue

A single paragraph (4–6 sentences) per fetched issue, focused on: the problem it describes, **what was specifically asked of the implementer** (the requirement / acceptance criteria), and any constraints or dependencies that shaped the work. Don't quote the body verbatim, don't pad, don't restate metadata. If the issue body is under 200 characters, include it verbatim instead.

### Step 9 — Compute aggregate stats (bulk mode only)

First **classify each PR into exactly one state bucket** so drafts aren't double-counted (precedence): `merged` (has `mergedAt`) → `closed-without-merge` (closed, not merged) → `draft` (open and `isDraft`) → `open` (open and not draft). Both open and draft PRs are included — each counted once. Then, across the window: total PRs with that per-state breakdown; total additions/deletions; total files changed; total commits; average time-to-merge over merged PRs (days, 1 dp); average human-reviewer comments per PR (1 dp); repos worked in (`owner/repo: <count>`, descending).

### Step 10 — Generate and write the report, then stop

Build the full markdown in memory (template below), then write it **once** to `<OUTPUT_DIR>/report-<YYYYMMDD>-<HHmmss>.md` — the `<OUTPUT_DIR>` resolved in Step 3 — using local date/time so repeated runs don't collide. If Step 1 switched accounts, note that in the header. Print the absolute path. **Stop** — the file path is the entire deliverable.

## Report template

```markdown
# PR Stats Report

**User**: <USER>
**Scope**: <OWNER, or "all repos">
**Window**: <SINCE> to <UNTIL> (<N> days)
**Generated**: <YYYY-MM-DD HH:mm> UTC

---

## Summary

- **Total PRs**: <N> (<merged> merged, <open> open, <closed> closed-without-merge, <draft> draft)
- **Lines changed**: +<additions> / −<deletions>
- **Files changed**: <N>
- **Commits**: <N>
- **Avg time to merge**: <X.X> days
- **Avg human reviewer comments per PR**: <X.X>
- **Repos worked in**:
  - `<owner>/<repo>` — <count> PRs

---

## Pull Requests

### #<num> — <title>

- **Repo**: `<owner>/<repo>`
- **State**: <Merged | Open | Closed | Draft>
- **Branch**: `<headRefName>` → `<baseRefName>`
- **Created**: <YYYY-MM-DD>
- **Merged / Closed**: <YYYY-MM-DD> (<X> days) | _still open_
- **URL**: <url>
- **Ticket**: [<KEY>](<TRACKER_URL_BASE>/<KEY>)   ← only when Track B derived a key; omit otherwise
- **Volume**: <changedFiles> files, +<additions> / −<deletions>, <commits> commits

#### Commits
- <messageHeadline>

#### Files changed
- `<filename>` — +<add> / −<del>

(If more than 20 files: list the top 10 by total lines changed, then `_(+ N more files)_`. Truncate only the rendering, never the collected data.)

#### Human reviewer comments (<total> total)
- **@<login>** — <count> comments

(If 0, write `_None_`.)

#### Linked issue: #<num> — <issue title>
**URL**: <issue url>

<paragraph summary of what the issue asked the implementer to deliver>

(Repeat per linked issue. Omit the block entirely if the PR links none — never fabricate one.)

---

(repeat the entire `### #<num>` section per PR, separated by `---`)
```

In single-PR mode, emit one `### #<num>` section and omit the `## Summary` aggregates.

## Rules

### What to do

- **Resolve and switch the account exactly once, in Step 1.** No later step calls `gh auth switch` — the whole report is gathered under the single account chosen up front.
- **Parallelize independent `gh` calls** in one tool-use block; fan out across PRs. Latency matters, token cost does not.
- **Collect everything; truncate only the rendering.** Gather every comment, file, and commit; truncation rules apply only to the rendered report.
- **Use UTC dates throughout** — the GitHub API returns UTC; don't convert to local time in the report body (the filename timestamp may be local).

### What NOT to do

- **NEVER hardcode a username, org, output path, tracker host, or automation account into the procedure** — resolve the user (Step 1); the output dir and tracker come from Configuration; default the window; take scope + extra-bot accounts as input.
- **NEVER post, comment, push, or open anything.** Read-only; one local file is the only write.
- **NEVER fetch an external tracker.** Track B renders a link built from Configuration — no API calls, no credentials, no tracker coupling.
- **NEVER append to or merge with a prior report** — every run is a fresh, uniquely-timestamped file.
- **NEVER fabricate a linked issue or ticket** — no `closes/fixes/resolves` match means no issue block; no key match means no ticket line.

### Format discipline

- The deliverable is the written file and its printed path. No preamble, no inline dump of the report. Build, write, print the path, stop.

