# Pr Status

> Show enriched status of your open PRs — CI checks, approvals, unresolved review threads, and linked Jira discussion, with transition-driven suggested next actions.

- Skill: `flurdy/pr-status` (Agent Skill, multi-file: 20 files)
- Install (CLI): `npx skillmds@latest add flurdy/pr-status`
- Raw SKILL.md: https://api.skillmd.com/api/skills/flurdy/pr-status/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: flurdy (https://skillmd.com/u/flurdy)
- Updated: 2026-09-10
- Page: https://skillmd.com/skills/flurdy/pr-status

---


# PR Status

Show enriched status for all open PRs created by you across your GitHub org: CI checks, approvals, unresolved review threads, and linked Jira discussion. Also shows recently closed PRs.

The GitHub org is auto-detected from the current repo's `origin` remote, falling back to the most common org across a multi-repo workspace's member repos when the cwd repo has no GitHub remote of its own (a workspace root is often deliberately local-only). Override via the `PR_STATUS_ORG` env var or a first positional argument. When nothing resolves the scripts exit non-zero with an explanatory message rather than returning an empty list.

**The output of this skill is the rendered dashboard** (steps 3–6). Fetching without rendering is not a valid run.

## Usage

```
/pr-status
```

## Instructions

Re-run the fetch scripts and `date` on every invocation, even seconds after the last one — never reuse earlier output or extrapolate the timestamp.

### 1. Fetch

Open PRs (org-wide), one JSON object per line `{number, title, owner, repo}`:

```bash
~/.agents/skills/pr-status/scripts/gh-pr-list-open.sh
```

Recently closed PRs (3-day lookback, 4 on Tuesdays; optional second arg overrides), one JSON object per line `{number, title, owner, repo, closedAt}`:

```bash
~/.agents/skills/pr-status/scripts/gh-pr-list-closed.sh
```

### 2. Fetch PR details

Group the PRs (open and closed) by `owner/repo`; one batch GraphQL call per group:

```bash
~/.agents/skills/pr-status/scripts/gh-pr-details.sh {owner} {repo} {number1} {number2} ...
```

Output is a JSON array, one object per PR:

```json
[
  {
    "number": 123,
    "base": "main",
    "isDraft": false,
    "mergeState": "CLEAN",
    "reviewDecision": "APPROVED",
    "approvers": ["alice"],
    "unresolvedThreads": 2,
    "checksState": "SUCCESS",
    "lastPush": "2026-04-15T09:30:00Z",
    "mergeCommitSha": "abc123...",
    "mergeCommitAt": "2026-04-15T10:00:00Z",
    "mainChecksState": "SUCCESS",
    "readyAt": "2026-04-14T15:32:11Z"
  }
]
```

Map `checksState` values: `SUCCESS` → ✅ / `FAILURE` or `ERROR` → ❌ / `PENDING` or `EXPECTED` → ⏳ / null → `—`

If the batch script is unavailable, the per-PR scripts in the same directory (`gh-pr-checks.sh`, `gh-pr-reviews.sh`, `gh-pr-threads.sh`, `gh-pr-merge-state.sh`) or plain `gh pr view/checks` cover the same fields.

### 2b. Fetch the normalized feedback inventory

For each `owner/repo` group of open PRs, make one bounded read-only inventory call:

```bash
~/.agents/skills/pr-status/scripts/gh-pr-feedback.py {owner} {repo} {number1} {number2} ...
```

The schema-v1 JSON envelope contains `records`, `partial`, `errors`, and explicit caps. Records
cover inline review roots and replies, submitted review summaries/states, top-level conversation,
and changed-file CI annotations. A record carries stable `identity` plus `updatedAt`, `updateKey`,
`stateKey`, author kind, lifecycle, semantic type, actionability candidate status, bounded body/gist, and the
GitHub IDs needed by the correct response surface. Pending draft review comments are not observable
through normal APIs and are never represented as fetched feedback.

Treat `actionability: candidate` as a triage hint, not a validated claim or permission to act. Human
and bot feedback still needs agent judgment against the code and requirements. Suppress
self-authored, resolved, outdated, dismissed, approval, and automated-status records from suggested
actions while retaining them in the factual inventory. When `partial` is true, render the available
data, name each failed source, and do not mark absent feedback as handled.

The existing `unresolvedThreads` detail remains a compatibility fallback for other status consumers;
this skill uses inventory identities and update times—not count changes—to detect feedback events.
If the inventory helper is unavailable, fall back to the old count-only behavior and label feedback
identity as unavailable rather than issuing overlapping ad-hoc comment fetches.

### 3. Fetch linked Jira discussion

For every distinct Jira key found while rendering (open PRs: branch first, then title; recently closed PRs: title; using `/[A-Z]+-\d+/`), fetch its newest Jira comment. Include both open and recently closed PRs when they carry a ticket key; fetch each key once, even if multiple PRs use it. Calls may run in parallel:

```
mcp__jira__jira_get
  path: /rest/api/3/issue/{key}/comment
  queryParams:
    orderBy: -created
    maxResults: 1
  jq: '{total: .total, latest: (.comments[0] // null | if . == null then null else {author: .author.displayName, accountId: .author.accountId, created: .created} end)}'
```

For a key with comments, render `💬 {total} · {latest author first name or @accountId} · {relative age}`. Render `—` for zero comments and `?` if its lookup fails. Do not show comment bodies in this status dashboard. Jira comment failures are non-fatal: render all PR data and add `_Some linked Jira discussion could not be fetched._` beneath the affected table(s).

### 4. Render as tables

Before the tables, output a timestamp line: `_Checked at HH:MM:SS_` in **local** time, 24h (`date '+%H:%M:%S'`, not `date -u`). Relative-time math against `lastPush` / `readyAt` / `closedAt` works in UTC since those are `Z`-suffixed; only the displayed timestamp is local.

**Recently closed** — render first, one table with a **Repo** column. Skip the section if the closed list is empty.

#### Recently closed

| PR | Repo | Ticket | Jira 💬 | Title | Status | CI | Ready | Wait | Closed |
|----|------|--------|---------|-------|--------|----|-------|------|--------|

- **PR**: markdown link `[#123](https://github.com/{owner}/{repo}/pull/123)`
- **Repo**: repository name
- **Ticket**: Jira ID matched by `/[A-Z]+-\d+/` against the PR title, or `—`
- **Status**: 🔀 merged / 🗑️ closed unmerged — emoji only
- **CI**: post-merge checks on the merge commit (`mainChecksState`), only if merged and `mergeCommitAt` is within the last 2 days — otherwise `—`
- **Ready**: relative time since `readyAt` (short units: `Nm`, `Nh`, `Nd`)
- **Wait**: `closedAt - readyAt` — how long the PR waited for review/merge
- **Closed**: relative time since close

**Open PRs** — render after closed, grouped by repo: heading `#### Open — {repo}` then a table per repo that has open PRs.

| PR | Ticket | Jira 💬 | Title | Branch | Target | Ready | Push | Sync | CI | Approved | Threads | LGTM |
|----|--------|---------|-------|--------|--------|-------|------|------|----|----------|---------|------|

- **PR**: markdown link as above
- **Ticket**: Jira ID matched against branch name first, then title, or `—`
- **Branch**: head branch minus conventional-commit prefix and ticket prefix (`feat/AB-1107-cta-clicked-event` → `cta-clicked-event`); truncate past ~30 chars with `…`
- **Target**: base branch. `main`/`master` are default branches — plain text. Anything else is a stacked PR: prefix with 📌
- **Sync**: from `mergeState` when base is `main`/`master`: `CLEAN`/`UNSTABLE` → ✅, `BEHIND` → ⚠️ behind, `DIRTY` → 💥 conflict, other → `—`. Stacked PRs (📌) → `—`
- **CI**: ✅ / ❌ / ⏳ — emoji only
- **Ready**: 🚧 if `isDraft`, else relative time since `readyAt`
- **Push**: relative time since `lastPush`
- **Approved**: one ✅ per approver when `reviewDecision` is `APPROVED`. `REVIEW_REQUIRED` with approvers → stale approvals, one ☑️ each. `CHANGES_REQUESTED` → 👎. No approvers, no changes requested → 🔔 (awaiting review; `—` for drafts)
- **Threads**: `💬 N` if N > 0, else `—`
- **LGTM**: 🚀 if not draft, `APPROVED`, CI `SUCCESS`, sync ✅, zero threads, and `mergeState` `CLEAN`; else 🚧

Truncate titles: ~50 chars in the closed table, ~25 in the open table (13 columns — wide rows break Claude Code's table renderer).

### 5. Summarise changes

After the tables, list deltas since the last check in this session as bullets (e.g. `#6142 CI: ❌ → ✅`). Treat an increase in a linked ticket's comment total as a delta, e.g. `AB-649 Jira discussion: 2 → 3 (Jane, 40m)`. Do not report a count decrease as a discussion update.

For PR feedback, compare the inventory by `identity`; the same identity with a later `updatedAt` is
an edit, not a second comment. Use `stateKey` to detect lifecycle-only changes such as resolution or
outdating; they may be shown as deltas but are not new actionable candidates. Report each new or
materially edited candidate once with PR, source, author, lifecycle, semantic type, and bounded
gist. Never infer identity from a
thread count. If a partial fetch prevents comparison, say so. Otherwise say "No changes." Render
both tables in full either way — the point of repeated checks is current state at a glance.

### 6. Suggest next actions (transition-driven)

Surface a **Suggested actions** bullet list — copy-pasteable commands for PRs that *just became* actionable this tick. Read-only: point at commands, never run them.

Fire on transitions, not standing state: on the first check of a session list the currently-actionable PRs as baseline; on later ticks list a PR only when it crosses into an actionable state.

| Transition this tick | Suggested command |
|---|---|
| → 🚀 LGTM (newly mergeable) | `/ready-to-merge {n}` |
| new or materially edited feedback candidate, or → 👎 changes requested | `/review-comments {n}` |
| → ⚠️ behind (fell behind base) | `/rebase main` (on that PR's branch) |
| → 🔔 awaiting review (no longer draft, still no reviewers) | `gh pr edit {n} --repo {owner}/{repo} --add-reviewer <handle>` |

Order most actionable first (🚀 → 💬 → ⚠️ → 🔔); omit the section when no PR changed state.

Use the normalized record's author and bounded gist in the bullet. Keep its `identity` and
`updatedAt` in session state so later polls distinguish an edit from a duplicate. Do not issue a
second comment/thread query. When the inventory is partial, suggest a safe recheck and name the
failed source instead of claiming that the candidate set is complete.

### 7. Next-tick recommendation

End with one cadence line for `/watch-prs` to pace from (harmless on a one-shot run):

```
next-tick: {hot|warm|cold} (~{N}s) — {reason}
```

- **hot (~180s)** — CI ⏳ on any open non-draft PR, a push in the last ~5 min, or a transition this tick
- **warm (~600s)** — open non-draft PRs awaiting review or carrying threads, nothing in flight
- **cold (1200 → 1800s)** — nothing actionable soon; escalate 1200 → 1500 → 1800 across consecutive cold ticks, reset on any non-cold tick

If the fetch failed, emit `next-tick: warm (~600s) — incomplete fetch`. Keep the reason to a few words; no other pacing commentary.

If the invoking prompt asks you to reschedule via `ScheduleWakeup`, do that only after everything above is printed — the turn ends when it returns.

