pr-status · diff
v1.12.0 to v1.13.0
42 added, 21 removed. Audit A to A.
---
name: pr-status
description: Show enriched status of your open PRs — CI checks, approvals, unresolved review threads, and linked Jira discussion, with transition-driven suggested next actions.
- allowed-tools: "Bash(~/.agents/skills/pr-status/scripts/gh-pr-list-open.sh:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-list-closed.sh:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-details.sh:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-checks.sh:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-reviews.sh:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-threads.sh:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-merge-state.sh:*), Bash(gh pr list:*), Bash(gh pr checks:*), Bash(gh pr view:*), Bash(gh api:*), Bash(gh search:*), Bash(date:*), mcp__jira__jira_get"
+ allowed-tools: "Bash(~/.agents/skills/pr-status/scripts/gh-pr-list-open.sh:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-list-closed.sh:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-details.sh:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-feedback.py:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-checks.sh:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-reviews.sh:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-threads.sh:*), Bash(~/.agents/skills/pr-status/scripts/gh-pr-merge-state.sh:*), Bash(gh pr list:*), Bash(gh pr checks:*), Bash(gh pr view:*), Bash(gh api:*), Bash(gh search:*), Bash(date:*), mcp__jira__jira_get"
model-tier: standard
model: sonnet
effort: medium
- version: "1.12.0"
+ version: "1.13.0"
author: "flurdy"
---
# 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. Otherwise say "No changes." Render both tables in full either way — the point of repeated checks is current state at a glance.
+ 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}` |
- | unresolved threads increased, or → 👎 changes requested | `/review-comments {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) | `/request-review` |
Order most actionable first (🚀 → 💬 → ⚠️ → 🔔); omit the section when no PR changed state.
- For a PR whose unresolved-thread count increased this tick (only those), fetch who commented and a ~80-char gist for the bullet:
-
- ```bash
- gh api graphql -f query='
- query($owner:String!,$repo:String!,$pr:Int!){
- repository(owner:$owner,name:$repo){
- pullRequest(number:$pr){
- reviewThreads(last:10){
- nodes{ isResolved comments(last:1){ nodes{ author{login} body } } }
- }
- }
- }
- }' -f owner="{owner}" -f repo="{repo}" -F pr={number} \
- --jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | .comments.nodes[0] | {author: .author.login, gist: (.body | .[0:80])}]'
- ```
-
- If the fetch fails, fall back to the bare count (`💬 N new`).
+ 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.