PR Feedback Resolver

workflow no always-loaded rules -- loads on demand updated 2026-06-22

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

  • pr
  • review
  • feedback
  • comments
  • cluster
  • github
  • graphql
  • resolver

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

  • gh CLI, authenticated (gh auth login)
  • jq on 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. manual clusters 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 /pr and /cpm in commands-core. For review orchestration beyond comments, see the ce-review module.

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.md so 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.

Manual, per file

Full control -- copy exactly the files you want from the sections below.

Files

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`.