Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/lib/github/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ Reusable procedures for GitHub PR workflows.
| [lookup-pr](lookup-pr.md) | Find PR by number, branch, or list all | All |
| [detect-permission](detect-permission.md) | Check push access to PR | fix-pr |
| [commit-and-push](commit-and-push.md) | Squash commits, rebase, and push | github-pr, fix-pr |
| [fetch-comments](fetch-comments.md) | Get unresolved PR review comments | fix-pr |
| [reply-and-resolve](reply-and-resolve.md) | Reply to and resolve review threads | fix-pr |
| [fetch-comments](fetch-comments.md) | Get all PR feedback — inline threads, review bodies, conversation comments | fix-pr |
| [reply-and-resolve](reply-and-resolve.md) | Reply to each feedback surface; resolve review threads | fix-pr |
| [branch-naming](branch-naming.md) | Generate branch name from commit | github-pr |
| [common-issues](common-issues.md) | Troubleshooting reference | All |

Expand Down
103 changes: 91 additions & 12 deletions .claude/lib/github/fetch-comments.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
# Fetch Unresolved PR Comments
# Fetch PR Feedback (all three comment surfaces)

Fetches all unresolved review threads for a given PR.
A PR carries feedback on **three separate surfaces**. Fetching only the inline
review threads silently drops the other two — which is where "please rebase",
"this whole approach is wrong", and most bot summaries actually land.

| # | Surface | GraphQL field | Resolvable? | Reply with |
| - | ------- | ------------- | ----------- | ---------- |
| A | Inline review threads (anchored to a file/line) | `reviewThreads.nodes` | Yes (`resolveReviewThread`) | thread reply on the comment's `databaseId` |
| B | Review summary bodies (text submitted with APPROVE / REQUEST_CHANGES / COMMENT) | `reviews.nodes[].body` | No | a PR conversation comment |
| C | PR conversation comments (issue comments, incl. `@`-mentions and most bot posts) | `comments.nodes` | No | a PR conversation comment |

**Important:** Inline values directly into the GraphQL query string. Do NOT use `-f`/`-F` flags with GraphQL `$variables` — bash mangles the `$` signs. See [common-issues](./common-issues.md) for details.

## One query for all three

```bash
gh api graphql -f query='
query {
Expand All @@ -13,6 +23,7 @@ query {
nodes {
id
isResolved
isOutdated
comments(first: 50) {
nodes {
id
Expand All @@ -28,26 +39,94 @@ query {
}
}
}
reviews(first: 100) {
nodes {
id
databaseId
state
body
author { login }
submittedAt
}
}
comments(first: 100) {
nodes {
id
databaseId
body
author { login }
createdAt
}
}
}
}
}'
}' > /tmp/pr-feedback.json
```

Replace `OWNER`, `REPO`, `NUMBER` with actual values (e.g., `"hw-native-sys"`, `"simpler"`, `276`).

Use `--jq` to filter unresolved threads:
Split the surfaces out of the saved file (`jq` on a *file* avoids the `gh --jq`
`$`-quoting pitfalls):

```bash
gh api graphql -f query='...' \
--jq '[.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved == false)]'
P='.data.repository.pullRequest'

# A — unresolved inline threads
jq "[${P}.reviewThreads.nodes[] | select(.isResolved == false)]" /tmp/pr-feedback.json

# B — review bodies that actually say something (empty body = drive-by approve)
jq "[${P}.reviews.nodes[] | select(.body != \"\")]" /tmp/pr-feedback.json

# C — conversation comments
jq "${P}.comments.nodes" /tmp/pr-feedback.json
```

**Limits:**
**Limits / pagination:** each connection is capped at `first: 100` (and
`comments(first: 50)` per thread). Add `pageInfo { hasNextPage endCursor }` and
re-query with `after: "<endCursor>"` on any connection reporting
`hasNextPage: true`.

## Surfaces B and C have no resolved bit — track them yourself

Only surface A carries `isResolved`. A review body or conversation comment stays
in the API response forever, so re-fetching it every iteration makes the same
feedback look perpetually unaddressed. Keep a handled-ID ledger per PR:

```bash
LEDGER="/tmp/pr-<NUMBER>-handled.txt" # one comment/review node id per line
touch "$LEDGER"

# Filter out what was handled in an earlier iteration
jq -r "${P}.comments.nodes[].id" /tmp/pr-feedback.json | grep -Fxv -f "$LEDGER"

# After replying to (or consciously skipping) one, record it
echo "$NODE_ID" >> "$LEDGER"
```

`createdAt` / `submittedAt` is the fallback when the ledger is missing: anything
predating the current HEAD commit's push was, in practice, already seen.

## Filtering bot noise (surface C)

Bots post status and walkthrough comments that carry no request. Treat a
conversation comment as **non-actionable** when its body matches any of:

- `<!-- This is an auto-generated comment: summarize by coderabbit.ai -->`
- `<!-- walkthrough_start -->` without an `Actionable comments posted: <n>` where `n > 0`
- `Review skipped` / `Pre-merge checks` status blocks
- A pure CI/deploy status echo

Everything else on surface C is real feedback and must be classified, including
short human one-liners (`please rebase onto main`, `can you split this?`) that
never appear as an inline thread.

- `reviewThreads(first: 100)`: Fetches up to 100 threads.
- `comments(first: 50)`: Fetches up to 50 comments per thread.
A bot's *actionable* findings usually arrive as surface A threads, with B/C
holding only the summary — so **dedup B/C against A** before presenting: if a
review body just enumerates its own inline threads, address the threads and
treat the body as informational.

Output: JSON array of unresolved threads. Each thread has:
Output per surface:

- `id` — GraphQL node ID (for resolving via mutation)
- `comments.nodes[].databaseId` — REST API ID (for replying)
- A — `id` (GraphQL node ID, for `resolveReviewThread`), `comments.nodes[].databaseId` (REST ID, for threaded replies), `path`/`line` for context
- B — `state` (`CHANGES_REQUESTED` outranks `COMMENTED`), `body`, `author`
- C — `databaseId`, `body`, `author`, `createdAt`
41 changes: 36 additions & 5 deletions .claude/lib/github/reply-and-resolve.md
Original file line number Diff line number Diff line change
@@ -1,23 +1,29 @@
# Reply and Resolve

For each comment, reply then resolve the thread. Both steps are required.
How to answer each of the three feedback surfaces from
[fetch-comments](./fetch-comments.md). Every piece of feedback gets a reply;
only surface A can additionally be resolved.

## Step 1: Reply to the comment
## Surface A — inline review threads

Both steps are required.

### Step 1: Reply to the comment

```bash
gh api "repos/${PR_REPO_OWNER}/${PR_REPO_NAME}/pulls/${PR_NUMBER}/comments/${COMMENT_DATABASE_ID}/replies" \
-f body='Fixed in <commit-hash> — description'
-f body='Fixed — description'
```

**Important:** Always use single quotes for `-f body='...'` to avoid bash history expansion issues with `!` and other special characters.

**Response templates:**

- Fixed: `'Fixed in <commit-hash> — description'`
- Fixed: `'Fixed — description'`
- Skip: `'Current code follows .claude/rules/<file>'`
- Acknowledged: `'Acknowledged, thank you!'`

## Step 2: Resolve the thread
### Step 2: Resolve the thread

Use the thread `id` (the GraphQL node ID from [fetch-comments](./fetch-comments.md), NOT the `databaseId`):

Expand All @@ -34,3 +40,28 @@ mutation ResolveThread($threadId: ID!) {
`$THREAD_ID` is the `id` field from the `reviewThreads.nodes[]` returned by [fetch-comments](./fetch-comments.md).

**Both steps are mandatory.** Do not skip the resolve step.

## Surfaces B and C — review bodies and conversation comments

These have **no resolve mutation** — `resolveReviewThread` accepts only a
review-thread node ID and errors on a review or issue-comment ID. Answer them
with a PR conversation comment and record the ID in the ledger.

```bash
gh pr comment "$PR_NUMBER" --body "@${AUTHOR} Fixed — description"
echo "$NODE_ID" >> "/tmp/pr-${PR_NUMBER}-handled.txt"
```

- **Address the author by `@login`** — a conversation comment is not threaded, so without the mention it is unclear which feedback it answers. Quote the point being answered when several are outstanding.
- **Batch one reply per iteration.** Answering five conversation comments with five separate posts spams the PR; one comment with a short bullet per point is the norm.
- **Recording the ID is what makes the reply stick.** Skip it and the next iteration re-presents the same feedback as unaddressed.
- Feedback that is purely informational (bot walkthrough, "nice work") needs no reply — just record its ID.

Template for a batched reply:

```text
@reviewer Addressed:
- Rebased onto main.
- Null check added on the predicate path.
- Kept the existing enum style — follows .claude/rules/codestyle.md §2.
```
Loading
Loading