chore(ci): Claude Code review - keep settled threads out of the subagent - #11888
Conversation
The thread-hygiene subagent was reading everything the PR had ever accumulated. `gh list-review-threads` returned resolved threads, whose every rule — resolve a stale thread, skip a duplicate finding — is gated on the thread being unresolved, so they were bodies it read only to discard. `mcp__github__get_issue_comments` could pull back our own tracking comments, which `exclude_comments_by_actor` had deliberately dropped; each embeds a whole past review round. On #10604: | Source | Before | After | |---|---|---| | Review threads | 71 (2 pages, 117KB) | 15 (1 page, 13KB) | | Tracking comments | 29 reachable (~190KB) | unreachable | The alias now filters resolved threads out, pages at 100 (the GraphQL maximum, which the filter makes affordable) and inlines up to 25 comments per thread instead of just the opening one. Across ten sampled PRs no unresolved thread had a reply at all — replies land on threads that then get resolved — so the chains this carries are the ones worth reading: a thread still open because a human pushed back. Past 25, `show-review-thread <id> <endCursor>` continues from the last comment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removing it took away the PR's human top-level comments too, which are worth reading. The thread-hygiene subagent has no use for them either way, so the guidance moves to review-threads.md rather than the tool list: the tracking comment is not a review thread and says nothing about which thread is live. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @ovr's task in 1m 54s —— View job Review complete — 0 high, 0 medium, 3 lowFindings and checklist
The change is sound and the two files agree with each other: the doc's description of the listing output ( Low1. Stale argument name in the script header — 2. Wrong PR number fails as a jq error — 3. The worst case moved, and the doc does not bound it. Note on the PR descriptionThe last paragraph still says |
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
|
Claude finished @ovr's task in 4m 2s —— View job Approve — no findings. Verified the three aliases live; one prior finding of mine withdrawn.Review detail
Behaviour checks against the real APIEvery claim the PR body and
On Prior thread
Same for a bad thread id through Notes, nothing to change
|
A finding that turns out to be wrong left its thread open forever. The resolve rule only covered a concern the diff had addressed, and the "human pushed back" guard then caught the opposite case it was written for: a human refuting the finding itself, with the bot agreeing, still counted as pushback. That thread then comes back as unresolved every round — the context this branch just spent two commits trimming. Real instance on this PR: #11888 (discussion_r4017175210), withdrawn in the next round and still open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @ovr's task in 5m 7s —— View job Review: 1 medium, 1 nit — the aliases behave as described; one rule the run has no tool forFull review
Medium —
|
| check | result |
|---|---|
gh list-review-threads cube-js cube 10604 |
one page, hasNextPage: false, 15 nodes; keys exactly comments, id, isOutdated, line, originalLine, path — no isResolved |
same, with the returned endCursor as 4th arg |
{"nodes":[],"pageInfo":{"endCursor":null,"hasNextPage":false}} |
gh show-review-thread <id> |
full thread, --jq '.data.node' unwrapping correct |
gh show-review-thread <id> <comments.pageInfo.endCursor> |
nodes: [], hasNextPage: false — continues past the last comment, not a re-read |
base64 cursor with == padding through -F |
passed through as a String, not coerced |
So the filter, the del(.isResolved), the two cursor arguments and the 100/25 page sizes all do what review-threads.md says they do. The 71-of-which-15 ratio on #10604 is the claimed one.
Rules consistency
No dangling isResolved references survive the removal of the field — grep over the committed file finds only prose uses. The two new caveats are the ones that actually bite and both are correct: the top-level pageInfo is emitted before the jq filter, so a page can come back with zero nodes and still have a next one (review-threads.md:53-55), and the endCursor to disambiguate is the one beside nodes, not a thread's own (:46-48).
Review threads
gh list-review-threads cube-js cube 11888 returns {"nodes":[],"pageInfo":{...,"hasNextPage":false}} — no unresolved threads on this PR, so nothing to resolve and no finding to suppress as a duplicate. I skipped the subagent for that reason rather than spawning one to read an empty list.
I did not re-raise the --jq-on-null finding from the earlier round: @ovr's refutation on setup-claude-code-review.sh:41 is correct — gh exits 1 on the GraphQL errors entry before --jq is applied, so there is no silent empty reading to lose and (.nodes // [])[] would only mask a legible error.
Not run
No tests or linter — the change is a bash alias-installer and a markdown doc, neither covered by a package test suite, and yarn lint does not reach .claude/ or .github/. bash -n on the installer is implicitly clean since the job's setup step ran it and the aliases work.
Restores the withdrawal note the previous commit had to drop. The review run had no way to write into an existing thread: the inline-comment tool has no in_reply_to and opens a new one every time, which on a thread you are answering is a second unresolved entry rather than a reply. `gh reply-to-thread <thread-id> <body>` wraps `addPullRequestReviewThreadReply`, keeping raw `gh api graphql` denied. Verified against #11888 — the reply landed on the withdrawn finding's thread and left `isResolved` true. Resolution alone cannot say whether a finding was fixed or withdrawn, and the next round reads the thread rather than this file, so the withdrawal now gets stated there. The "still applies" rule keeps its silence, now by choice rather than for want of a tool. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The review run had no way to write into an existing review thread, so "say you are withdrawing it" was not executable: the inline-comment tool has no in_reply_to and opens a new thread every time, which on a thread you are answering is a second unresolved entry rather than a reply. `gh pr comment` is top-level, and raw `gh api graphql` stays denied. `gh reply-to-thread <thread-id> <body>` wraps `addPullRequestReviewThreadReply`. Verified against #11888 — the reply landed on the withdrawn finding's thread and left `isResolved` true. Resolution alone cannot say whether a finding was fixed or withdrawn, and the next round reads the thread rather than this file, so the withdrawal gets stated there. The "still applies" rule keeps its silence, now by choice rather than for want of a tool. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
a6f386f to
050a3c2
Compare
Empty commit to exercise the new `gh reply-to-thread` alias end to end — the local test posted as `ovr`, so the only way to see the CI identity is a real run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

The review-thread subagent was reading everything a PR had accumulated:
gh list-review-threadsreturned resolved threads, whose every rule (resolve a stale thread, skip a duplicate finding) is gated on the thread being unresolved, so they were bodies it read only to discard — on #10604, 71 threads over 2 pages and 117KB, where 15 threads and 13KB are actionable. The alias now filters them out, pages at 100 (the GraphQL maximum, which the filter makes affordable) and inlines up to 25 comments per thread instead of only the opening one, withshow-review-thread <id> <endCursor>continuing past 25; across ten sampled PRs no unresolved thread had a reply at all, so the chains this carries are the ones worth reading — a thread still open because a human pushed back.review-threads.mdalso tells the subagent not to go after the PR's top-level comments: our own tracking comments are there, each restating a whole past review round, and none of them say whether a thread is live.Verified live against #10604, #11881 and #11859: filtered listing, thread paging, comment-cursor continuation (0 nodes past the last comment, not a re-read), and the jq shape on a synthetic three-comment thread. The workflow file ends up unchanged.
Check List
🤖 Generated with Claude Code