From aac1bf645333e8a4a9afa4f49d31893590238eb2 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 16:58:26 +0000 Subject: [PATCH 01/14] feat(github): add triage-prs skill Package the previously personal triage-prs skill into the github plugin, made generic for reuse: - Remove the Todoist integration (no cross-plugin MCP dependency) - Keep the single scoped gh api call for removing review requests (the one operation the GitHub MCP server does not cover), degrading gracefully when gh is unavailable - Gate the needs-rebase auto-dismiss rule on Prow detection - Prefer the upstream remote over origin, with an optional owner/repo argument to override detection entirely - Resolve the git remote via dynamic context injection instead of a runtime Bash call - Replace list_commits (broken for fork PRs whose head branch is not in the base repo) with pull_request_read get_commits - Drop unused update_pull_request and the list_pull_requests probe (Prow detection now reuses the first collected PR) - Explicit invocation only (disable-model-invocation), since the skill dismisses notifications and mutes subscriptions Bump plugin to 1.3.0 and document the skill in the plugin and root READMEs. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- README.md | 2 +- github/.claude-plugin/plugin.json | 2 +- github/README.md | 15 +++ github/skills/triage-prs/SKILL.md | 172 ++++++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 github/skills/triage-prs/SKILL.md diff --git a/README.md b/README.md index ef3feb5..301d5d2 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ claude plugin marketplace add cblecker/claude-plugins | Plugin | Description | |----------------------------------------------|--------------------------------------------------------------------------------------------------| | [git](./git) | Dynamic git instructions via SessionStart hook with mainline detection, conventional commits, fork handling, and safety guardrails | -| [github](./github) | GitHub MCP server with all toolsets enabled, always loaded for immediate tool availability | +| [github](./github) | GitHub MCP server with all toolsets enabled, always loaded, plus a PR triage skill | | [pr-review-toolkit](./pr-review-toolkit) | Comprehensive PR review board using shared workflow context | | [gws](./gws) | Google Workspace CLI skills for Gmail, Calendar, Drive, Docs, Sheets, Slides, and Meet | | [rh-dataverse](./rh-dataverse) | Red Hat Dataverse MCP server | diff --git a/github/.claude-plugin/plugin.json b/github/.claude-plugin/plugin.json index 2fb7d41..bac1079 100644 --- a/github/.claude-plugin/plugin.json +++ b/github/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "github", "description": "GitHub MCP server with selected toolsets for repository management, code security, discussions, notifications, and more.", - "version": "1.2.0", + "version": "1.3.0", "author": { "name": "GitHub" }, diff --git a/github/README.md b/github/README.md index 29827cf..6fe8ae1 100644 --- a/github/README.md +++ b/github/README.md @@ -26,6 +26,21 @@ HTTP-based MCP server connected to `api.githubcopilot.com`. The server is config - **SessionStart** — instructs Claude to prefer GitHub MCP tools over the `gh` CLI and `WebFetch` for all GitHub operations +### Skills + +- **triage-prs** (`/github:triage-prs [owner/repo]`) — triages open PRs where you are + assigned or a requested reviewer. Investigates each PR with a parallel subagent, + auto-clears notifications for closed PRs (and, on Prow-managed repos, for + `needs-rebase` and `lgtm`+`approved` PRs), then presents batched options to + unassign, remove review requests, or unsubscribe. Explicit invocation only — + Claude never triggers it automatically. The target repo is taken from the + optional `owner/repo` argument, or detected from the `upstream` (preferred) + or `origin` git remote. Prow-managed repos are auto-detected and acted on via + bot commands (`/unassign`, `/uncc`); on other repos, removing a review request + requires an authenticated `gh` CLI (the one operation the GitHub MCP server + does not cover) — without it, that single action is reported for manual + follow-up instead. + ## Configuration This plugin requires the `GITHUB_PERSONAL_ACCESS_TOKEN` environment variable to be set with a valid GitHub Personal Access Token. diff --git a/github/skills/triage-prs/SKILL.md b/github/skills/triage-prs/SKILL.md new file mode 100644 index 0000000..f414d98 --- /dev/null +++ b/github/skills/triage-prs/SKILL.md @@ -0,0 +1,172 @@ +--- +name: triage-prs +description: >- + Triage open pull requests that need the user's attention — PRs where they are + assigned or a requested reviewer. Investigates each PR in parallel, + auto-clears notifications for closed or bot-blocked PRs, then presents + batched triage options to unassign, remove review requests, or unsubscribe. +disable-model-invocation: true +argument-hint: '[owner/repo]' +allowed-tools: + - Bash(gh api */pulls/*/requested_reviewers -X DELETE *) + - mcp__plugin_github_github__get_me + - mcp__plugin_github_github__search_pull_requests + - mcp__plugin_github_github__pull_request_read + - mcp__plugin_github_github__add_issue_comment + - mcp__plugin_github_github__issue_write + - mcp__plugin_github_github__list_notifications + - mcp__plugin_github_github__dismiss_notification + - mcp__plugin_github_github__manage_notification_subscription + - AskUserQuestion + - Agent +--- + +# Triage PRs + +Investigate open PRs that need your attention in the target repo, then present batched triage with options to unassign or unsubscribe. + +## Repo Context + +- Repo argument (overrides remote detection if non-empty): $ARGUMENTS +- Detected remote: !`git remote get-url upstream 2>/dev/null || git remote get-url origin 2>/dev/null || echo __NO_REMOTE__` + +## Phase 1: Setup + +**Determine repo identity:** +If the repo argument above is non-empty, parse it as `OWNER/REPO`. Otherwise parse `OWNER` and `REPO` from the detected remote URL — support both SSH (`git@github.com:OWNER/REPO.git`) and HTTPS (`https://github.com/OWNER/REPO`) forms. The `upstream` remote is preferred over `origin` so fork-based workflows triage the upstream repo, not the fork. If the remote is `__NO_REMOTE__` and no argument was given, print "No GitHub remote detected — re-run as `/github:triage-prs owner/repo`." and stop. + +**Get authenticated user:** +Call `mcp__plugin_github_github__get_me` → store login as `USERNAME`. Needed for identifying your reviews, Prow commands, and non-Prow API actions. + +## Phase 2: Collect PRs + +Run two searches in parallel using `@me` syntax: + +```text +search_pull_requests(query: "assignee:@me is:open", owner, repo) +search_pull_requests(query: "review-requested:@me is:open", owner, repo) +``` + +Deduplicate by PR number. Tag each PR with its roles: `assigned`, `reviewer`, or both. Paginate using `perPage: 100` if results are truncated. + +If no PRs found: print "No open PRs found where you are assigned or a requested reviewer in OWNER/REPO." and stop. + +**Detect Prow:** call `pull_request_read(method: "get_status")` on the first collected PR. Set `HAS_PROW = true` if any status context name contains `"tide"`, else `false`. + +## Phase 3: Parallel Investigation + +Dispatch one Agent per PR, all in parallel, using `model: "sonnet"`. Each agent prompt must be self-contained — pass `owner`, `repo`, `pr_number`, and `username` explicitly. + +Each agent performs these MCP calls: + +1. `pull_request_read(method: "get")` — title, author, draft status, labels, timestamps +2. `pull_request_read(method: "get_check_runs")` — aggregate to: X/Y passing, Z failing [names], W pending +3. `pull_request_read(method: "get_reviews")` — find your most recent review (state + date); note all other reviewers and their states; record whether USERNAME appears at all (any state) as `reviewed_before` +4. `pull_request_read(method: "get_files")` — file count, key filenames, total additions/deletions +5. `pull_request_read(method: "get_commits", perPage: 100)` — filter to commits dated after LAST_REVIEW_DATE; summarize via commit messages. Skip if not yet reviewed. +6. `pull_request_read(method: "get_review_comments")` — count your unresolved vs resolved review threads +7. `pull_request_read(method: "get_comments", perPage: 100)` — paginate through all issue comments (not just recent); note mentions or questions directed at you, and record whether USERNAME appears among comment authors as `commented_before` + +If the PR is merged or closed: return `"Merged/Closed"` and stop (this still counts as investigated — Phase 4 auto-dismisses it). + +**Return this exact format:** + +```text +PR #NUMBER: TITLE +Author: AUTHOR | Created: DATE | Updated: DATE | Draft: yes/no +Role: [assigned] [reviewer] +Labels: LABEL1, LABEL2 +State: open/closed/merged + +CI: X/Y passing, Z failing [CHECK_NAMES], W pending → green/red/yellow/pending +My Last Review: STATE on DATE (or: Not yet reviewed) +Previously Engaged: yes/no (reviewed_before OR commented_before) +Changes Since Review: N new commits — SUMMARY (or: None / N/A) +My Open Threads: N unresolved of M total +Other Reviews: REVIEWER (STATE), ... +Files: N files (+ADDS/-DELS) — KEY_FILENAMES +Recent Activity: SUMMARY + +Recommended Action: ACTION_CATEGORY +``` + +**Action categories** (pick highest-priority that applies): + +- **Author addressed feedback** — you requested changes; author pushed new commits and/or resolved threads +- **Review needed** — not yet reviewed by you +- **Re-review needed** — new commits since your last review (you hadn't requested changes) +- **CI failing** — CI is red +- **Ready to merge** — approved, CI green, no blockers +- **Waiting on author** — you requested changes, no new commits +- **Waiting on CI** — CI still running +- **Stale** — no activity in >7 days + +## Phase 4: Auto-Classification + +Before presenting anything to the user, run each investigated PR through these rules **in order**. The first matching rule wins; stop evaluating further rules for that PR once one matches. + +1. **Closed or merged** → `dismiss` +2. **Previously Engaged** (`reviewed_before` or `commented_before` is yes) → `keep` (you're already invested in this PR — leave its notification alone and skip triage entirely; don't ask about it, don't touch it) +3. **`HAS_PROW` is true, and `needs-rebase` label present** → `dismiss` +4. **`HAS_PROW` is true, and both `lgtm` and `approved` labels are present** (from anyone other than you — Previously Engaged already excluded you above) → `unsubscribe` +5. **Otherwise** → `manual` + +Execute the auto-classified actions now, per PR: + +- **`dismiss`**: `list_notifications(owner, repo)`, find the thread(s) whose `subject.url` matches this PR, `dismiss_notification(threadID, state: "done")`. Do **not** call `manage_notification_subscription`. +- **`unsubscribe`**: same lookup, then both `dismiss_notification(threadID, state: "done")` **and** `manage_notification_subscription(notificationID, action: "ignore")`. +- **`keep`**: no action at all — don't dismiss, don't touch the subscription, don't ask about it in Phase 5. The notification stays exactly as it is. +- **`manual`**: no action yet — these carry forward into Phase 5. + +> **Dismiss vs. unsubscribe:** `dismiss_notification(state: "done")` clears only the current notification thread — new PR activity creates a fresh notification later, which is what `dismiss` cases (closed, needs-rebase) want. Only `manage_notification_subscription(action: "ignore")` stops future notifications, so it is reserved for `unsubscribe` cases. + +Report auto-classified PRs in a short summary line before moving to Phase 5, e.g.: "Auto-dismissed 3 (2 closed, 1 needs-rebase). Auto-unsubscribed 1 (lgtm+approved). Kept 2 for review (already engaged)." + +## Phase 5: Triage + +Sort all `manual`-classified agent results by urgency (order matches the category list above). + +Present in batches of up to 4 using `AskUserQuestion` (tool supports 1–4 questions per call, 2–4 options each). Each question is one PR. + +**Question:** `PR #NUMBER: TITLE (by @AUTHOR) — RECOMMENDED_ACTION` + +**Description:** CI state, last review date, changes since review, other reviewer states + +**Options** (adapt by role): + +| Role | Options | +|------|---------| +| `assigned` only | Skip / Unassign me | +| `reviewer` only | Skip / Remove review request | +| both | Skip / Unassign me / Remove review request / Unsubscribe (both) | + +After each batch, continue to the next batch if more PRs remain. + +## Phase 6: Execute Actions + +For each PR where the user chose an unassign/unsubscribe action in Phase 5, execute all sub-steps in parallel across PRs. + +### Remove assignment / review request + +**If `HAS_PROW`:** post a GitHub comment with Prow bot commands: + +- Unassign: `add_issue_comment(owner, repo, issue_number: PR, body: "/unassign")` +- Remove review: `add_issue_comment(owner, repo, issue_number: PR, body: "/uncc")` +- Both: single comment with `/unassign` and `/uncc` on separate lines + +**If not `HAS_PROW`:** + +- Unassign: `issue_write(method: "update", owner, repo, issue_number: PR, assignees: [all current assignees except USERNAME])` +- Remove review: `gh api repos/OWNER/REPO/pulls/PR/requested_reviewers -X DELETE -f 'reviewers[]=USERNAME'` (no GitHub MCP tool can remove a review request). If `gh` is unavailable or unauthenticated, skip this call, note in the final summary that the review request must be removed manually, and continue with the remaining actions. +- Both: execute both + +### Clear GitHub notifications + +Call `list_notifications(owner, repo)`. For each notification matching this PR: + +1. `dismiss_notification(threadID, state: "done")` +2. `manage_notification_subscription(notificationID, action: "ignore")` + +### Summary + +Report all actions taken, combining Phase 4's auto-classification with this phase's user-driven actions: "Auto-dismissed 3 (2 closed, 1 needs-rebase). Auto-unsubscribed 1 (lgtm+approved). Kept 2 for review (already engaged). Unassigned from PR #X. Removed review request on PR #Y. Cleared N notifications." From 69d6caefe8019551d1981bba602c5b5d65fa3e6f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 17:05:55 +0000 Subject: [PATCH 02/14] fix(github): address review feedback on triage-prs skill - Paginate every list endpoint in the investigation phase, and both notification lookups (fetched once per phase and reused across PRs) - Only ignore the PR subscription when the chosen action removed every role held on the PR; partial removals dismiss the notification only - Bound the investigation fan-out to parallel batches of 10 agents Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- github/skills/triage-prs/SKILL.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/github/skills/triage-prs/SKILL.md b/github/skills/triage-prs/SKILL.md index f414d98..a75b3b9 100644 --- a/github/skills/triage-prs/SKILL.md +++ b/github/skills/triage-prs/SKILL.md @@ -55,7 +55,7 @@ If no PRs found: print "No open PRs found where you are assigned or a requested ## Phase 3: Parallel Investigation -Dispatch one Agent per PR, all in parallel, using `model: "sonnet"`. Each agent prompt must be self-contained — pass `owner`, `repo`, `pr_number`, and `username` explicitly. +Dispatch one Agent per PR using `model: "sonnet"`, in parallel batches of at most 10 PRs at a time. Each agent prompt must be self-contained — pass `owner`, `repo`, `pr_number`, and `username` explicitly. Each agent performs these MCP calls: @@ -67,6 +67,8 @@ Each agent performs these MCP calls: 6. `pull_request_read(method: "get_review_comments")` — count your unresolved vs resolved review threads 7. `pull_request_read(method: "get_comments", perPage: 100)` — paginate through all issue comments (not just recent); note mentions or questions directed at you, and record whether USERNAME appears among comment authors as `commented_before` +Every list method above is paginated: request `perPage: 100` and traverse every page (cursor pagination via `after` for `get_review_comments`) before summarizing, so late pages can't hide your latest review, open threads, or new commits. + If the PR is merged or closed: return `"Merged/Closed"` and stop (this still counts as investigated — Phase 4 auto-dismisses it). **Return this exact format:** @@ -113,7 +115,7 @@ Before presenting anything to the user, run each investigated PR through these r Execute the auto-classified actions now, per PR: -- **`dismiss`**: `list_notifications(owner, repo)`, find the thread(s) whose `subject.url` matches this PR, `dismiss_notification(threadID, state: "done")`. Do **not** call `manage_notification_subscription`. +- **`dismiss`**: from the notification list (call `list_notifications(owner, repo)` once for this phase, paginating through all pages, and reuse it across PRs), find the thread(s) whose `subject.url` matches this PR, `dismiss_notification(threadID, state: "done")`. Do **not** call `manage_notification_subscription`. - **`unsubscribe`**: same lookup, then both `dismiss_notification(threadID, state: "done")` **and** `manage_notification_subscription(notificationID, action: "ignore")`. - **`keep`**: no action at all — don't dismiss, don't touch the subscription, don't ask about it in Phase 5. The notification stays exactly as it is. - **`manual`**: no action yet — these carry forward into Phase 5. @@ -162,10 +164,10 @@ For each PR where the user chose an unassign/unsubscribe action in Phase 5, exec ### Clear GitHub notifications -Call `list_notifications(owner, repo)`. For each notification matching this PR: +Call `list_notifications(owner, repo)` once, paginating through all pages, and reuse the results across PRs. For each notification matching this PR: 1. `dismiss_notification(threadID, state: "done")` -2. `manage_notification_subscription(notificationID, action: "ignore")` +2. `manage_notification_subscription(notificationID, action: "ignore")` — **only if the chosen action removed every role you held on the PR** (unassign when you were only assigned, remove review request when you were only a reviewer, or both removals when you held both roles). If a role remains — e.g. "Unassign me" chosen but you're still a requested reviewer — dismiss only, so future activity still notifies you. ### Summary From 260aaa6b3ca63bcd5e2ba5ef83c3f31b43a9dd2d Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 18:13:25 +0000 Subject: [PATCH 03/14] fix(github): harden triage-prs CI, review, and notification handling Address remaining reviewer feedback on the triage-prs skill: - Include read notifications in both notification lookups (the API returns only unread threads by default, silently skipping dismissals) - Aggregate commit status contexts alongside check runs, since Prow and other external CI report via statuses - Derive the effective review decision from the latest APPROVED or CHANGES_REQUESTED review, so a later COMMENTED review doesn't hide it - Don't mute a PR subscription when a removal step was skipped Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- github/skills/triage-prs/SKILL.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/github/skills/triage-prs/SKILL.md b/github/skills/triage-prs/SKILL.md index a75b3b9..8d68151 100644 --- a/github/skills/triage-prs/SKILL.md +++ b/github/skills/triage-prs/SKILL.md @@ -60,8 +60,8 @@ Dispatch one Agent per PR using `model: "sonnet"`, in parallel batches of at mos Each agent performs these MCP calls: 1. `pull_request_read(method: "get")` — title, author, draft status, labels, timestamps -2. `pull_request_read(method: "get_check_runs")` — aggregate to: X/Y passing, Z failing [names], W pending -3. `pull_request_read(method: "get_reviews")` — find your most recent review (state + date); note all other reviewers and their states; record whether USERNAME appears at all (any state) as `reviewed_before` +2. `pull_request_read(method: "get_check_runs")` **and** `pull_request_read(method: "get_status")` — merge both sources (Prow and other external CI report via commit status contexts, not check runs); aggregate to: X/Y passing, Z failing [names], W pending +3. `pull_request_read(method: "get_reviews")` — record the date of your most recent review (any state); for your effective decision use your latest `APPROVED` or `CHANGES_REQUESTED` review, since a later `COMMENTED` review does not supersede it; note all other reviewers and their states; record whether USERNAME appears at all (any state) as `reviewed_before` 4. `pull_request_read(method: "get_files")` — file count, key filenames, total additions/deletions 5. `pull_request_read(method: "get_commits", perPage: 100)` — filter to commits dated after LAST_REVIEW_DATE; summarize via commit messages. Skip if not yet reviewed. 6. `pull_request_read(method: "get_review_comments")` — count your unresolved vs resolved review threads @@ -115,7 +115,7 @@ Before presenting anything to the user, run each investigated PR through these r Execute the auto-classified actions now, per PR: -- **`dismiss`**: from the notification list (call `list_notifications(owner, repo)` once for this phase, paginating through all pages, and reuse it across PRs), find the thread(s) whose `subject.url` matches this PR, `dismiss_notification(threadID, state: "done")`. Do **not** call `manage_notification_subscription`. +- **`dismiss`**: from the notification list (call `list_notifications(owner, repo, filter: "include_read_notifications")` once for this phase — read notifications are excluded by default — paginating through all pages, and reuse it across PRs), find the thread(s) whose `subject.url` matches this PR, `dismiss_notification(threadID, state: "done")`. Do **not** call `manage_notification_subscription`. - **`unsubscribe`**: same lookup, then both `dismiss_notification(threadID, state: "done")` **and** `manage_notification_subscription(notificationID, action: "ignore")`. - **`keep`**: no action at all — don't dismiss, don't touch the subscription, don't ask about it in Phase 5. The notification stays exactly as it is. - **`manual`**: no action yet — these carry forward into Phase 5. @@ -164,10 +164,10 @@ For each PR where the user chose an unassign/unsubscribe action in Phase 5, exec ### Clear GitHub notifications -Call `list_notifications(owner, repo)` once, paginating through all pages, and reuse the results across PRs. For each notification matching this PR: +Call `list_notifications(owner, repo, filter: "include_read_notifications")` once, paginating through all pages, and reuse the results across PRs. For each notification matching this PR: 1. `dismiss_notification(threadID, state: "done")` -2. `manage_notification_subscription(notificationID, action: "ignore")` — **only if the chosen action removed every role you held on the PR** (unassign when you were only assigned, remove review request when you were only a reviewer, or both removals when you held both roles). If a role remains — e.g. "Unassign me" chosen but you're still a requested reviewer — dismiss only, so future activity still notifies you. +2. `manage_notification_subscription(notificationID, action: "ignore")` — **only if the chosen action removed every role you held on the PR** (unassign when you were only assigned, remove review request when you were only a reviewer, or both removals when you held both roles) **and every removal step actually executed** (e.g. don't ignore when the `gh` call was skipped as unavailable). If a role remains or a removal was skipped, dismiss only, so future activity still notifies you. ### Summary From 1f824eea85df46096ff42cb4725db0530cebe227 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 18:42:42 +0000 Subject: [PATCH 04/14] perf(github): restructure triage-prs to classify before investigating Token-efficiency pass over the triage-prs skill: - Classify PRs from search data before dispatching any agents: engagement now comes from commenter:@me / reviewed-by:@me searches, so kept, dismissed, and unsubscribed PRs never spawn an investigation subagent (previously every PR was investigated, and engaged PRs discarded the result) - Trim agent calls: get_files and get_comments read a single page; full pagination is reserved for reviews, commits, and review threads - Hoist the duplicated notification-lookup procedure into one shared definition used by both phases - Drop redundant prose and the now-constant Previously Engaged and State report lines Net effect: the skill body shrinks ~20% and auto-classified PRs cost zero agent contexts at runtime. Phase count drops from six to five. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- github/README.md | 9 +-- github/skills/triage-prs/SKILL.md | 107 ++++++++++++++---------------- 2 files changed, 55 insertions(+), 61 deletions(-) diff --git a/github/README.md b/github/README.md index 6fe8ae1..0053fcc 100644 --- a/github/README.md +++ b/github/README.md @@ -29,10 +29,11 @@ HTTP-based MCP server connected to `api.githubcopilot.com`. The server is config ### Skills - **triage-prs** (`/github:triage-prs [owner/repo]`) — triages open PRs where you are - assigned or a requested reviewer. Investigates each PR with a parallel subagent, - auto-clears notifications for closed PRs (and, on Prow-managed repos, for - `needs-rebase` and `lgtm`+`approved` PRs), then presents batched options to - unassign, remove review requests, or unsubscribe. Explicit invocation only — + assigned or a requested reviewer. Auto-classifies PRs from search data first — + skipping ones you've already engaged with, and clearing notifications for closed + PRs (and, on Prow-managed repos, for `needs-rebase` and `lgtm`+`approved` PRs) — + then investigates only the remainder with parallel subagents and presents batched + options to unassign, remove review requests, or unsubscribe. Explicit invocation only — Claude never triggers it automatically. The target repo is taken from the optional `owner/repo` argument, or detected from the `upstream` (preferred) or `origin` git remote. Prow-managed repos are auto-detected and acted on via diff --git a/github/skills/triage-prs/SKILL.md b/github/skills/triage-prs/SKILL.md index 8d68151..4143e06 100644 --- a/github/skills/triage-prs/SKILL.md +++ b/github/skills/triage-prs/SKILL.md @@ -2,8 +2,8 @@ name: triage-prs description: >- Triage open pull requests that need the user's attention — PRs where they are - assigned or a requested reviewer. Investigates each PR in parallel, - auto-clears notifications for closed or bot-blocked PRs, then presents + assigned or a requested reviewer. Auto-classifies and clears notifications for + PRs that need no attention, investigates the rest in parallel, then presents batched triage options to unassign, remove review requests, or unsubscribe. disable-model-invocation: true argument-hint: '[owner/repo]' @@ -32,44 +32,64 @@ Investigate open PRs that need your attention in the target repo, then present b ## Phase 1: Setup -**Determine repo identity:** -If the repo argument above is non-empty, parse it as `OWNER/REPO`. Otherwise parse `OWNER` and `REPO` from the detected remote URL — support both SSH (`git@github.com:OWNER/REPO.git`) and HTTPS (`https://github.com/OWNER/REPO`) forms. The `upstream` remote is preferred over `origin` so fork-based workflows triage the upstream repo, not the fork. If the remote is `__NO_REMOTE__` and no argument was given, print "No GitHub remote detected — re-run as `/github:triage-prs owner/repo`." and stop. +**Repo identity:** if the repo argument above is non-empty, parse it as `OWNER/REPO`. Otherwise parse the detected remote URL — SSH (`git@github.com:OWNER/REPO.git`) or HTTPS (`https://github.com/OWNER/REPO`). If it is `__NO_REMOTE__` and no argument was given, print "No GitHub remote detected — re-run as `/github:triage-prs owner/repo`." and stop. -**Get authenticated user:** -Call `mcp__plugin_github_github__get_me` → store login as `USERNAME`. Needed for identifying your reviews, Prow commands, and non-Prow API actions. +**Authenticated user:** call `mcp__plugin_github_github__get_me` → store login as `USERNAME`. -## Phase 2: Collect PRs +## Phase 2: Collect and Classify -Run two searches in parallel using `@me` syntax: +Run four searches in parallel (paginate with `perPage: 100` if truncated): ```text -search_pull_requests(query: "assignee:@me is:open", owner, repo) -search_pull_requests(query: "review-requested:@me is:open", owner, repo) +search_pull_requests(query: "assignee:@me is:open", owner, repo) # role: assigned +search_pull_requests(query: "review-requested:@me is:open", owner, repo) # role: reviewer +search_pull_requests(query: "commenter:@me is:open", owner, repo) # engagement +search_pull_requests(query: "reviewed-by:@me is:open", owner, repo) # engagement ``` -Deduplicate by PR number. Tag each PR with its roles: `assigned`, `reviewer`, or both. Paginate using `perPage: 100` if results are truncated. +Candidates are the union of the first two searches, deduplicated by PR number and tagged with roles `assigned`, `reviewer`, or both. A candidate is **engaged** if its number appears in either engagement search. -If no PRs found: print "No open PRs found where you are assigned or a requested reviewer in OWNER/REPO." and stop. +If no candidates: print "No open PRs found where you are assigned or a requested reviewer in OWNER/REPO." and stop. -**Detect Prow:** call `pull_request_read(method: "get_status")` on the first collected PR. Set `HAS_PROW = true` if any status context name contains `"tide"`, else `false`. +**Detect Prow:** call `pull_request_read(method: "get_status")` on the first candidate. `HAS_PROW = true` if any status context name contains `"tide"`. + +**Classify** each candidate from its search-result data (state, labels, engagement) — first matching rule wins: + +1. **Closed or merged** → `dismiss` +2. **Engaged** → `keep` (already invested — leave its notification alone and exclude it from all later phases) +3. **`HAS_PROW` and `needs-rebase` label** → `dismiss` +4. **`HAS_PROW` and both `lgtm` and `approved` labels** → `unsubscribe` +5. **Otherwise** → `manual` + +**Notification lookup** (used here and in Phase 5): call `list_notifications(owner, repo, filter: "include_read_notifications")` once per phase — read notifications are excluded by default — paginate all pages, and match threads whose `subject.url` points at the PR. + +Execute the classifications now: + +- **`dismiss`**: `dismiss_notification(threadID, state: "done")` on matching threads. Do **not** touch the subscription. +- **`unsubscribe`**: `dismiss_notification(threadID, state: "done")` **and** `manage_notification_subscription(notificationID, action: "ignore")`. +- **`keep`** / **`manual`**: no action; `manual` PRs continue to Phase 3. + +> **Dismiss vs. unsubscribe:** `dismiss` clears only the current notification thread — new PR activity notifies again, which is what `dismiss` cases (closed, needs-rebase) want. Only `ignore` stops future notifications, so it is reserved for `unsubscribe`. + +Report a summary line, e.g.: "Auto-dismissed 3 (2 closed, 1 needs-rebase). Auto-unsubscribed 1 (lgtm+approved). Kept 2 (already engaged). Investigating 4." ## Phase 3: Parallel Investigation -Dispatch one Agent per PR using `model: "sonnet"`, in parallel batches of at most 10 PRs at a time. Each agent prompt must be self-contained — pass `owner`, `repo`, `pr_number`, and `username` explicitly. +For `manual` PRs only, dispatch one Agent per PR using `model: "sonnet"`, in parallel batches of at most 10. Each agent prompt must be self-contained — pass `owner`, `repo`, `pr_number`, and `username` explicitly. -Each agent performs these MCP calls: +Each agent performs these `pull_request_read` calls: -1. `pull_request_read(method: "get")` — title, author, draft status, labels, timestamps -2. `pull_request_read(method: "get_check_runs")` **and** `pull_request_read(method: "get_status")` — merge both sources (Prow and other external CI report via commit status contexts, not check runs); aggregate to: X/Y passing, Z failing [names], W pending -3. `pull_request_read(method: "get_reviews")` — record the date of your most recent review (any state); for your effective decision use your latest `APPROVED` or `CHANGES_REQUESTED` review, since a later `COMMENTED` review does not supersede it; note all other reviewers and their states; record whether USERNAME appears at all (any state) as `reviewed_before` -4. `pull_request_read(method: "get_files")` — file count, key filenames, total additions/deletions -5. `pull_request_read(method: "get_commits", perPage: 100)` — filter to commits dated after LAST_REVIEW_DATE; summarize via commit messages. Skip if not yet reviewed. -6. `pull_request_read(method: "get_review_comments")` — count your unresolved vs resolved review threads -7. `pull_request_read(method: "get_comments", perPage: 100)` — paginate through all issue comments (not just recent); note mentions or questions directed at you, and record whether USERNAME appears among comment authors as `commented_before` +1. `get` — title, author, draft status, labels, timestamps +2. `get_check_runs` **and** `get_status` — merge both (external CI such as Prow reports via status contexts); aggregate to: X/Y passing, Z failing [names], W pending +3. `get_reviews` — date of your most recent review (any state); for your effective decision use your latest `APPROVED` or `CHANGES_REQUESTED` review, since a later `COMMENTED` review does not supersede it; note all other reviewers and their states +4. `get_files(perPage: 100)` — first page only; file count ("100+" if truncated), key filenames, additions/deletions +5. `get_commits(perPage: 100)` — commits dated after your last review; summarize via commit messages. Skip if not yet reviewed. +6. `get_review_comments` — count your unresolved vs resolved review threads +7. `get_comments(perPage: 100)` — most recent page only; note mentions or questions directed at you -Every list method above is paginated: request `perPage: 100` and traverse every page (cursor pagination via `after` for `get_review_comments`) before summarizing, so late pages can't hide your latest review, open threads, or new commits. +Paginate `get_reviews`, `get_commits`, and `get_review_comments` fully (`perPage: 100`; cursor via `after` for `get_review_comments`). Steps 4 and 7 deliberately read a single page. -If the PR is merged or closed: return `"Merged/Closed"` and stop (this still counts as investigated — Phase 4 auto-dismisses it). +If the PR is merged or closed: return `"Merged/Closed"` — run the `dismiss` action for it. **Return this exact format:** @@ -78,11 +98,9 @@ PR #NUMBER: TITLE Author: AUTHOR | Created: DATE | Updated: DATE | Draft: yes/no Role: [assigned] [reviewer] Labels: LABEL1, LABEL2 -State: open/closed/merged CI: X/Y passing, Z failing [CHECK_NAMES], W pending → green/red/yellow/pending My Last Review: STATE on DATE (or: Not yet reviewed) -Previously Engaged: yes/no (reviewed_before OR commented_before) Changes Since Review: N new commits — SUMMARY (or: None / N/A) My Open Threads: N unresolved of M total Other Reviews: REVIEWER (STATE), ... @@ -103,32 +121,9 @@ Recommended Action: ACTION_CATEGORY - **Waiting on CI** — CI still running - **Stale** — no activity in >7 days -## Phase 4: Auto-Classification - -Before presenting anything to the user, run each investigated PR through these rules **in order**. The first matching rule wins; stop evaluating further rules for that PR once one matches. +## Phase 4: Triage -1. **Closed or merged** → `dismiss` -2. **Previously Engaged** (`reviewed_before` or `commented_before` is yes) → `keep` (you're already invested in this PR — leave its notification alone and skip triage entirely; don't ask about it, don't touch it) -3. **`HAS_PROW` is true, and `needs-rebase` label present** → `dismiss` -4. **`HAS_PROW` is true, and both `lgtm` and `approved` labels are present** (from anyone other than you — Previously Engaged already excluded you above) → `unsubscribe` -5. **Otherwise** → `manual` - -Execute the auto-classified actions now, per PR: - -- **`dismiss`**: from the notification list (call `list_notifications(owner, repo, filter: "include_read_notifications")` once for this phase — read notifications are excluded by default — paginating through all pages, and reuse it across PRs), find the thread(s) whose `subject.url` matches this PR, `dismiss_notification(threadID, state: "done")`. Do **not** call `manage_notification_subscription`. -- **`unsubscribe`**: same lookup, then both `dismiss_notification(threadID, state: "done")` **and** `manage_notification_subscription(notificationID, action: "ignore")`. -- **`keep`**: no action at all — don't dismiss, don't touch the subscription, don't ask about it in Phase 5. The notification stays exactly as it is. -- **`manual`**: no action yet — these carry forward into Phase 5. - -> **Dismiss vs. unsubscribe:** `dismiss_notification(state: "done")` clears only the current notification thread — new PR activity creates a fresh notification later, which is what `dismiss` cases (closed, needs-rebase) want. Only `manage_notification_subscription(action: "ignore")` stops future notifications, so it is reserved for `unsubscribe` cases. - -Report auto-classified PRs in a short summary line before moving to Phase 5, e.g.: "Auto-dismissed 3 (2 closed, 1 needs-rebase). Auto-unsubscribed 1 (lgtm+approved). Kept 2 for review (already engaged)." - -## Phase 5: Triage - -Sort all `manual`-classified agent results by urgency (order matches the category list above). - -Present in batches of up to 4 using `AskUserQuestion` (tool supports 1–4 questions per call, 2–4 options each). Each question is one PR. +Sort the agent results by urgency (order matches the category list above). Present in batches of up to 4 using `AskUserQuestion`, one question per PR, until all are covered. **Question:** `PR #NUMBER: TITLE (by @AUTHOR) — RECOMMENDED_ACTION` @@ -142,11 +137,9 @@ Present in batches of up to 4 using `AskUserQuestion` (tool supports 1–4 quest | `reviewer` only | Skip / Remove review request | | both | Skip / Unassign me / Remove review request / Unsubscribe (both) | -After each batch, continue to the next batch if more PRs remain. - -## Phase 6: Execute Actions +## Phase 5: Execute Actions -For each PR where the user chose an unassign/unsubscribe action in Phase 5, execute all sub-steps in parallel across PRs. +For each PR where the user chose an action, execute all sub-steps in parallel across PRs. ### Remove assignment / review request @@ -164,11 +157,11 @@ For each PR where the user chose an unassign/unsubscribe action in Phase 5, exec ### Clear GitHub notifications -Call `list_notifications(owner, repo, filter: "include_read_notifications")` once, paginating through all pages, and reuse the results across PRs. For each notification matching this PR: +Using the Notification lookup from Phase 2 (fresh call for this phase), for each thread matching the PR: 1. `dismiss_notification(threadID, state: "done")` 2. `manage_notification_subscription(notificationID, action: "ignore")` — **only if the chosen action removed every role you held on the PR** (unassign when you were only assigned, remove review request when you were only a reviewer, or both removals when you held both roles) **and every removal step actually executed** (e.g. don't ignore when the `gh` call was skipped as unavailable). If a role remains or a removal was skipped, dismiss only, so future activity still notifies you. ### Summary -Report all actions taken, combining Phase 4's auto-classification with this phase's user-driven actions: "Auto-dismissed 3 (2 closed, 1 needs-rebase). Auto-unsubscribed 1 (lgtm+approved). Kept 2 for review (already engaged). Unassigned from PR #X. Removed review request on PR #Y. Cleared N notifications." +Combine Phase 2's auto-classification summary with this phase's actions, e.g.: "Auto-dismissed 3 (2 closed, 1 needs-rebase). Auto-unsubscribed 1 (lgtm+approved). Kept 2 (already engaged). Unassigned from PR #X. Removed review request on PR #Y. Cleared N notifications." From 88c22080ad4ed2aea41d216fc0b2282ee21442af Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 18:45:12 +0000 Subject: [PATCH 05/14] refactor(github): use short MCP tool names in triage-prs body Reference GitHub MCP tools by short name throughout the skill body, with a one-line mapping note; allowed-tools keeps the fully qualified names required for permission matching. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- github/skills/triage-prs/SKILL.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/github/skills/triage-prs/SKILL.md b/github/skills/triage-prs/SKILL.md index 4143e06..b03bcba 100644 --- a/github/skills/triage-prs/SKILL.md +++ b/github/skills/triage-prs/SKILL.md @@ -25,6 +25,8 @@ allowed-tools: Investigate open PRs that need your attention in the target repo, then present batched triage with options to unassign or unsubscribe. +Tool names below are short forms of the GitHub MCP tools listed in `allowed-tools` (e.g. `get_me` → `mcp__plugin_github_github__get_me`). + ## Repo Context - Repo argument (overrides remote detection if non-empty): $ARGUMENTS @@ -34,7 +36,7 @@ Investigate open PRs that need your attention in the target repo, then present b **Repo identity:** if the repo argument above is non-empty, parse it as `OWNER/REPO`. Otherwise parse the detected remote URL — SSH (`git@github.com:OWNER/REPO.git`) or HTTPS (`https://github.com/OWNER/REPO`). If it is `__NO_REMOTE__` and no argument was given, print "No GitHub remote detected — re-run as `/github:triage-prs owner/repo`." and stop. -**Authenticated user:** call `mcp__plugin_github_github__get_me` → store login as `USERNAME`. +**Authenticated user:** call `get_me` → store login as `USERNAME`. ## Phase 2: Collect and Classify From e14a38fc7f4072249f967df168fd5c3a11d30b26 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 19:19:00 +0000 Subject: [PATCH 06/14] fix(github): correct comment ordering and harden triage-prs edge cases Address reviewer findings on the restructured skill: - get_comments returns oldest-first, so a single unqualified page read the oldest comments; follow pagination to the final page instead - Anchor changes-since-review on the review's commit_id rather than commit dates, which rebases and cherry-picks can falsify - Probe up to three candidates for Prow detection, since new or draft PRs may have no status contexts yet - Add a Draft action category so drafts aren't recommended for review or merge - Confirm role removal (re-read the PR after async Prow commands) before muting a subscription, and disclose the mute side effect in triage option descriptions - Stop claiming closed-PR notification cleanup in the docs; is:open collection only catches PRs that close mid-run Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- github/README.md | 9 +++++---- github/skills/triage-prs/SKILL.md | 20 ++++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/github/README.md b/github/README.md index 0053fcc..f4e223b 100644 --- a/github/README.md +++ b/github/README.md @@ -30,10 +30,11 @@ HTTP-based MCP server connected to `api.githubcopilot.com`. The server is config - **triage-prs** (`/github:triage-prs [owner/repo]`) — triages open PRs where you are assigned or a requested reviewer. Auto-classifies PRs from search data first — - skipping ones you've already engaged with, and clearing notifications for closed - PRs (and, on Prow-managed repos, for `needs-rebase` and `lgtm`+`approved` PRs) — - then investigates only the remainder with parallel subagents and presents batched - options to unassign, remove review requests, or unsubscribe. Explicit invocation only — + skipping ones you've already engaged with and, on Prow-managed repos, dismissing + notifications for `needs-rebase` PRs and unsubscribing entirely from + `lgtm`+`approved` PRs — then investigates only the remainder with parallel + subagents and presents batched options to unassign, remove review requests, or + unsubscribe. Explicit invocation only — Claude never triggers it automatically. The target repo is taken from the optional `owner/repo` argument, or detected from the `upstream` (preferred) or `origin` git remote. Prow-managed repos are auto-detected and acted on via diff --git a/github/skills/triage-prs/SKILL.md b/github/skills/triage-prs/SKILL.md index b03bcba..205710b 100644 --- a/github/skills/triage-prs/SKILL.md +++ b/github/skills/triage-prs/SKILL.md @@ -2,9 +2,10 @@ name: triage-prs description: >- Triage open pull requests that need the user's attention — PRs where they are - assigned or a requested reviewer. Auto-classifies and clears notifications for - PRs that need no attention, investigates the rest in parallel, then presents - batched triage options to unassign, remove review requests, or unsubscribe. + assigned or a requested reviewer. Auto-classifies PRs first, dismissing or + muting notifications for ones that need no attention, investigates the rest + in parallel, then presents batched triage options to unassign, remove review + requests, or unsubscribe. disable-model-invocation: true argument-hint: '[owner/repo]' allowed-tools: @@ -53,7 +54,7 @@ Candidates are the union of the first two searches, deduplicated by PR number an If no candidates: print "No open PRs found where you are assigned or a requested reviewer in OWNER/REPO." and stop. -**Detect Prow:** call `pull_request_read(method: "get_status")` on the first candidate. `HAS_PROW = true` if any status context name contains `"tide"`. +**Detect Prow:** call `pull_request_read(method: "get_status")` on candidates until one returns status contexts (up to three — new or draft PRs may have none yet). `HAS_PROW = true` if any context name contains `"tide"`. **Classify** each candidate from its search-result data (state, labels, engagement) — first matching rule wins: @@ -85,11 +86,11 @@ Each agent performs these `pull_request_read` calls: 2. `get_check_runs` **and** `get_status` — merge both (external CI such as Prow reports via status contexts); aggregate to: X/Y passing, Z failing [names], W pending 3. `get_reviews` — date of your most recent review (any state); for your effective decision use your latest `APPROVED` or `CHANGES_REQUESTED` review, since a later `COMMENTED` review does not supersede it; note all other reviewers and their states 4. `get_files(perPage: 100)` — first page only; file count ("100+" if truncated), key filenames, additions/deletions -5. `get_commits(perPage: 100)` — commits dated after your last review; summarize via commit messages. Skip if not yet reviewed. +5. `get_commits(perPage: 100)` — commits since your last review: locate the review's `commit_id` in the ordered commit list and take everything after it (fall back to comparing dates if a force-push removed that SHA); summarize via commit messages. Skip if not yet reviewed. 6. `get_review_comments` — count your unresolved vs resolved review threads -7. `get_comments(perPage: 100)` — most recent page only; note mentions or questions directed at you +7. `get_comments(perPage: 100)` — issue comments are oldest-first, so if the first page is full, follow pagination to the final page; only the most recent ~100 matter. Note mentions or questions directed at you. -Paginate `get_reviews`, `get_commits`, and `get_review_comments` fully (`perPage: 100`; cursor via `after` for `get_review_comments`). Steps 4 and 7 deliberately read a single page. +Paginate `get_reviews`, `get_commits`, and `get_review_comments` fully (`perPage: 100`; cursor via `after` for `get_review_comments`). Step 4 deliberately reads a single page. If the PR is merged or closed: return `"Merged/Closed"` — run the `dismiss` action for it. @@ -114,6 +115,7 @@ Recommended Action: ACTION_CATEGORY **Action categories** (pick highest-priority that applies): +- **Draft** — PR is marked draft; not actionable for review or merge unless you were explicitly asked - **Author addressed feedback** — you requested changes; author pushed new commits and/or resolved threads - **Review needed** — not yet reviewed by you - **Re-review needed** — new commits since your last review (you hadn't requested changes) @@ -139,6 +141,8 @@ Sort the agent results by urgency (order matches the category list above). Prese | `reviewer` only | Skip / Remove review request | | both | Skip / Unassign me / Remove review request / Unsubscribe (both) | +In each option's description, disclose when the action will also mute the PR: any choice that removes your only remaining role (and `Unsubscribe (both)`) stops all future notifications for that PR (see Phase 5). + ## Phase 5: Execute Actions For each PR where the user chose an action, execute all sub-steps in parallel across PRs. @@ -162,7 +166,7 @@ For each PR where the user chose an action, execute all sub-steps in parallel ac Using the Notification lookup from Phase 2 (fresh call for this phase), for each thread matching the PR: 1. `dismiss_notification(threadID, state: "done")` -2. `manage_notification_subscription(notificationID, action: "ignore")` — **only if the chosen action removed every role you held on the PR** (unassign when you were only assigned, remove review request when you were only a reviewer, or both removals when you held both roles) **and every removal step actually executed** (e.g. don't ignore when the `gh` call was skipped as unavailable). If a role remains or a removal was skipped, dismiss only, so future activity still notifies you. +2. `manage_notification_subscription(notificationID, action: "ignore")` — **only if the chosen action removed every role you held on the PR** (unassign when you were only assigned, remove review request when you were only a reviewer, or both removals when you held both roles) **and the removal is confirmed**: a skipped `gh` call doesn't count, and a Prow comment only queues the bot command — re-read the PR (`get`) and check the role is actually gone; if it is still present, dismiss only and report the removal as pending. If a role remains or a removal is unconfirmed, dismiss only, so future activity still notifies you. ### Summary From 0aab5449e4ab14011624faa6dded65e6a0256617 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 19:28:04 +0000 Subject: [PATCH 07/14] fix(github): refine Prow detection and review-decision logic in triage-prs - Keep probing candidates for a tide status context instead of stopping at the first PR that has any statuses; cap at five candidates - Exclude DISMISSED reviews from the effective review decision - Mention review-request removal in the skill intro alongside unassign and unsubscribe Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- github/skills/triage-prs/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/github/skills/triage-prs/SKILL.md b/github/skills/triage-prs/SKILL.md index 205710b..9394dc0 100644 --- a/github/skills/triage-prs/SKILL.md +++ b/github/skills/triage-prs/SKILL.md @@ -24,7 +24,7 @@ allowed-tools: # Triage PRs -Investigate open PRs that need your attention in the target repo, then present batched triage with options to unassign or unsubscribe. +Investigate open PRs that need your attention in the target repo, then present batched triage with options to unassign, remove review requests, or unsubscribe. Tool names below are short forms of the GitHub MCP tools listed in `allowed-tools` (e.g. `get_me` → `mcp__plugin_github_github__get_me`). @@ -54,7 +54,7 @@ Candidates are the union of the first two searches, deduplicated by PR number an If no candidates: print "No open PRs found where you are assigned or a requested reviewer in OWNER/REPO." and stop. -**Detect Prow:** call `pull_request_read(method: "get_status")` on candidates until one returns status contexts (up to three — new or draft PRs may have none yet). `HAS_PROW = true` if any context name contains `"tide"`. +**Detect Prow:** call `pull_request_read(method: "get_status")` on candidates in order, setting `HAS_PROW = true` as soon as any context name contains `"tide"`. A candidate with other statuses but no tide context is not proof of absence — keep checking. Treat the repo as non-Prow after five candidates without a tide context. **Classify** each candidate from its search-result data (state, labels, engagement) — first matching rule wins: @@ -84,7 +84,7 @@ Each agent performs these `pull_request_read` calls: 1. `get` — title, author, draft status, labels, timestamps 2. `get_check_runs` **and** `get_status` — merge both (external CI such as Prow reports via status contexts); aggregate to: X/Y passing, Z failing [names], W pending -3. `get_reviews` — date of your most recent review (any state); for your effective decision use your latest `APPROVED` or `CHANGES_REQUESTED` review, since a later `COMMENTED` review does not supersede it; note all other reviewers and their states +3. `get_reviews` — date of your most recent review (any state); for your effective decision use your latest **active** `APPROVED` or `CHANGES_REQUESTED` review — `DISMISSED` reviews are inactive history, and a later `COMMENTED` review does not supersede the decision; note all other reviewers and their states 4. `get_files(perPage: 100)` — first page only; file count ("100+" if truncated), key filenames, additions/deletions 5. `get_commits(perPage: 100)` — commits since your last review: locate the review's `commit_id` in the ordered commit list and take everything after it (fall back to comparing dates if a force-push removed that SHA); summarize via commit messages. Skip if not yet reviewed. 6. `get_review_comments` — count your unresolved vs resolved review threads From 31c304f83e91db0088ddba0a1083db661d1c8e94 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 23:28:41 +0000 Subject: [PATCH 08/14] feat(github): sweep stale closed-PR notifications in triage-prs Add a Phase 2 sweep that dismisses notification threads left by PRs that closed or merged before invocation: PullRequest-type threads not among the open candidates get one state check, and closed/merged ones are dismissed (never muted, so a reopen still notifies). The sweep runs before the no-candidates early exit, since an emptied review queue is exactly when stale notifications accumulate. The is:open searches structurally cannot find these PRs, so this covers the gap directly instead of leaving it out of scope. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- github/README.md | 3 ++- github/skills/triage-prs/SKILL.md | 20 +++++++++++--------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/github/README.md b/github/README.md index f4e223b..3225a39 100644 --- a/github/README.md +++ b/github/README.md @@ -29,7 +29,8 @@ HTTP-based MCP server connected to `api.githubcopilot.com`. The server is config ### Skills - **triage-prs** (`/github:triage-prs [owner/repo]`) — triages open PRs where you are - assigned or a requested reviewer. Auto-classifies PRs from search data first — + assigned or a requested reviewer. Sweeps stale notification threads left by PRs + that have since closed or merged, then auto-classifies open PRs from search data — skipping ones you've already engaged with and, on Prow-managed repos, dismissing notifications for `needs-rebase` PRs and unsubscribing entirely from `lgtm`+`approved` PRs — then investigates only the remainder with parallel diff --git a/github/skills/triage-prs/SKILL.md b/github/skills/triage-prs/SKILL.md index 9394dc0..1fd7bc3 100644 --- a/github/skills/triage-prs/SKILL.md +++ b/github/skills/triage-prs/SKILL.md @@ -2,10 +2,10 @@ name: triage-prs description: >- Triage open pull requests that need the user's attention — PRs where they are - assigned or a requested reviewer. Auto-classifies PRs first, dismissing or - muting notifications for ones that need no attention, investigates the rest - in parallel, then presents batched triage options to unassign, remove review - requests, or unsubscribe. + assigned or a requested reviewer. Sweeps stale notifications left by closed or + merged PRs, auto-classifies open PRs — dismissing or muting notifications for + ones that need no attention — investigates the rest in parallel, then presents + batched triage options to unassign, remove review requests, or unsubscribe. disable-model-invocation: true argument-hint: '[owner/repo]' allowed-tools: @@ -52,7 +52,11 @@ search_pull_requests(query: "reviewed-by:@me is:open", owner, repo) # enga Candidates are the union of the first two searches, deduplicated by PR number and tagged with roles `assigned`, `reviewer`, or both. A candidate is **engaged** if its number appears in either engagement search. -If no candidates: print "No open PRs found where you are assigned or a requested reviewer in OWNER/REPO." and stop. +**Notification lookup** (used here and in Phase 5): call `list_notifications(owner, repo, filter: "include_read_notifications")` once per phase — read notifications are excluded by default — paginate all pages, and match threads by the PR their `subject.url` points at. + +**Sweep stale closed-PR notifications:** take `PullRequest`-type threads from the lookup whose PR number is not among the candidates. For each, call `pull_request_read(method: "get")`; if that PR is closed or merged, `dismiss_notification(threadID, state: "done")` — dismiss only, never ignore, so a reopened PR still notifies you. Threads whose PR is still open stay untouched. + +If no candidates: report the sweep result, print "No open PRs found where you are assigned or a requested reviewer in OWNER/REPO." and stop. **Detect Prow:** call `pull_request_read(method: "get_status")` on candidates in order, setting `HAS_PROW = true` as soon as any context name contains `"tide"`. A candidate with other statuses but no tide context is not proof of absence — keep checking. Treat the repo as non-Prow after five candidates without a tide context. @@ -64,9 +68,7 @@ If no candidates: print "No open PRs found where you are assigned or a requested 4. **`HAS_PROW` and both `lgtm` and `approved` labels** → `unsubscribe` 5. **Otherwise** → `manual` -**Notification lookup** (used here and in Phase 5): call `list_notifications(owner, repo, filter: "include_read_notifications")` once per phase — read notifications are excluded by default — paginate all pages, and match threads whose `subject.url` points at the PR. - -Execute the classifications now: +Execute the classifications now, using the notification lookup above: - **`dismiss`**: `dismiss_notification(threadID, state: "done")` on matching threads. Do **not** touch the subscription. - **`unsubscribe`**: `dismiss_notification(threadID, state: "done")` **and** `manage_notification_subscription(notificationID, action: "ignore")`. @@ -74,7 +76,7 @@ Execute the classifications now: > **Dismiss vs. unsubscribe:** `dismiss` clears only the current notification thread — new PR activity notifies again, which is what `dismiss` cases (closed, needs-rebase) want. Only `ignore` stops future notifications, so it is reserved for `unsubscribe`. -Report a summary line, e.g.: "Auto-dismissed 3 (2 closed, 1 needs-rebase). Auto-unsubscribed 1 (lgtm+approved). Kept 2 (already engaged). Investigating 4." +Report a summary line, e.g.: "Swept 5 stale closed-PR notifications. Auto-dismissed 3 (2 closed, 1 needs-rebase). Auto-unsubscribed 1 (lgtm+approved). Kept 2 (already engaged). Investigating 4." ## Phase 3: Parallel Investigation From a6f738ee30cf540c4a00c6de680f67e8f7c5a797 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 23:37:12 +0000 Subject: [PATCH 09/14] fix(github): refine triage-prs Prow cutoff, comment buffer, team requests - Count only status-bearing candidates toward the five-candidate Prow detection cutoff; status-less PRs are uninformative - Keep a rolling last-100 comment buffer instead of only the final page, which can be short - Never delete a team-based review request (that removes it for the whole team); report it as team-scoped and offer unsubscribe instead Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- github/skills/triage-prs/SKILL.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/github/skills/triage-prs/SKILL.md b/github/skills/triage-prs/SKILL.md index 1fd7bc3..151e791 100644 --- a/github/skills/triage-prs/SKILL.md +++ b/github/skills/triage-prs/SKILL.md @@ -58,7 +58,7 @@ Candidates are the union of the first two searches, deduplicated by PR number an If no candidates: report the sweep result, print "No open PRs found where you are assigned or a requested reviewer in OWNER/REPO." and stop. -**Detect Prow:** call `pull_request_read(method: "get_status")` on candidates in order, setting `HAS_PROW = true` as soon as any context name contains `"tide"`. A candidate with other statuses but no tide context is not proof of absence — keep checking. Treat the repo as non-Prow after five candidates without a tide context. +**Detect Prow:** call `pull_request_read(method: "get_status")` on candidates in order, setting `HAS_PROW = true` as soon as any context name contains `"tide"`. A candidate with other statuses but no tide context is not proof of absence — keep checking. Conclude non-Prow after five candidates that returned status contexts but none containing tide (status-less candidates, such as new or draft PRs, don't count toward the five), or when candidates are exhausted. **Classify** each candidate from its search-result data (state, labels, engagement) — first matching rule wins: @@ -90,7 +90,7 @@ Each agent performs these `pull_request_read` calls: 4. `get_files(perPage: 100)` — first page only; file count ("100+" if truncated), key filenames, additions/deletions 5. `get_commits(perPage: 100)` — commits since your last review: locate the review's `commit_id` in the ordered commit list and take everything after it (fall back to comparing dates if a force-push removed that SHA); summarize via commit messages. Skip if not yet reviewed. 6. `get_review_comments` — count your unresolved vs resolved review threads -7. `get_comments(perPage: 100)` — issue comments are oldest-first, so if the first page is full, follow pagination to the final page; only the most recent ~100 matter. Note mentions or questions directed at you. +7. `get_comments(perPage: 100)` — issue comments are oldest-first, so if the first page is full, follow pagination and keep the most recent ~100 comments (the final page plus the prior page when the final page is short). Note mentions or questions directed at you. Paginate `get_reviews`, `get_commits`, and `get_review_comments` fully (`perPage: 100`; cursor via `after` for `get_review_comments`). Step 4 deliberately reads a single page. @@ -160,7 +160,7 @@ For each PR where the user chose an action, execute all sub-steps in parallel ac **If not `HAS_PROW`:** - Unassign: `issue_write(method: "update", owner, repo, issue_number: PR, assignees: [all current assignees except USERNAME])` -- Remove review: `gh api repos/OWNER/REPO/pulls/PR/requested_reviewers -X DELETE -f 'reviewers[]=USERNAME'` (no GitHub MCP tool can remove a review request). If `gh` is unavailable or unauthenticated, skip this call, note in the final summary that the review request must be removed manually, and continue with the remaining actions. +- Remove review: `gh api repos/OWNER/REPO/pulls/PR/requested_reviewers -X DELETE -f 'reviewers[]=USERNAME'` (no GitHub MCP tool can remove a review request). If the request is team-based — USERNAME absent from `requested_reviewers` but a team appears in `requested_teams` — do **not** delete the team's request (that would remove it for every teammate); report that the request came via the team and can only be unsubscribed from, not individually removed. If `gh` is unavailable or unauthenticated, skip this call, note in the final summary that the review request must be removed manually, and continue with the remaining actions. - Both: execute both ### Clear GitHub notifications From 5a2b2451c1cd5a4565a188d40c461dbfe9562513 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Aug 2026 23:53:43 +0000 Subject: [PATCH 10/14] docs: add PR review-batching convention to CLAUDE.md Push review fixes once per round (after all bot reviewers finish with the current head) rather than once per reviewer, and pause CodeRabbit auto-reviews during iterative push bursts, so its rate-limited incremental reviews aren't burned on intermediate states. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- CLAUDE.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index f5fdef4..f6a72f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -54,6 +54,14 @@ structure. changes now warrant a higher semver level (e.g., patch already bumped but a new skill was added — upgrade to minor) - Use plugin-dev skills: `/plugin-dev:create-plugin`, `/plugin-dev:skill-reviewer`, `/plugin-dev:plugin-validator` +- When addressing automated review feedback on a PR (Copilot, CodeRabbit): wait until + every reviewer has finished reviewing the current head, then push fixes for all + findings as a single commit — one push per review round, not one per reviewer. + CodeRabbit auto-reviews every push and its incremental reviews are rate-limited, + so per-reviewer pushes burn the allowance on intermediate states +- For a burst of iterative pushes on an open PR, pause auto-reviews first with an + `@coderabbitai pause` comment, then `@coderabbitai resume` followed by + `@coderabbitai review` once the branch is stable ## Documentation From 587c0cc5329af5b7f02e486f308d3180c01748a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 00:43:30 +0000 Subject: [PATCH 11/14] fix(github): batch review-round refinements for triage-prs Batched fixes for all outstanding reviewer findings on the current head, per the new one-push-per-review-round convention: - Drop the vestigial yellow CI state the aggregation can never produce - Paginate get_check_runs and get_status fully so multi-page CI can't read as green - Re-read the PR before unassigning, since issue_write replaces the whole assignee set and stale data would drop new assignees - Give team-based review requests an executable path: agents report the request source, triage offers Unsubscribe instead of an impossible removal, and an explicit Unsubscribe choice may mute without role removal - Correct the root README's "all toolsets" claim to match .mcp.json Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- README.md | 2 +- github/skills/triage-prs/SKILL.md | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 301d5d2..8c0e6cd 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ claude plugin marketplace add cblecker/claude-plugins | Plugin | Description | |----------------------------------------------|--------------------------------------------------------------------------------------------------| | [git](./git) | Dynamic git instructions via SessionStart hook with mainline detection, conventional commits, fork handling, and safety guardrails | -| [github](./github) | GitHub MCP server with all toolsets enabled, always loaded, plus a PR triage skill | +| [github](./github) | GitHub MCP server with selected toolsets enabled, always loaded, plus a PR triage skill | | [pr-review-toolkit](./pr-review-toolkit) | Comprehensive PR review board using shared workflow context | | [gws](./gws) | Google Workspace CLI skills for Gmail, Calendar, Drive, Docs, Sheets, Slides, and Meet | | [rh-dataverse](./rh-dataverse) | Red Hat Dataverse MCP server | diff --git a/github/skills/triage-prs/SKILL.md b/github/skills/triage-prs/SKILL.md index 151e791..0c9f2da 100644 --- a/github/skills/triage-prs/SKILL.md +++ b/github/skills/triage-prs/SKILL.md @@ -84,7 +84,7 @@ For `manual` PRs only, dispatch one Agent per PR using `model: "sonnet"`, in par Each agent performs these `pull_request_read` calls: -1. `get` — title, author, draft status, labels, timestamps +1. `get` — title, author, draft status, labels, timestamps; whether your review request is individual (USERNAME in `requested_reviewers`) or team-based (only via `requested_teams`) 2. `get_check_runs` **and** `get_status` — merge both (external CI such as Prow reports via status contexts); aggregate to: X/Y passing, Z failing [names], W pending 3. `get_reviews` — date of your most recent review (any state); for your effective decision use your latest **active** `APPROVED` or `CHANGES_REQUESTED` review — `DISMISSED` reviews are inactive history, and a later `COMMENTED` review does not supersede the decision; note all other reviewers and their states 4. `get_files(perPage: 100)` — first page only; file count ("100+" if truncated), key filenames, additions/deletions @@ -92,7 +92,7 @@ Each agent performs these `pull_request_read` calls: 6. `get_review_comments` — count your unresolved vs resolved review threads 7. `get_comments(perPage: 100)` — issue comments are oldest-first, so if the first page is full, follow pagination and keep the most recent ~100 comments (the final page plus the prior page when the final page is short). Note mentions or questions directed at you. -Paginate `get_reviews`, `get_commits`, and `get_review_comments` fully (`perPage: 100`; cursor via `after` for `get_review_comments`). Step 4 deliberately reads a single page. +Paginate `get_reviews`, `get_commits`, `get_review_comments`, `get_check_runs`, and `get_status` fully (`perPage: 100`; cursor via `after` for `get_review_comments`). Step 4 deliberately reads a single page. If the PR is merged or closed: return `"Merged/Closed"` — run the `dismiss` action for it. @@ -101,10 +101,10 @@ If the PR is merged or closed: return `"Merged/Closed"` — run the `dismiss` ac ```text PR #NUMBER: TITLE Author: AUTHOR | Created: DATE | Updated: DATE | Draft: yes/no -Role: [assigned] [reviewer] +Role: [assigned] [reviewer — individual/team] Labels: LABEL1, LABEL2 -CI: X/Y passing, Z failing [CHECK_NAMES], W pending → green/red/yellow/pending +CI: X/Y passing, Z failing [CHECK_NAMES], W pending → green/red/pending My Last Review: STATE on DATE (or: Not yet reviewed) Changes Since Review: N new commits — SUMMARY (or: None / N/A) My Open Threads: N unresolved of M total @@ -143,7 +143,9 @@ Sort the agent results by urgency (order matches the category list above). Prese | `reviewer` only | Skip / Remove review request | | both | Skip / Unassign me / Remove review request / Unsubscribe (both) | -In each option's description, disclose when the action will also mute the PR: any choice that removes your only remaining role (and `Unsubscribe (both)`) stops all future notifications for that PR (see Phase 5). +When the review request is team-based, replace "Remove review request" with "Unsubscribe" — a team request can't be individually removed, so muting is the only opt-out. + +In each option's description, disclose when the action will also mute the PR: any choice that removes your only remaining role, and any explicit Unsubscribe, stops all future notifications for that PR (see Phase 5). ## Phase 5: Execute Actions @@ -159,8 +161,8 @@ For each PR where the user chose an action, execute all sub-steps in parallel ac **If not `HAS_PROW`:** -- Unassign: `issue_write(method: "update", owner, repo, issue_number: PR, assignees: [all current assignees except USERNAME])` -- Remove review: `gh api repos/OWNER/REPO/pulls/PR/requested_reviewers -X DELETE -f 'reviewers[]=USERNAME'` (no GitHub MCP tool can remove a review request). If the request is team-based — USERNAME absent from `requested_reviewers` but a team appears in `requested_teams` — do **not** delete the team's request (that would remove it for every teammate); report that the request came via the team and can only be unsubscribed from, not individually removed. If `gh` is unavailable or unauthenticated, skip this call, note in the final summary that the review request must be removed manually, and continue with the remaining actions. +- Unassign: re-read the PR (`get`) first and build the list from that response — `issue_write` replaces the full assignee set, so stale investigation data would silently drop assignees added meanwhile. Then `issue_write(method: "update", owner, repo, issue_number: PR, assignees: [current assignees except USERNAME])` +- Remove review: `gh api repos/OWNER/REPO/pulls/PR/requested_reviewers -X DELETE -f 'reviewers[]=USERNAME'` (no GitHub MCP tool can remove a review request). If the request is team-based — USERNAME absent from `requested_reviewers` but a team appears in `requested_teams` — do **not** delete the team's request (that would remove it for every teammate); Phase 4 offers Unsubscribe for these instead. If `gh` is unavailable or unauthenticated, skip this call, note in the final summary that the review request must be removed manually, and continue with the remaining actions. - Both: execute both ### Clear GitHub notifications @@ -168,7 +170,7 @@ For each PR where the user chose an action, execute all sub-steps in parallel ac Using the Notification lookup from Phase 2 (fresh call for this phase), for each thread matching the PR: 1. `dismiss_notification(threadID, state: "done")` -2. `manage_notification_subscription(notificationID, action: "ignore")` — **only if the chosen action removed every role you held on the PR** (unassign when you were only assigned, remove review request when you were only a reviewer, or both removals when you held both roles) **and the removal is confirmed**: a skipped `gh` call doesn't count, and a Prow comment only queues the bot command — re-read the PR (`get`) and check the role is actually gone; if it is still present, dismiss only and report the removal as pending. If a role remains or a removal is unconfirmed, dismiss only, so future activity still notifies you. +2. `manage_notification_subscription(notificationID, action: "ignore")` — when the user explicitly chose **Unsubscribe**, or when **the chosen action removed every role you held on the PR** (unassign when you were only assigned, remove review request when you were only a reviewer, or both removals when you held both roles) **and the removal is confirmed**: a skipped `gh` call doesn't count, and a Prow comment only queues the bot command — re-read the PR (`get`) and check the role is actually gone; if it is still present, dismiss only and report the removal as pending. If a role remains or a removal is unconfirmed (and Unsubscribe wasn't explicitly chosen), dismiss only, so future activity still notifies you. ### Summary From 73bf2abc65dcbc8b3bd8363fa8d809676bb68495 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 00:51:08 +0000 Subject: [PATCH 12/14] fix(github): align triage-prs with actual MCP server behavior Batched fixes from Copilot's review of the previous round: - Match the Prow tide status context exactly; a substring match could classify unrelated CI (e.g. tideways) as Prow and trigger bot commands and auto-unsubscribes on a non-Prow repo - Infer team-based review requests from the reviewer role plus USERNAME's absence from requested_reviewers, since the MCP get response omits requested_teams entirely - Call get_status once and flag an incomplete CI summary via total_count, since the server ignores pagination for it Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- github/skills/triage-prs/SKILL.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/github/skills/triage-prs/SKILL.md b/github/skills/triage-prs/SKILL.md index 0c9f2da..f2e914c 100644 --- a/github/skills/triage-prs/SKILL.md +++ b/github/skills/triage-prs/SKILL.md @@ -58,7 +58,7 @@ Candidates are the union of the first two searches, deduplicated by PR number an If no candidates: report the sweep result, print "No open PRs found where you are assigned or a requested reviewer in OWNER/REPO." and stop. -**Detect Prow:** call `pull_request_read(method: "get_status")` on candidates in order, setting `HAS_PROW = true` as soon as any context name contains `"tide"`. A candidate with other statuses but no tide context is not proof of absence — keep checking. Conclude non-Prow after five candidates that returned status contexts but none containing tide (status-less candidates, such as new or draft PRs, don't count toward the five), or when candidates are exhausted. +**Detect Prow:** call `pull_request_read(method: "get_status")` on candidates in order, setting `HAS_PROW = true` as soon as a status context named exactly `tide` appears (substring matches like `tideways` are unrelated CI, not Prow). A candidate with other statuses but no `tide` context is not proof of absence — keep checking. Conclude non-Prow after five candidates that returned status contexts but none named `tide` (status-less candidates, such as new or draft PRs, don't count toward the five), or when candidates are exhausted. **Classify** each candidate from its search-result data (state, labels, engagement) — first matching rule wins: @@ -80,11 +80,11 @@ Report a summary line, e.g.: "Swept 5 stale closed-PR notifications. Auto-dismis ## Phase 3: Parallel Investigation -For `manual` PRs only, dispatch one Agent per PR using `model: "sonnet"`, in parallel batches of at most 10. Each agent prompt must be self-contained — pass `owner`, `repo`, `pr_number`, and `username` explicitly. +For `manual` PRs only, dispatch one Agent per PR using `model: "sonnet"`, in parallel batches of at most 10. Each agent prompt must be self-contained — pass `owner`, `repo`, `pr_number`, `username`, and the PR's roles explicitly. Each agent performs these `pull_request_read` calls: -1. `get` — title, author, draft status, labels, timestamps; whether your review request is individual (USERNAME in `requested_reviewers`) or team-based (only via `requested_teams`) +1. `get` — title, author, draft status, labels, timestamps; review-request source: individual if USERNAME appears in `requested_reviewers`, otherwise team-based (the PR carries the `reviewer` role from the search but you aren't individually requested — the response does not include `requested_teams`) 2. `get_check_runs` **and** `get_status` — merge both (external CI such as Prow reports via status contexts); aggregate to: X/Y passing, Z failing [names], W pending 3. `get_reviews` — date of your most recent review (any state); for your effective decision use your latest **active** `APPROVED` or `CHANGES_REQUESTED` review — `DISMISSED` reviews are inactive history, and a later `COMMENTED` review does not supersede the decision; note all other reviewers and their states 4. `get_files(perPage: 100)` — first page only; file count ("100+" if truncated), key filenames, additions/deletions @@ -92,7 +92,7 @@ Each agent performs these `pull_request_read` calls: 6. `get_review_comments` — count your unresolved vs resolved review threads 7. `get_comments(perPage: 100)` — issue comments are oldest-first, so if the first page is full, follow pagination and keep the most recent ~100 comments (the final page plus the prior page when the final page is short). Note mentions or questions directed at you. -Paginate `get_reviews`, `get_commits`, `get_review_comments`, `get_check_runs`, and `get_status` fully (`perPage: 100`; cursor via `after` for `get_review_comments`). Step 4 deliberately reads a single page. +Paginate `get_reviews`, `get_commits`, `get_review_comments`, and `get_check_runs` fully (`perPage: 100`; cursor via `after` for `get_review_comments`). Call `get_status` once — the server does not paginate it — and mark the CI summary incomplete if its `total_count` exceeds the contexts returned. Step 4 deliberately reads a single page. If the PR is merged or closed: return `"Merged/Closed"` — run the `dismiss` action for it. @@ -162,7 +162,7 @@ For each PR where the user chose an action, execute all sub-steps in parallel ac **If not `HAS_PROW`:** - Unassign: re-read the PR (`get`) first and build the list from that response — `issue_write` replaces the full assignee set, so stale investigation data would silently drop assignees added meanwhile. Then `issue_write(method: "update", owner, repo, issue_number: PR, assignees: [current assignees except USERNAME])` -- Remove review: `gh api repos/OWNER/REPO/pulls/PR/requested_reviewers -X DELETE -f 'reviewers[]=USERNAME'` (no GitHub MCP tool can remove a review request). If the request is team-based — USERNAME absent from `requested_reviewers` but a team appears in `requested_teams` — do **not** delete the team's request (that would remove it for every teammate); Phase 4 offers Unsubscribe for these instead. If `gh` is unavailable or unauthenticated, skip this call, note in the final summary that the review request must be removed manually, and continue with the remaining actions. +- Remove review: `gh api repos/OWNER/REPO/pulls/PR/requested_reviewers -X DELETE -f 'reviewers[]=USERNAME'` (no GitHub MCP tool can remove a review request). If the request is team-based — the PR carries the `reviewer` role but USERNAME is absent from `requested_reviewers` — do **not** attempt to delete the team's request (that would remove it for every teammate); Phase 4 offers Unsubscribe for these instead. If `gh` is unavailable or unauthenticated, skip this call, note in the final summary that the review request must be removed manually, and continue with the remaining actions. - Both: execute both ### Clear GitHub notifications From db925b00369c483fc506ca5eab7dc345f6b613c1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 00:52:34 +0000 Subject: [PATCH 13/14] fix(github): bring triage-prs back under the skill token budget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trim accumulated wording from the review-hardening rounds — no behavior changes — to clear the skillsaw context-budget warning that failed CI's lint check (3,081 tokens vs the 3,000 warn limit). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- github/skills/triage-prs/SKILL.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/github/skills/triage-prs/SKILL.md b/github/skills/triage-prs/SKILL.md index f2e914c..26daf80 100644 --- a/github/skills/triage-prs/SKILL.md +++ b/github/skills/triage-prs/SKILL.md @@ -26,7 +26,7 @@ allowed-tools: Investigate open PRs that need your attention in the target repo, then present batched triage with options to unassign, remove review requests, or unsubscribe. -Tool names below are short forms of the GitHub MCP tools listed in `allowed-tools` (e.g. `get_me` → `mcp__plugin_github_github__get_me`). +Tool names below are short forms of the `allowed-tools` entries (`get_me` → `mcp__plugin_github_github__get_me`). ## Repo Context @@ -58,7 +58,7 @@ Candidates are the union of the first two searches, deduplicated by PR number an If no candidates: report the sweep result, print "No open PRs found where you are assigned or a requested reviewer in OWNER/REPO." and stop. -**Detect Prow:** call `pull_request_read(method: "get_status")` on candidates in order, setting `HAS_PROW = true` as soon as a status context named exactly `tide` appears (substring matches like `tideways` are unrelated CI, not Prow). A candidate with other statuses but no `tide` context is not proof of absence — keep checking. Conclude non-Prow after five candidates that returned status contexts but none named `tide` (status-less candidates, such as new or draft PRs, don't count toward the five), or when candidates are exhausted. +**Detect Prow:** call `pull_request_read(method: "get_status")` on candidates in order, setting `HAS_PROW = true` when a status context named exactly `tide` appears (substring matches like `tideways` are not Prow). Other statuses without `tide` aren't proof of absence — keep checking. Conclude non-Prow after five status-bearing candidates with no `tide` (status-less candidates don't count), or when candidates are exhausted. **Classify** each candidate from its search-result data (state, labels, engagement) — first matching rule wins: @@ -84,7 +84,7 @@ For `manual` PRs only, dispatch one Agent per PR using `model: "sonnet"`, in par Each agent performs these `pull_request_read` calls: -1. `get` — title, author, draft status, labels, timestamps; review-request source: individual if USERNAME appears in `requested_reviewers`, otherwise team-based (the PR carries the `reviewer` role from the search but you aren't individually requested — the response does not include `requested_teams`) +1. `get` — title, author, draft status, labels, timestamps; review-request source: individual if USERNAME is in `requested_reviewers`, else team-based (the response omits `requested_teams`, so infer from the `reviewer` role) 2. `get_check_runs` **and** `get_status` — merge both (external CI such as Prow reports via status contexts); aggregate to: X/Y passing, Z failing [names], W pending 3. `get_reviews` — date of your most recent review (any state); for your effective decision use your latest **active** `APPROVED` or `CHANGES_REQUESTED` review — `DISMISSED` reviews are inactive history, and a later `COMMENTED` review does not supersede the decision; note all other reviewers and their states 4. `get_files(perPage: 100)` — first page only; file count ("100+" if truncated), key filenames, additions/deletions @@ -161,8 +161,8 @@ For each PR where the user chose an action, execute all sub-steps in parallel ac **If not `HAS_PROW`:** -- Unassign: re-read the PR (`get`) first and build the list from that response — `issue_write` replaces the full assignee set, so stale investigation data would silently drop assignees added meanwhile. Then `issue_write(method: "update", owner, repo, issue_number: PR, assignees: [current assignees except USERNAME])` -- Remove review: `gh api repos/OWNER/REPO/pulls/PR/requested_reviewers -X DELETE -f 'reviewers[]=USERNAME'` (no GitHub MCP tool can remove a review request). If the request is team-based — the PR carries the `reviewer` role but USERNAME is absent from `requested_reviewers` — do **not** attempt to delete the team's request (that would remove it for every teammate); Phase 4 offers Unsubscribe for these instead. If `gh` is unavailable or unauthenticated, skip this call, note in the final summary that the review request must be removed manually, and continue with the remaining actions. +- Unassign: re-read the PR (`get`) first — `issue_write` replaces the full assignee set, so stale data would drop assignees added meanwhile — then `issue_write(method: "update", owner, repo, issue_number: PR, assignees: [current assignees except USERNAME])` +- Remove review: `gh api repos/OWNER/REPO/pulls/PR/requested_reviewers -X DELETE -f 'reviewers[]=USERNAME'` (no GitHub MCP tool can remove a review request). If the request is team-based — the PR carries the `reviewer` role but USERNAME is absent from `requested_reviewers` — do **not** attempt to delete the team's request (that would remove it for every teammate); Phase 4 offers Unsubscribe for these instead. If `gh` is unavailable or unauthenticated, skip this call, note in the summary that the request must be removed manually, and continue. - Both: execute both ### Clear GitHub notifications @@ -170,7 +170,7 @@ For each PR where the user chose an action, execute all sub-steps in parallel ac Using the Notification lookup from Phase 2 (fresh call for this phase), for each thread matching the PR: 1. `dismiss_notification(threadID, state: "done")` -2. `manage_notification_subscription(notificationID, action: "ignore")` — when the user explicitly chose **Unsubscribe**, or when **the chosen action removed every role you held on the PR** (unassign when you were only assigned, remove review request when you were only a reviewer, or both removals when you held both roles) **and the removal is confirmed**: a skipped `gh` call doesn't count, and a Prow comment only queues the bot command — re-read the PR (`get`) and check the role is actually gone; if it is still present, dismiss only and report the removal as pending. If a role remains or a removal is unconfirmed (and Unsubscribe wasn't explicitly chosen), dismiss only, so future activity still notifies you. +2. `manage_notification_subscription(notificationID, action: "ignore")` — when the user explicitly chose **Unsubscribe**, or when the chosen action removed **every role you held** and the removal is confirmed: a skipped `gh` call doesn't count, and a Prow comment only queues the bot command — re-read the PR (`get`) and check the role is gone (if not, report the removal as pending). Otherwise dismiss only, so future activity still notifies you. ### Summary From 5e07b811e8912e91d93e1f50f5442fe398448445 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 5 Aug 2026 01:51:38 +0000 Subject: [PATCH 14/14] docs: bind "always loaded" to the MCP server in the plugin table The comma chain read ambiguously; the always-loaded component is the MCP server (alwaysLoad in .mcp.json), not the on-demand triage skill. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01HjR9S1B6GLtPLBU1d596yK --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c0e6cd..ec5bf6e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ claude plugin marketplace add cblecker/claude-plugins | Plugin | Description | |----------------------------------------------|--------------------------------------------------------------------------------------------------| | [git](./git) | Dynamic git instructions via SessionStart hook with mainline detection, conventional commits, fork handling, and safety guardrails | -| [github](./github) | GitHub MCP server with selected toolsets enabled, always loaded, plus a PR triage skill | +| [github](./github) | GitHub MCP server with selected toolsets enabled and always loaded, plus a PR triage skill | | [pr-review-toolkit](./pr-review-toolkit) | Comprehensive PR review board using shared workflow context | | [gws](./gws) | Google Workspace CLI skills for Gmail, Calendar, Drive, Docs, Sheets, Slides, and Meet | | [rh-dataverse](./rh-dataverse) | Red Hat Dataverse MCP server |