hoopit

review-github-comments

Handle all review comments on a GitHub PR. Use when the user asks to fix or handle a PR or PR comments.

hoopit 0 Actualitzat fa 4 setm.
GitHub

Instal·la

npx skillscat add hoopit/skills/review-github-comments

Instal·la des del registre de SkillsCat.

SKILL.md

GitHub PR Review Comments Workflow

Caller-owned rounds

A calling skill (e.g. monitor-pr) may say it owns the round. It then manages the
whole lifecycle — worktree, agent-working label, push, the ledger and any re-review
trigger — and you do only the comment work: steps 2–4 plus the commit in step 5. Skip
steps 1a, 1b, 5a, and step 5's push. Your step 6 rows are what the caller writes into
the ledger.

Decide every thread before acting on any. When every one declines, the round is a
closing round (Closing the rounds in `../monitor-pr/LEDGER.md`):
reply, resolve and dismiss as usual, and commit nothing.

Prerequisites

  • The gh CLI must be authenticated.
  • You must be in the project repository (or a worktree of it).

Steps

1. Get PR metadata

Ask the user for the PR URL if not provided. Extract the <owner>/<repo> and <pr_number> from the URL.

Then echo the PR url once, so Claude Code renders its footer PR badge for the rest of
the session (it builds that badge by scanning command output for a PR url, and the
gh api calls below never print one):

gh api repos/<owner>/<repo>/pulls/<pr_number> --jq .html_url

Every call REST can serve goes over REST — gh api for reads, the label script for
labels. The GraphQL bucket is metered separately and far more tightly, and this skill
needs it for the thread query in step 2 and the resolves in step 4.

1a. Label the PR while you work

Mark the PR so humans see an agent is on it, and clear the label as your final action
before the summary (also on failure or early exit). Leave the PR's draft state alone: a
one-off pass does not own the review rounds — whoever asked for it does — so a ready PR
stays ready and a draft stays a draft. Only a watch that owns the rounds moves it
(monitor-pr).

bash "${CLAUDE_PLUGIN_ROOT}/skills/monitor-pr/scripts/pr-labels.sh" <owner>/<repo> <pr_number> +agent-working  # now
bash "${CLAUDE_PLUGIN_ROOT}/skills/monitor-pr/scripts/pr-labels.sh" <owner>/<repo> <pr_number> -agent-working  # when done

1b. Work in the PR's worktree if one exists

Fixes must land on the PR branch, so before touching any code find where that branch
is checked out:

gh api repos/<owner>/<repo>/pulls/<pr_number> --jq .head.ref
git worktree list
  • If a worktree already has the PR branch checked out, cd into it and do all
    edits, commits and pushes there — don't create a second one.
  • Otherwise, if the current checkout is on a different branch, add a worktree for
    the PR branch (git fetch origin <branch> && git worktree add <path> <branch>)
    rather than switching the user's current checkout.
  • Run git pull --ff-only in the chosen worktree so you're editing the latest
    PR head before applying fixes.

2. Fetch review comments and thread resolution status

Ask which threads are unresolved first, then fetch only their comments. A PR in its
third round carries every thread the first two settled, and reading them all costs tens of
thousands of tokens to rediscover that they are closed.

Thread resolution has no REST equivalent — a review thread's resolved flag appears in no
REST response — so it is worth spending the GraphQL call on. reviewThreads is paginated,
so walk every page: a fixed first: 50 silently drops unresolved threads on a busy PR:

THREADS=$(gh api graphql --paginate \
  -f query='
query($owner: String!, $repo: String!, $pr: Int!, $endCursor: String) {
  repository(owner: $owner, name: $repo) {
    pullRequest(number: $pr) {
      reviewThreads(first: 100, after: $endCursor) {
        pageInfo { hasNextPage endCursor }
        nodes {
          id
          isResolved
          comments(first: 1) {
            nodes {
              databaseId
            }
          }
        }
      }
    }
  }
}' -F owner=<owner> -F repo=<repo> -F pr=<pr_number>)

OPEN=$(jq -s '[.[].data.repository.pullRequest.reviewThreads.nodes[]
  | select(.isResolved | not)
  | {thread: .id, root: .comments.nodes[0].databaseId}]' <<<"$THREADS")

--paginate requires both the $endCursor variable and the pageInfo block to follow
the cursor. It prints one JSON document per page, which is why jq -s slurps them
before reading the node arrays — reading only the first document drops later pages.

Then the comments, projected to the six fields this skill uses and filtered to the open
threads. A raw comment object is ~5 KB — diff_hunk, the full user object, _links,
reactions and six URL variants — against ~600 bytes of substance, and nothing below
reads any of it:

gh api "repos/<owner>/<repo>/pulls/<pr_number>/comments?per_page=100" --paginate \
  --jq '.[] | {id, in_reply_to_id, user: .user.login, path, line: (.line // .original_line), body}' \
  | jq -s --argjson open "$OPEN" '[ .[] as $c
      | ($open[] | select(.root == ($c.in_reply_to_id // $c.id))) as $t
      | $c + {thread: $t.thread} ]'

A reply carries its thread root in in_reply_to_id, so the join reaches whole threads, not
just their first comment, and drops every comment belonging to a resolved one. Each surviving
comment arrives with the thread id step 4 resolves it by — no second cross-reference.

diff_hunk is deliberately absent: it is the code as the reviewer saw it, and step 1 left
the worktree at PR head, so read path and line in the checkout instead. An empty result
means there is nothing unresolved to process — go to step 5.

3. For each unresolved comment thread

  1. Read the comment — understand the reviewer's finding, suggestion, or question.
  2. Locate the relevant code — use the path and line fields to find the file and line(s) in the local checkout.
  3. Decide, and reply in the same move. The three decisions are the ledger's — see
    "Classifying an item" in `../monitor-pr/LEDGER.md` — whose
    challenge command reads GATE_SCRIPT, which a caller-owned round is given and a
    standalone run resolves itself as
    ${CLAUDE_PLUGIN_ROOT}/skills/review-gate/scripts/run_external_reviewers.sh — which
    also defines the source, severity, why and scope your step 6 row carries.
    A finding on text follows A finding on text in the same file:
    • applied — a real bug, improvement or style fix. Apply it, reply saying what
      changed, resolve the thread.
    • declined — incorrect, outdated, or not applicable here. Reply with the
      evidence that makes it wrong, resolve the thread, and carry what What a decline
      carries
      in the ledger reference asks: the reason in the code on a judgement, the
      challenge on a Critical/High.
    • open — you have findings but not the confidence to settle it. Reply with them
      and leave the thread unresolved for discussion.

Classify every thread as you go, whatever it decided — the row is what the ledger
filters, and a thread with no row is a thread that was dropped.

4. Reply to and resolve comment threads

Replying to a comment (works for both replies and disagreements):

gh api repos/<owner>/<repo>/pulls/<pr_number>/comments \
  --field in_reply_to=<comment_id> \
  --field body="Your reply text here."

Note: in_reply_to must be an integer. Do not use -f (string flag) — use --field so it is sent as a number.

Resolving a thread is the other call with no REST equivalent — there is no resolve endpoint. Use the thread field step 2 attached to the comment:

gh api graphql -f query='
mutation {
  resolveReviewThread(input: { threadId: "<PRRT_...node_id>" }) {
    thread { isResolved }
  }
}'

5. Commit and push

After processing all comments:

  1. Stage all modified files.
  2. Commit with a descriptive message, e.g.:
    fix: address PR review comments
    
    Resolved comments from coderabbitai on PR #<pr_number>.
  3. Push to the PR branch. Always push unless the user explicitly asked not to
    (e.g. "don't push", "leave it local") — a fix that only exists locally doesn't
    answer the reviewer, and the reply you already posted claims it's addressed.
    If pushing is declined, say so plainly in the summary.

5a. Trigger a fresh Codex review if nothing was pushed

If the run produced no push (no code changed), the reviewers have nothing new to look
at — start another review round yourself. A thread you left open is no reason to hold
it back; the reviewers work on the head, and the head is unchanged either way:

gh workflow run codex-review-manual.yml -f pr=<pr_number> --repo <owner>/<repo>

5b. Clear a stale CodeRabbit "changes requested" review

CodeRabbit often submits its findings as a REQUEST_CHANGES review. Resolving the threads
does not clear that state — the PR stays blocked on "changes requested" until the review
is dismissed. Check at the end of the round, whether or not it pushed:

gh api repos/<owner>/<repo>/pulls/<pr_number>/reviews \
  --jq '.[] | select(.state=="CHANGES_REQUESTED") | {id, user: .user.login}'

CodeRabbit reviews on every push, by itself — the push is the only trigger this run uses,
and a fresh COMMENTED review from it leaves the old CHANGES_REQUESTED standing regardless.
Dismissal is what clears the block.

Always dismiss every CHANGES_REQUESTED review from coderabbitai[bot] at the end of
the round — unconditionally, including when threads are still open:

gh api -X PUT repos/<owner>/<repo>/pulls/<pr_number>/reviews/<review_id>/dismissals \
  -f message="<what happened>" -f event=DISMISS

A bot's merge block is not a signal a human chose to raise, and an open thread is already
visible as an open thread — the CHANGES_REQUESTED state on top of it only blocks the PR
while adding nothing a reviewer can act on. CodeRabbit re-reviews on the next push regardless,
so dismissing loses nothing.

Make the message say which case it was, since that is the only record of the round's reasoning:

  • fixed — Addressed in <short-sha>; threads resolved.
  • declined — Findings declined, see thread replies; no code change.
  • still open — Dismissed; <n> thread(s) left open for discussion, see replies.

Then say in the summary that you dismissed it, and name any thread you left open — a dismissed
review must never be how an unresolved question disappears.

⚠️ Never dismiss a CHANGES_REQUESTED review from a human reviewer, or from any bot other
than CodeRabbit; only they (or a re-review) should clear it. Mention it in the summary as still
pending.

6. Report summary

One classified row per thread — every thread, not only the interesting ones. The
decision carries resolution: applied and declined are resolved, open is not.

# Source Sev Where Decision Why Scope
1 CodeRabbit Low api/urls.py:12 applied trailing slash missing
2 CodeRabbit Med api/serializers.py:41 declined already select_related('team'); test_roster_queries pins the count
3 @ola High api/models.py:12 applied owner must be nullable for legacy rows +2 files, +1 migration
4 codex High api/views.py:88 open lock placement has two valid shapes