# SkillPatch skill: gh-review-requests

This skill fetches unread GitHub notifications for open, unmerged pull requests where a review is requested from a specified team or opened by a team member. It uses the GitHub CLI and a Python script (via `uv`) to filter PRs by team slug, then presents results in a formatted markdown table. A manual fallback using raw `gh api` calls is also provided.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/gh-review-requests
curl -sSL https://skillpatch.dev/install_skill/gh-review-requests | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (2)

- `SKILL.md`
- `scripts/fetch_review_requests.py`


### `SKILL.md`

````markdown
---
name: gh-review-requests
description: Fetch unread GitHub notifications for open PRs where review is requested from a specified team or opened by a team member. Use when asked to "find PRs I need to review", "show my review requests", "what needs my review", "fetch GitHub review requests", or "check team review queue".
allowed-tools: Bash
---

# GitHub Review Requests

Fetch unread `review_requested` notifications for open (unmerged) PRs, filtered by a GitHub team.

**Requires**: GitHub CLI (`gh`) authenticated.

**Requires**: The `uv` CLI for python package management, install guide at https://docs.astral.sh/uv/getting-started/installation/

## Step 1: Identify the Team

If the user has not specified a team, ask:

> Which GitHub team should I filter by? (e.g. `streaming-platform`)

Accept either a team slug (`streaming-platform`) or a display name ("Streaming Platform") — convert to lowercase-hyphenated slug before passing to the script.

## Step 2: Run the Script

```bash
uv run scripts/fetch_review_requests.py --org getsentry --teams <team-slug>
```

To filter by multiple teams, pass a comma-separated list:

```bash
uv run scripts/fetch_review_requests.py --org getsentry --teams <team slugs>
```

### Script output

```json
{
  "total": 3,
  "prs": [
    {
      "notification_id": "12345",
      "title": "feat(kafka): add workflow to restart a broker",
      "url": "https://github.com/getsentry/ops/pull/19144",
      "repo": "getsentry/ops",
      "pr_number": 19144,
      "author": "bmckerry",
      "reasons": ["opened by: bmckerry"]
    }
  ]
}
```

`reasons` will contain one or both of:
- `"review requested from: <Team Name>"` — the team is a requested reviewer
- `"opened by: <login>"` — the PR author is a team member

## Step 3: Present Results

Display results as a markdown table with full URLs:

| # | Title | URL | Reason |
|---|-------|-----|--------|
| 1 | feat(kafka): add workflow to restart a broker | https://github.com/getsentry/ops/pull/19144 | opened by: evanh |

If `total` is 0, say: "No unread review requests found for that team."

## Fallback

If the script fails, run manually:

```bash
gh api notifications --paginate
```

Then for each `review_requested` notification, check:
- `gh api repos/{repo}/pulls/{number}` — skip if `state == "closed"` or `merged_at` is set
- `gh api repos/{repo}/pulls/{number}/requested_reviewers` — check `teams[].name`
- `gh api orgs/{org}/teams/{slug}/members` — check if author is a member

````


### `scripts/fetch_review_requests.py`

```
# /// script
# requires-python = ">=3.12"
# dependencies = []
# ///
"""
Fetch unread GitHub review-requested notifications for open (unmerged) PRs,
filtered by team membership and/or team review requests.

Usage:
    uv run fetch_review_requests.py --org ORG --teams TEAM1,TEAM2

Arguments:
    --org     GitHub organization slug (default: getsentry)
    --teams   Comma-separated team slugs to filter by (e.g. streaming-platform)

Output: JSON to stdout
"""

import argparse
import json
import subprocess
import sys


def gh(path: str, paginate: bool = False) -> list | dict:
    cmd = ["gh", "api", path]
    if paginate:
        cmd.append("--paginate")
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0 or not result.stdout:
        print(f"Error running gh {' '.join(cmd)}: {result.stderr}", file=sys.stderr)
        return [] if paginate else {}
    return json.loads(result.stdout)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--org", default="getsentry")
    parser.add_argument("--teams", required=True, help="Comma-separated team slugs")
    args = parser.parse_args()

    team_slugs = [t.strip() for t in args.teams.split(",")]

    # Resolve team members for all specified teams
    members: set[str] = set()
    team_display_names: dict[str, str] = {}
    for slug in team_slugs:
        data = gh(f"orgs/{args.org}/teams/{slug}/members", paginate=True)
        for m in data:
            members.add(m["login"])
        # Get display name
        team_data = gh(f"orgs/{args.org}/teams/{slug}")
        team_display_names[slug] = team_data.get("name", slug)

    # Fetch unread notifications (GitHub API default: unread only)
    all_notifs = gh("notifications", paginate=True)
    review_notifs = [
        n for n in all_notifs
        if n["reason"] == "review_requested" and n["unread"]
    ]

    prs = []
    for n in review_notifs:
        url = n["subject"]["url"]
        repo_path = url.replace("https://api.github.com/repos/", "")
        repo = repo_path.rsplit("/pulls/", 1)[0]
        pr_num = repo_path.rsplit("/", 1)[-1]
        html_url = f"https://github.com/{repo}/pull/{pr_num}"

        pr_data = gh(f"repos/{repo}/pulls/{pr_num}")
        if not pr_data:
            continue

        # Skip merged or closed PRs
        if pr_data.get("merged_at") or pr_data.get("state") == "closed":
            continue

        author = pr_data["user"]["login"]

        reviewers_data = gh(f"repos/{repo}/pulls/{pr_num}/requested_reviewers")
        requested_team_names = [t["slug"] for t in reviewers_data.get("teams", [])]
        matching_teams = [
            t for t in requested_team_names
            if any(slug.lower() == t.lower() for slug in team_slugs)
        ]

        by_team_member = author in members
        review_from_team = len(matching_teams) > 0

        if not (by_team_member or review_from_team):
            continue

        reasons = []
        if review_from_team:
            reasons.append(f"review requested from: {', '.join(matching_teams)}")
        if by_team_member:
            reasons.append(f"opened by: {author}")

        prs.append({
            "notification_id": n["id"],
            "title": n["subject"]["title"],
            "url": html_url,
            "repo": repo,
            "pr_number": int(pr_num),
            "author": author,
            "reasons": reasons,
        })

    print(json.dumps({"total": len(prs), "prs": prs}, indent=2))


if __name__ == "__main__":
    main()

```
