PR Feedback Resolver
Structured resolver for PR review comments. /resolve-pr-feedback fetches unresolved review threads via GraphQL, triages new vs already-handled, and if 3+ new items arrive (or a cross-invocation signal fires) runs cluster analysis - categorizes each into 11 fixed concern categories and groups by category + spatial proximity. Clusters surface systemic issues instead of dispatching 10 one-off fixes. Parallel pr-comment-resolver subagents apply unambiguous fixes, post inline replies via gh api, and resolve threads; taste questions are batched for human decision.
Tags
README
PR Feedback Resolver
Structured resolver for PR review comments. /resolve-pr-feedback fetches unresolved review threads via GraphQL, triages new vs already-handled, and - when 3+ new comments land - runs cluster analysis across 11 fixed concern categories before dispatching parallel resolver subagents.
Why Cluster
A PR with ten review comments usually encodes two or three real concerns. Dispatching ten one-off fixes churns files, produces ten tiny commits, and still leaves the systemic concern un-addressed. Clustering by concern category + spatial proximity surfaces the systemic view before any code is touched.
The cluster-gate only activates at 3+ new threads. Below that, overhead of categorization is not justified - the skill dispatches per-thread.
What This Module Provides
Files installed globally to ~/.claude/:
| Source | Target | Purpose |
|---|---|---|
skills/resolve-pr-feedback/SKILL.md |
skills/resolve-pr-feedback/SKILL.md |
/resolve-pr-feedback - orchestrator |
skills/resolve-pr-feedback/references/cluster-categories.md |
skills/resolve-pr-feedback/references/cluster-categories.md |
11 fixed categories + proximity + autofix routing |
scripts/get-pr-comments |
scripts/get-pr-comments |
GraphQL fetcher for unresolved threads |
agents/pr-comment-resolver.md |
agents/pr-comment-resolver.md |
Fan-out subagent that implements one cluster |
Manual Installation
# From the CCGM repo root:
mkdir -p ~/.claude/skills/resolve-pr-feedback/references
mkdir -p ~/.claude/scripts
mkdir -p ~/.claude/agents
cp modules/pr-feedback/skills/resolve-pr-feedback/SKILL.md \
~/.claude/skills/resolve-pr-feedback/SKILL.md
cp modules/pr-feedback/skills/resolve-pr-feedback/references/cluster-categories.md \
~/.claude/skills/resolve-pr-feedback/references/cluster-categories.md
cp modules/pr-feedback/scripts/get-pr-comments \
~/.claude/scripts/get-pr-comments
chmod +x ~/.claude/scripts/get-pr-comments
cp modules/pr-feedback/agents/pr-comment-resolver.md \
~/.claude/agents/pr-comment-resolver.md
Requirements
ghCLI, authenticated (gh auth login)jqon the PATH- Bash 4+
Usage
Standard run
/resolve-pr-feedback pr:123
Fetches unresolved threads for PR 123 in the current repo, triages, clusters if 3+ new threads, and dispatches resolver subagents in parallel after confirmation.
Modes
/resolve-pr-feedback pr:123 mode:autofix # no prompts; write run artifact
/resolve-pr-feedback pr:123 mode:report-only # fetch + triage + cluster, no dispatch
/resolve-pr-feedback pr:123 mode:headless # skill-to-skill composition
Filters
/resolve-pr-feedback pr:123 cluster:force # cluster even for 1-2 threads
/resolve-pr-feedback pr:123 cluster:skip # per-thread dispatch even for 3+
/resolve-pr-feedback pr:123 only:1,3,5 # only listed thread indices
/resolve-pr-feedback pr:123 repo:owner/name # override repo
Fetcher standalone
The fetcher script is useful on its own when you want to inspect thread state:
# JSON (machine-consumable)
bash modules/pr-feedback/scripts/get-pr-comments 123 --state unresolved
# Markdown (human-readable)
bash modules/pr-feedback/scripts/get-pr-comments 123 --format markdown
Cluster Categories
The 11 fixed categories are:
error-handling, validation, type-safety, testing, naming, style-consistency, architecture, performance, security, documentation, other.
Each has a definition, sample reviewer phrases, and an autofix-class mapping (safe_auto / gated_auto / manual / advisory). See skills/resolve-pr-feedback/references/cluster-categories.md.
Clusters group threads by category and spatial proximity: same file, shared subtree (e.g., src/auth/ but not the repo root), or cross-cutting across 3+ files. A cross-cutting cluster is a systemic finding, not N one-off fixes.
Dependencies
skill-authoring- skills follow the authoring discipline (reference files via backticks, imperative voice, one command per Bash call)subagent-patterns- cluster fan-out uses pass-paths-not-contents; completion statuses use the four-state protocol
Non-Goals
This module does not:
- Replace human taste on architectural comments.
manualclusters are batched for explicit human decision, not auto-applied. - Auto-dispatch
security-category fixes. Security always requires at least one human-in-the-loop confirmation. - Resolve threads with no code change on record. Replies are cheap; closing a thread without a fix is dishonest.
- Manage PR workflow beyond reviews. For creating PRs, see
/prand/cpmincommands-core. For review orchestration beyond comments, see thece-reviewmodule.
Source
Ported from EveryInc/compound-engineering-plugin's skills/resolve-pr-feedback/SKILL.md and scripts/get-pr-comments. Adaptations:
- Bundled as a standalone CCGM module rather than part of a larger compound-engineering plugin
- Mode token names match CCGM's skill-authoring convention (
mode:interactive,mode:autofix,mode:report-only,mode:headless) - Cluster categories and autofix routing extracted to
references/cluster-categories.mdso the skill body does not carry them in every invocation - Resolver agent lives under
agents/per CCGM's agents-directory convention - Four-state completion protocol (
DONE/DONE_WITH_CONCERNS/BLOCKED/NEEDS_CONTEXT) for subagent returns
Will install
| Path | Action | Target | Type |
|---|---|---|---|
skills/resolve-pr-feedback/SKILL.md | → | skills/resolve-pr-feedback/SKILL.md | skill |
skills/resolve-pr-feedback/references/cluster-categories.md | → | skills/resolve-pr-feedback/references/cluster-categories.md | doc |
scripts/get-pr-comments | → | scripts/get-pr-comments | script |
agents/pr-comment-resolver.md | → | agents/pr-comment-resolver.md | agent |
Dependencies
Required by
No other module depends on this one.
Included in presets
Install this module
Agent prompt
Recommended for agent users -- hands the whole install off to your assistant.
Fetch https://cd23a9be.ccgm-site.pages.dev/modules/pr-feedback.md and install this module into my Claude Code setup.
Native plugin marketplace
One command via the native plugin marketplace -- additive, does not merge settings.json.
claude plugin install pr-feedback@ccgm
The marketplace path is additive, not a replacement: it installs commands, agents, and skills as native plugin components, but it does not perform the bash installer's deep settings.json merge, and it does not write the always-loaded global CLAUDE.md context. Rules are only injected via an opt-in SessionStart hook rather than being auto-loaded. Use the bash installer when those pieces matter to you.
Files
skill (1)
skills/resolve-pr-feedback/SKILL.md
---
name: resolve-pr-feedback
description: >
Structured resolver for PR review comments. Fetches unresolved review threads via GraphQL, triages new vs already-handled, and if 3+ new items arrive (or a cross-invocation signal fires) runs cluster analysis across 11 fixed concern categories grouped by spatial proximity. Dispatches parallel pr-comment-resolver subagents for unambiguous fixes, posts inline replies via gh api, and resolves threads; taste questions are batched for human decision. Skips cluster overhead when only 1-2 new comments exist.
Triggers: resolve pr feedback, address review comments, work through pr comments, burn down pr review, fix reviewer feedback, unresolved threads, tackle pr review.
disable-model-invocation: true
---
# /resolve-pr-feedback - Structured PR Review Resolver
Turn a stack of PR review comments into a small number of cluster-aware fix plans, dispatch them in parallel, and close the loop on GitHub. The cluster-gate is the key idea: ten one-off "nit" replies are usually three systemic issues, and surfacing the systemic view changes the fix.
This skill does **not** replace human taste on architectural comments. It batches those for explicit decisions while burning through the unambiguous fixes in parallel.
## When to Run
Run `/resolve-pr-feedback` after:
- A PR review lands with 3+ unresolved threads
- A reviewer comes back with a second round after prior fixes
- You return to a stale PR and want to see what threads are still open
Do not run:
- Before a review exists (no comments to resolve)
- During active feature development on the same files - the parallel fan-out will conflict with in-flight edits
- On PRs that are already merged - threads cannot be resolved post-merge
## Mode Selection
Parse `$ARGUMENTS` for a mode token:
- `mode:interactive` (default) - Plan clusters, show the plan, confirm before dispatching
- `mode:autofix` - Dispatch without asking; write a run artifact at `.claude/pr-feedback/runs/YYYYMMDD-HHMM.md`
- `mode:report-only` - Fetch, triage, and cluster; stop before dispatch. Safe for concurrent runs and for audit-only uses
- `mode:headless` - For skill-to-skill composition. Structured output envelope, no prompts, terminal "Resolve complete" line
See `modules/subagent-patterns/rules/subagent-patterns.md` for the full mode contract.
## Arguments
- `pr:NNN` - target PR number. Required unless the current branch has exactly one open PR (in which case pick it).
- `repo:owner/name` - override the repo. Default is the current working directory's repo.
- `only:1,3,5` - restrict to explicit thread indices from the triage listing
- `cluster:force` - force cluster analysis even for 1-2 threads
- `cluster:skip` - force per-thread dispatch even for 3+ threads
## Phase 1: Fetch Unresolved Threads
Invoke the bundled fetcher script. It uses GraphQL because REST cannot tell you whether a thread is resolved:
```
!`bash scripts/get-pr-comments NNN --state unresolved --format json`
```
The script returns structured JSON with one entry per thread - `thread_id`, `path`, `line`, `is_resolved`, `is_outdated`, and all comments in chronological order.
If the script exits non-zero:
- `gh not authenticated` - stop and tell the user to run `gh auth login`
- `repo not found` or `pr not found` - stop and ask for the correct `pr:` / `repo:` argument
- Rate limit error - wait and retry once; if it fails again, stop and report
## Phase 2: Triage - New vs Already-Handled
Not every unresolved thread needs new work. Some were already addressed in a later commit the reviewer has not seen yet.
For each thread, classify as:
- **new** - the last comment is from a reviewer and no commit on the branch addresses the specific `path:line` after that comment. These are what the skill works on.
- **handled-not-replied** - a later commit touched the relevant file and line range, but no reply was posted. Reply acknowledging and referencing the commit SHA; do not re-fix.
- **handled-and-replied** - both a fix commit and a reply exist; the reviewer has not resolved the thread yet. Leave alone. Note in the plan.
- **outdated** - thread's `is_outdated` flag is true. Reply suggesting the reviewer re-review the current code; do not re-fix without new evidence that the concern still stands.
Evidence for "addressed by commit":
- `git log --format="%H %s" origin/main..HEAD -- <path>` where a commit message references the concern, OR
- `git log -L :<function>:<path> origin/main..HEAD` shows a change in the called-out region after the thread's `created_at`
When unsure, classify as `new` and let the cluster analysis route it. Triage errors are cheap; missed fixes are not.
## Phase 3: Cluster Gate
Count `new` threads. Decide whether to run cluster analysis:
- `new_count >= 3` - run clustering (Phase 4)
- `new_count in {1, 2}` - skip clustering, dispatch one `pr-comment-resolver` per thread with a thin spec (Phase 6)
- `cluster:force` - run clustering regardless
- `cluster:skip` - skip clustering regardless
Cross-invocation signal: if `.claude/pr-feedback/runs/` contains a recent artifact (within 24 hours, same `pr:`) with unfinished clusters, resume from that artifact rather than starting fresh. Merge newly arrived comments into the existing cluster set.
## Phase 4: Cluster Analysis
For each `new` thread, classify into one of 11 fixed categories. See `references/cluster-categories.md` for the list, definitions, sample phrases, spatial-proximity rules, and autofix-class routing.
Output of this phase is a list of clusters, each with:
- `category` (one of the 11)
- `proximity` (`same-file` / `subtree:<prefix>` / `cross-cutting`)
- `thread_ids` (list of GraphQL thread node ids this cluster covers)
- `paths` (deduped list of file paths touched)
- `autofix_class` (`safe_auto` / `gated_auto` / `manual` / `advisory`)
- `fix_hypothesis` (one paragraph: what a resolver subagent should do)
- `taste_questions` (if `manual`, the open decisions to surface)
Example:
```
Cluster 1: validation / same-file / src/api/users.ts
threads: 3 (T1, T4, T7)
autofix: safe_auto
hypothesis: Add input validation for empty string, negative age,
and unknown role - one validator function covers all three.
Cluster 2: architecture / cross-cutting
threads: 2 (T2, T5)
autofix: manual
taste_questions:
- Extract AuthContext into a shared package vs keep inline?
- Should the middleware own rate limiting or delegate?
```
Three threads that individually read as nits often become one architectural finding at this stage. That is the point.
## Phase 5: Plan and Confirm
Print the plan:
```
Plan: resolve NNN.
fetched: N threads
triaged:
new: M
handled-not-replied: K
handled-and-replied: J
outdated: L
clusters:
safe_auto: P (dispatch in parallel)
gated_auto: Q (dispatch after confirm)
manual: R (batch for human decision)
advisory: S (reply-only, no code change)
```
In `mode:interactive`: list each `gated_auto` cluster and ask per-cluster `[y/skip/manual]`. `manual` promotes it to the manual batch for the end. `skip` leaves the cluster alone for this run.
In `mode:autofix`: proceed with `safe_auto` and `gated_auto` automatically. Never auto-dispatch `manual` or `security`-category clusters without at least one human-in-the-loop prompt.
In `mode:report-only`: stop here and write the plan to stdout. Do not dispatch.
In `mode:headless`: proceed with `safe_auto` only. Return `manual`, `gated_auto`, and `advisory` clusters in the output envelope for the caller to handle.
## Phase 6: Dispatch pr-comment-resolver Subagents
Dispatch one `pr-comment-resolver` per cluster (or per thread, when clustering was skipped), in parallel. Pass paths, not contents - see `modules/subagent-patterns/rules/subagent-patterns.md`.
> **Concurrency — avoid the 429 throttle.** Dispatch resolvers at `model: "sonnet"` with medium reasoning effort. Clustering already keeps the count low, but a busy PR can yield more clusters than is safe to burst — launch **at most 4 at once**, and if there are more, run them in sequential waves of 4 rather than one message. Bursting too many heavy agents trips a server-side rate limit (`Server is temporarily limiting requests · Rate limited`) that fails the whole batch; if you hit it, wait 30–60s and re-dispatch only the unfinished clusters in waves of ≤4. See `~/.claude/rules/concurrency-and-rate-limits.md`.
Per-subagent spec:
**Objective** - Implement the `fix_hypothesis` for this cluster; post an inline reply on each covered thread; resolve each thread if the reply is a fix (not an acknowledgment).
**Context (paths)**:
- Path to a temp cluster brief written by this skill at `.claude/pr-feedback/runs/YYYYMMDD-HHMM/cluster-NNN.md`
- Paths from the cluster's `paths` field
- Path to the repo's `AGENTS.md` or `CLAUDE.md` for house style
**Constraints**:
- Modify only files listed in the cluster brief. Expanding scope is `BLOCKED`, not "while I am here."
- One commit per cluster; message format `pr:NNN resolve <cluster-slug>`.
- Post one inline reply per thread referencing the commit SHA; do not paraphrase the fix across threads - cite the commit once.
- Resolve the thread via `gh api` only when the reply represents a code change. Advisory replies do not resolve.
**Deliverable** (four-state):
- `DONE` + diff + commit SHA + list of threads replied and resolved
- `DONE_WITH_CONCERNS` + same + concerns (e.g., "fixed but tests are missing")
- `BLOCKED` + reason (e.g., "fix requires editing a file not in scope")
- `NEEDS_CONTEXT` + specific missing info
## Phase 7: Two-Stage Review
Do not trust self-reports. Two passes (see subagent-patterns):
1. **Spec compliance** - Did the subagent respect the cluster's `paths` constraint? Commit message format correct? Replies posted?
2. **Code quality** - Does the diff match project patterns? Any missed edge cases the cluster hypothesis implied?
Re-dispatch with specific feedback for any subagent that failed either stage. Do not silently patch subagent output.
## Phase 8: Post Advisory Replies
For `advisory` clusters, no code changes. Post a single acknowledging reply on each covered thread. Example voice:
```
Thanks - leaving as-is for this PR; noted for a future cleanup pass.
```
Do not post content-free acknowledgments. If there is genuinely nothing to say, leave the thread alone for the reviewer to resolve.
## Phase 9: Manual Batch Report
For `manual` clusters, do not dispatch. Instead, write a decision brief to `.claude/pr-feedback/runs/YYYYMMDD-HHMM/manual-decisions.md` with:
- Each cluster's `fix_hypothesis` and `taste_questions`
- Proposed directions (2-3 options per question when possible, with trade-offs)
- Recommended default (the one to pick if the user does not weigh in)
Surface this file to the user at the end of the run. Do not auto-post anything to GitHub for manual clusters.
## Phase 10: Run Artifact and Summary
Write `.claude/pr-feedback/runs/YYYYMMDD-HHMM.md` summarizing:
```
PR: owner/repo#NNN
fetched: N threads
resolved: M threads (commits: <shas>)
replied: K threads (advisory or handled-not-replied)
manual: J threads (see manual-decisions.md)
blocked: P threads (see per-cluster notes)
```
Print the same summary to the terminal. In `mode:headless`, the envelope contains the same fields as structured JSON, and the final line is literally `Resolve complete` so the caller can detect termination.
If the user was running with `/compound` installed and the run surfaced a pattern across clusters, suggest (do not auto-invoke):
```
Consider running /compound to capture the pattern that surfaced
across these clusters - this looked like a systemic <category>
issue.
```
## GraphQL vs REST
The REST `gh pr view --comments` surface returns comments but not thread state (`isResolved`). This skill uses GraphQL via the bundled `scripts/get-pr-comments` fetcher. `gh pr view --comments` is still a fine first-look tool for humans, but it cannot drive the cluster gate because it does not distinguish resolved from unresolved threads.
Resolving a thread programmatically uses the GraphQL `resolveReviewThread` mutation via `gh api graphql`. The `pr-comment-resolver` agent handles this.
## Why Cluster
A PR with ten review comments typically encodes two or three real concerns. Dispatching ten one-off fixes churns files, produces ten tiny commits, and still leaves the systemic concern un-addressed. Clustering by category + spatial proximity surfaces the systemic view before any code is touched. That is the only reason the gate exists. Below three new threads, the ratio of orchestration overhead to insight is wrong; above three, the opposite.
agent (1)
agents/pr-comment-resolver.md
---
name: pr-comment-resolver
description: >
Implements the fix hypothesis for a single PR review cluster (or a single thread when clustering was skipped), posts inline replies on each covered thread, and resolves threads where the reply represents a code change. Dispatched in parallel by /resolve-pr-feedback. Scope is strictly limited to the paths listed in the cluster brief.
tools: Read, Edit, Write, Glob, Grep, Bash
---
# pr-comment-resolver
Resolve one cluster of PR review threads. A cluster is one concern category in one spatial neighborhood (same file, subtree, or cross-cutting). The caller (`/resolve-pr-feedback`) has already done the triage, categorization, and scope definition. Your job is to land the fix and close the loop on GitHub.
You are a worker, not an orchestrator. Do not re-triage, do not re-classify, do not expand scope. If the fix requires editing a path not listed in the cluster brief, return `BLOCKED` with a clear reason - let the orchestrator decide what to do.
## Inputs
The caller passes paths (not contents - see `modules/subagent-patterns/rules/subagent-patterns.md`):
- `cluster_brief_path` (required) - path to the per-cluster brief written by the orchestrator at `.claude/pr-feedback/runs/YYYYMMDD-HHMM/cluster-NNN.md`
- `paths` (required) - list of file paths the cluster covers; modifying anything else is out of scope
- `house_style_path` (optional) - path to repo `AGENTS.md` or `CLAUDE.md`
- `pr_number`, `repo` (required) - for posting replies and resolving threads
The cluster brief contains:
- `category` (one of the 11 fixed categories)
- `autofix_class` (`safe_auto` / `gated_auto`)
- `fix_hypothesis` - one paragraph on what to do
- `threads` - list of `{thread_id, path, line, last_comment_body, last_comment_author, url}`
`manual` and `advisory` clusters are not dispatched to this agent; the orchestrator handles them.
## Procedure
### 1. Read the Cluster Brief and House Style
Read `cluster_brief_path` first, then `house_style_path` if present. Do not read `paths` yet - you may not need all of them.
### 2. Read Only What You Need
For each file in `paths`, read only when the fix hypothesis requires it. If the hypothesis says "add validation in the `createUser` handler", read the file containing that handler; do not read unrelated files in the cluster.
### 3. Implement the Fix
Apply the edits described by `fix_hypothesis`. Follow house style. One fix per cluster - do not bundle improvements that the cluster did not ask for.
If partway through you realize the fix needs a file not in `paths`, stop and return `BLOCKED` with:
- which additional path is needed
- why the listed paths are insufficient
- what you would change if given that path
Do not silently expand scope.
### 4. Verify
Run the project's verification before claiming completion - lint, type-check, tests as appropriate. See `modules/verification/rules/verification.md` for the evidence requirement. If the project has a standard pre-push command, run that.
If verification fails on something unrelated to your change (a pre-existing flake), report it in your `DONE_WITH_CONCERNS` envelope; do not fix unrelated failures in this run.
### 5. Commit
One commit per cluster. Message format:
```
pr:NNN resolve <cluster-slug>
<one sentence on the concern the cluster surfaced>
<one sentence on the fix mechanism>
```
Where `<cluster-slug>` is the cluster's category + a short descriptor (e.g., `validation-users-endpoint`, `type-safety-auth-middleware`). Do not reference individual thread numbers in the commit message - they are volatile. Do not add AI-attribution trailers.
### 6. Post Inline Replies
For each thread in the cluster, post a single inline reply via `gh api`. Content:
- One sentence acknowledging the concern in the reviewer's own terms
- Reference to the commit SHA that fixed it
- Nothing else
Example:
```
Fixed in <sha>: input now rejects empty and negative values via the new `validateUserInput` helper.
```
Do not paraphrase the full fix across every thread; the commit is the source of truth. Do not apologize. Do not editorialize.
Use the GraphQL `addPullRequestReviewThreadReply` mutation:
```bash
gh api graphql -f query='
mutation($thread_id: ID!, $body: String!) {
addPullRequestReviewThreadReply(input: {
pullRequestReviewThreadId: $thread_id,
body: $body
}) {
comment { id url }
}
}' -F thread_id="<thread_id>" -F body="<body>"
```
### 7. Resolve Threads
For each thread where the reply represents a code change (not an acknowledgment), resolve via the `resolveReviewThread` mutation:
```bash
gh api graphql -f query='
mutation($thread_id: ID!) {
resolveReviewThread(input: { threadId: $thread_id }) {
thread { id isResolved }
}
}' -F thread_id="<thread_id>"
```
Do not resolve advisory threads - the caller has already filtered those out before dispatch. If you find one in your cluster anyway, return `DONE_WITH_CONCERNS` and let the orchestrator decide.
### 8. Return
Return one of the four structured statuses (see `modules/subagent-patterns/rules/subagent-patterns.md`):
- `DONE` with: `commit_sha`, `paths_modified`, `threads_replied`, `threads_resolved`, one-paragraph resolution note
- `DONE_WITH_CONCERNS` with: same + concerns section (e.g., "fix landed but unit test for edge case X is still missing")
- `BLOCKED` with: reason + which additional path/context would unblock
- `NEEDS_CONTEXT` with: what specific information is missing
## Guardrails
- **No scope expansion.** Paths not in the brief are off-limits. If you need one, return `BLOCKED`.
- **No thread resolution without a code change.** Replies are cheap; resolving a thread with no fix on record is dishonest.
- **No AI-attribution.** No `Co-Authored-By: Claude`, no "Generated with Claude Code" footers in commit messages or replies.
- **No multiple commits per cluster.** If your fix naturally wants two commits, that is a sign the cluster was mis-formed; return `DONE_WITH_CONCERNS` noting the split.
- **No destructive git operations.** No force push, no history rewriting. This agent lands one additional commit and pushes normally.
- **Security concerns escalate.** If the cluster is `security`-category and you find the fix has non-obvious implications (e.g., may affect other endpoints), return `DONE_WITH_CONCERNS` rather than landing it silently.
## When to Invoke
This agent is dispatched by `/resolve-pr-feedback` Phase 6. It is not intended to be run standalone - the cluster brief format is the input contract, and writing one by hand defeats the purpose of the orchestrator. If you want to fix a single PR comment manually, do that directly; if you want cluster-aware fan-out, run `/resolve-pr-feedback`.
script (1)
scripts/get-pr-comments
#!/usr/bin/env bash
# Fetch unresolved PR review threads via GitHub GraphQL.
#
# Usage:
# get-pr-comments <pr-number> [--repo owner/repo] [--state unresolved|all]
# [--format json|markdown]
#
# Defaults:
# --repo Current repo (via `gh repo view`)
# --state unresolved (drops threads where isResolved=true)
# --format json
#
# Output (json):
# {
# "pr": 123,
# "repo": "owner/repo",
# "fetched_at": "2026-04-16T14:32:00Z",
# "threads": [
# {
# "thread_id": "PRRT_...", # GraphQL node id (use to resolve)
# "is_resolved": false,
# "is_outdated": false,
# "path": "src/foo.ts",
# "line": 42,
# "diff_side": "RIGHT",
# "comments": [
# {
# "id": "IC_...",
# "author": "login",
# "body": "...",
# "created_at": "...",
# "url": "https://github.com/..."
# }
# ]
# }
# ]
# }
#
# Markdown format is human-readable; json is for machine consumption by the
# resolve-pr-feedback skill.
#
# Requires: gh (authenticated), jq.
set -euo pipefail
PR=""
REPO=""
STATE="unresolved"
FORMAT="json"
while [ $# -gt 0 ]; do
case "$1" in
--repo) REPO="$2"; shift 2 ;;
--state) STATE="$2"; shift 2 ;;
--format) FORMAT="$2"; shift 2 ;;
--help|-h)
sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
-*)
echo "Unknown flag: $1" >&2
exit 2
;;
*)
if [ -z "$PR" ]; then PR="$1"; else
echo "Unexpected argument: $1" >&2
exit 2
fi
shift
;;
esac
done
if [ -z "$PR" ]; then
echo "Usage: get-pr-comments <pr-number> [--repo owner/repo] [--state unresolved|all] [--format json|markdown]" >&2
exit 2
fi
# Resolve repo if not provided.
if [ -z "$REPO" ]; then
REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner)
fi
OWNER="${REPO%%/*}"
NAME="${REPO##*/}"
# GraphQL query. Paginates review threads up to 100 at a time; each thread's
# comments up to 50. Most PRs fit in one page - paginate threads if HasNextPage.
read -r -d '' QUERY <<'GRAPHQL' || true
query($owner: String!, $name: String!, $pr: Int!, $cursor: String) {
repository(owner: $owner, name: $name) {
pullRequest(number: $pr) {
reviewThreads(first: 100, after: $cursor) {
pageInfo { hasNextPage endCursor }
nodes {
id
isResolved
isOutdated
path
line
originalLine
diffSide
comments(first: 50) {
nodes {
id
author { login }
body
createdAt
url
}
}
}
}
}
}
}
GRAPHQL
# Accumulate threads across paginated requests.
ACCUM='[]'
CURSOR="null"
while :; do
if [ "$CURSOR" = "null" ]; then
RESPONSE=$(gh api graphql \
-f query="$QUERY" \
-F owner="$OWNER" -F name="$NAME" -F pr="$PR")
else
RESPONSE=$(gh api graphql \
-f query="$QUERY" \
-F owner="$OWNER" -F name="$NAME" -F pr="$PR" -F cursor="$CURSOR")
fi
PAGE_THREADS=$(echo "$RESPONSE" | jq '.data.repository.pullRequest.reviewThreads.nodes')
ACCUM=$(jq -n --argjson a "$ACCUM" --argjson b "$PAGE_THREADS" '$a + $b')
HAS_NEXT=$(echo "$RESPONSE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
if [ "$HAS_NEXT" != "true" ]; then break; fi
CURSOR=$(echo "$RESPONSE" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
done
# Filter by state.
if [ "$STATE" = "unresolved" ]; then
ACCUM=$(echo "$ACCUM" | jq '[.[] | select(.isResolved == false)]')
fi
# Shape output.
SHAPED=$(echo "$ACCUM" | jq '[.[] | {
thread_id: .id,
is_resolved: .isResolved,
is_outdated: .isOutdated,
path: .path,
line: (.line // .originalLine),
diff_side: .diffSide,
comments: [.comments.nodes[] | {
id: .id,
author: (.author.login // "ghost"),
body: .body,
created_at: .createdAt,
url: .url
}]
}]')
FETCHED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
if [ "$FORMAT" = "json" ]; then
jq -n \
--argjson threads "$SHAPED" \
--arg repo "$REPO" \
--argjson pr "$PR" \
--arg fetched_at "$FETCHED_AT" \
'{pr: $pr, repo: $repo, fetched_at: $fetched_at, threads: $threads}'
elif [ "$FORMAT" = "markdown" ]; then
echo "# PR #$PR review threads ($REPO)"
echo ""
echo "Fetched at $FETCHED_AT. State filter: $STATE."
echo ""
COUNT=$(echo "$SHAPED" | jq 'length')
echo "$COUNT threads."
echo ""
echo "$SHAPED" | jq -r '.[] |
"## " + (.path // "(general)") + ":" + ((.line|tostring) // "?") +
" (thread " + .thread_id + ", " +
(if .is_resolved then "resolved" else "unresolved" end) +
(if .is_outdated then ", outdated" else "" end) + ")\n\n" +
(.comments | map(
"- **@" + .author + "** (" + .created_at + "): " +
(.body | gsub("\n"; "\n ")) + "\n " + .url
) | join("\n\n")) + "\n"'
else
echo "Unknown format: $FORMAT (expected json|markdown)" >&2
exit 2
fi
doc (1)
skills/resolve-pr-feedback/references/cluster-categories.md
# Cluster Categories Eleven fixed concern categories used by `/resolve-pr-feedback` to classify review threads before grouping. Keep the set small and stable so a cluster over three runs is still a cluster; do not add ad-hoc categories. Each category has a short definition and sample phrases. When a thread plausibly fits more than one, pick the most specific. When it fits none, use `other` and flag the thread for manual review - do not invent a new category in-flight. | Category | Definition | Sample phrases a reviewer would use | |---|---|---| | `error-handling` | Missing, swallowed, or over-broad error handling. Includes unhandled promise rejections, bare `except`, silent fallbacks, and missing logging on failure paths. | "this can throw and we lose it", "catching Exception is too broad", "no retry", "silently returning null" | | `validation` | Missing or incorrect input validation at an API/function/form boundary. Includes type checks, range checks, enum guards, and sanitization gaps that are not security-critical. | "what if this is empty", "should reject negative N", "no length limit", "enum not narrowed" | | `type-safety` | TypeScript or equivalent static-type issues. Includes `any`, unsafe casts, missing generics, nullable-not-narrowed, and assertion-as-truth. | "remove the any", "cast is unsound", "this can be undefined here", "missing discriminated union" | | `testing` | Missing tests, weak coverage, wrong test style, or tests that pass for the wrong reason. Includes "add a test for X", "mock is testing the mock", "no failing case." | "no test for this path", "test should fail first", "flaky", "missing edge case" | | `naming` | Identifier quality - variable, function, file, or type names that obscure meaning. | "rename to", "this name is misleading", "shadows outer", "too generic" | | `style-consistency` | Repo/house-style divergence where a convention exists and this code diverges. Includes formatter output, import order, file layout, comment style. | "we use X here", "matches codebase pattern?", "reorder imports", "house style" | | `architecture` | Layering, coupling, module boundaries, and pattern-fit concerns that go beyond one symbol. Includes "wrong layer", "leak across boundary", "duplicate responsibility." | "belongs in service", "why is this in the controller", "couples these modules", "duplicates the resolver" | | `performance` | Algorithmic complexity, N+1 queries, avoidable re-renders, synchronous blocking on hot paths, oversized payloads. | "O(n^2) on the whole list", "unnecessary render", "blocks the event loop", "missing index" | | `security` | Authentication, authorization, input sanitization for untrusted data, secret handling, RLS gaps. Escalate immediately; never auto-apply fixes without explicit confirm. | "unsanitized", "RLS missing", "injection", "authorization check" | | `documentation` | Missing, stale, or incorrect comments, docstrings, READMEs, or inline rationale on non-obvious decisions. | "why does this", "document the contract", "doc says X but code does Y", "add example" | | `other` | Legitimately does not fit any of the above. Use sparingly. A thread tagged `other` is always surfaced for manual review; the orchestrator does not auto-fix `other` clusters. | (catch-all) | ## Spatial Proximity After category classification, the orchestrator groups threads by: 1. **Exact-file match** - two or more threads in the same category on the same `path`. 2. **Subtree match** - two or more in the same category under a shared directory prefix that is not the repo root (e.g., `src/auth/` but not `src/`). 3. **Cross-cutting** - same category across three or more disparate files. This signals a systemic issue and becomes a single finding, not N one-off fixes. A cluster is the intersection of a category and a proximity bucket. Emit one fix plan per cluster. ## Cluster Gate The cluster analysis phase only activates when: - **3 or more new unresolved threads** exist after triage, OR - A **cross-invocation signal** is present (e.g., `cluster:force` token in arguments, or a prior run's artifact in `.claude/pr-feedback/` indicates unfinished clusters). For 1-2 new threads, skip clustering and dispatch individual resolvers. Overhead of category + proximity analysis is not justified at that volume. ## Confidence and Autofix Routing For each cluster, the orchestrator tags an `autofix_class`: - `safe_auto` - unambiguous fix, mechanical, well-covered by tests. Examples: rename, extract constant, add type annotation, obvious missing validation with a single correct answer. - `gated_auto` - fix is clear but has a small judgment call. Dispatch with confirmation. - `manual` - taste question, architectural choice, or cross-cutting design decision. Batch for human review. - `advisory` - reviewer was flagging context, not requesting a change (e.g., "nit, up to you"). Reply acknowledging, do not modify code. `security` clusters are always at least `gated_auto` regardless of heuristic score. `other` clusters are always `manual`.