Skip to content

improvement(review-pr): add thread management — dedup, outdated resolution, human reply handling#4847

Open
JBWatenbergScality wants to merge 2 commits intodevelopment/133.0from
improvement/review-pr-thread-management
Open

improvement(review-pr): add thread management — dedup, outdated resolution, human reply handling#4847
JBWatenbergScality wants to merge 2 commits intodevelopment/133.0from
improvement/review-pr-thread-management

Conversation

@JBWatenbergScality
Copy link
Copy Markdown
Contributor

Problem

Every push triggers a full re-review (workflow fires on opened + synchronize). Observed on this repo in PR #4820: 15 review submissions, 14 inline comments all from claude[bot], many duplicates on the same lines across pushes.

Three root causes:

  1. Duplicate comments — same issue re-posted at same path:line on every push
  2. Stale outdated threads — when the author fixes a flagged line, the thread stays visible but is never resolved
  3. Human replies ignored — dismissals, questions, and "fixed" claims are never read

Solution

One file changed: .claude/skills/review-pr/SKILL.md

New Step 2 inserted between "Fetch PR details" and "Read changed files". It runs before the diff analysis and manages all existing Claude Code threads.

Thread fetch (with pagination)

GraphQL query fetches all threads including isResolved, isOutdated, full comment history per thread, and pageInfo for pagination. Loops until hasNextPage is false — handles PRs with more than 100 review threads.

Per-thread classification and action

Class Condition Action
outdated isOutdated=true, isResolved=false Resolve via resolveReviewThread mutation
no_human_reply No non-bot replies Add to SKIP SET
author_asked_question Human asked a direct question Answer in thread via in_reply_to
justified Human provided technical reasoning Accept silently, add to SKIP SET
unjustified_dismissal Dismissed with no reasoning, issue still in code Reply in thread asking for justification
fix_claimed_but_code_unchanged Human said "fixed" but code unchanged Reply noting it persists

Security/correctness dismissals get stricter language. Minor issues may be accepted silently.

SKIP SET deduplication

The SKIP SET holds {path, line, body_excerpt}. Part A checks it before posting any new comment. Dedup uses body comparison — a genuinely different issue at the same path:line can still be posted.

Alignment

  • scality/agent-hub#26 — same logic in the central claude-code-review.yml workflow (affects all repos using that workflow)
  • scality/ring#6462 — same approach for the ring repo skill (pagination + body comparison originated there)

🤖 Generated with Claude Code

Adds Step 2 to the review skill covering all existing Claude Code threads
before the diff analysis runs. This fixes the three problems observed on
active PRs (e.g. #4820: 15 review submissions, 14 inline
comments, many duplicates from claude[bot]):

1. Duplicate comments: SKIP SET built from active threads prevents re-posting
   the same issue at the same path+line on every synchronize event.

2. Stale outdated threads: threads whose code has changed are auto-resolved
   via resolveReviewThread GraphQL mutation.

3. Human replies ignored: threads are classified by conversation history:
   - no_human_reply → skip set only
   - author_asked_question → Claude answers inline via in_reply_to
   - justified → accepted silently (substantive technical reasoning)
   - unjustified_dismissal → challenge reply asking for reasoning;
     stricter language for security/correctness issues
   - fix_claimed_but_code_unchanged → reply noting issue persists

Dedup uses {path, line, body_excerpt} comparison so a genuinely different
issue at the same path+line can still be raised. Pagination handles PRs
with more than 100 review threads.

Aligned with scality/agent-hub#26 (central workflow) and ring#6462.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
@JBWatenbergScality JBWatenbergScality requested a review from a team as a code owner March 27, 2026 14:27
@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented Mar 27, 2026

Hello jbwatenbergscality,

My role is to assist you with the merge of this
pull request. Please type @bert-e help to get information
on this process, or consult the user documentation.

Available options
name description privileged authored
/after_pull_request Wait for the given pull request id to be merged before continuing with the current one.
/bypass_author_approval Bypass the pull request author's approval
/bypass_build_status Bypass the build and test status
/bypass_commit_size Bypass the check on the size of the changeset TBA
/bypass_incompatible_branch Bypass the check on the source branch prefix
/bypass_jira_check Bypass the Jira issue check
/bypass_peer_approval Bypass the pull request peers' approval
/bypass_leader_approval Bypass the pull request leaders' approval
/approve Instruct Bert-E that the author has approved the pull request. ✍️
/create_pull_requests Allow the creation of integration pull requests.
/create_integration_branches Allow the creation of integration branches.
/no_octopus Prevent Wall-E from doing any octopus merge and use multiple consecutive merge instead
/unanimity Change review acceptance criteria from one reviewer at least to all reviewers
/wait Instruct Bert-E not to run until further notice.
Available commands
name description privileged
/help Print Bert-E's manual in the pull request.
/status Print Bert-E's current status in the pull request TBA
/clear Remove all comments from Bert-E from the history TBA
/retry Re-start a fresh build TBA
/build Re-start a fresh build TBA
/force_reset Delete integration branches & pull requests, and restart merge process from the beginning.
/reset Try to remove integration branches unless there are commits on them which do not appear on the source branch.

Status report is not available.

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented Mar 27, 2026

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • 2 peers

Peer approvals must include at least 1 approval from the following list:


- **`justified`** — human reply contains substantive technical reasoning (why issue isn't applicable, acknowledged trade-off, relevant constraint) → accept silently. Add to SKIP SET. Optionally resolve thread if still open.

- **`unjustified_dismissal`** — thread resolved by human with no reply, OR reply is dismissive with no technical reasoning (`"done"`, `"ok"`, `"wontfix"` with nothing substantive) AND flagged code still unchanged in new diff → reply in thread (do NOT post new top-level comment). Add to SKIP SET.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unjustified_dismissal category re-challenges humans who resolve or dismiss a bot comment with "done"/"wontfix". This contradicts the "collaborative not adversarial" principle on line 74. When a maintainer consciously dismisses a bot finding, pushing back creates friction and makes the tool argumentative. The "scale to severity" escape clause is vague enough that the LLM may still argue on minor issues in practice. Consider removing this category or limiting it strictly to security/correctness issues with a much higher bar.

— Claude Code

```
For security/correctness issues add: `"This is a security/correctness concern — a brief explanation helps reviewers and future contributors."` Scale to severity: minor issues may be accepted silently.

- **`fix_claimed_but_code_unchanged`** — human claimed fix but code unchanged in new diff → reply in thread. Add to SKIP SET.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This heuristic only checks whether code changed at the flagged location. Fixes are often applied elsewhere — e.g., fixing a null-check issue by changing the caller, or fixing a security issue by adding validation in a different file. The bot would incorrectly insist the issue persists when the human genuinely fixed it. Consider checking the broader diff (not just the flagged path+line) or softening the reply to acknowledge the fix may be elsewhere.

— Claude Code

@claude
Copy link
Copy Markdown

claude bot commented Mar 27, 2026

  • unjustified_dismissal category (line 63) contradicts the "collaborative not adversarial" principle — re-challenging human dismissals makes the bot argumentative. Consider removing or limiting strictly to security/correctness issues with a higher bar.
    - fix_claimed_but_code_unchanged (line 69) only checks the flagged location, but fixes are often applied elsewhere. Could produce false positives and friction.
    - Bot detection (line 52): author.login != "claude[bot]" treats other bots (dependabot, codecov) as human replies. Filter by checking for [bot] suffix instead.

    Review by Claude Code

ChengYanJin
ChengYanJin previously approved these changes Mar 27, 2026
@JBWatenbergScality
Copy link
Copy Markdown
Contributor Author

/approve

@bert-e
Copy link
Copy Markdown
Contributor

bert-e commented Mar 27, 2026

Waiting for approval

The following approvals are needed before I can proceed with the merge:

  • the author

  • 2 peers

Peer approvals must include at least 1 approval from the following list:

The following options are set: approve

…ndition

As written, the condition read: (resolved with no reply) OR (dismissive
reply AND code unchanged). This meant if someone fixed the code and
resolved the thread without commenting, Claude would still challenge them.

Fix: add parentheses so the code-unchanged guard applies to both branches:
(resolved with no reply OR dismissive reply) AND code unchanged in new diff

Caught by claude[bot] review on scality/agent-hub#26.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
```
For security/correctness issues add: `"This is a security/correctness concern — a brief explanation helps reviewers and future contributors."` Scale to severity: minor issues may be accepted silently.

- **`fix_claimed_but_code_unchanged`** — human claimed fix but code unchanged in new diff → reply in thread. Add to SKIP SET.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unjustified_dismissal category treats a human resolving a thread (with no reply) the same as a dismissive reply. Resolving a thread is a deliberate action — it means the author has handled it. Challenging humans who resolve threads contradicts the collaborative not adversarial instruction at line 81. Consider removing the thread resolved by human with no reply condition and only acting on explicitly dismissive replies on unresolved threads.

— Claude Code

All replies: collaborative not adversarial. 1-2 sentences. Name the specific construct.

3. **Read changed files** to understand the full context around each change (not just the diff hunks).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fix_claimed_but_code_unchanged checks if code is unchanged at this location, but fixes can be applied elsewhere — the author may have refactored, moved the code, or fixed it in a different layer. This will produce false positives where the bot incorrectly claims a fix was not applied. Consider also checking whether the flagged code still exists anywhere in the file (not just the same line), or defaulting to silent acceptance when uncertain.

— Claude Code

gh api -X POST -H "Accept: application/vnd.github+json" "repos/<owner/repo>/pulls/<number>/comments" -f body="<answer><br><br>— Claude Code" -F in_reply_to=<databaseId_of_first_comment> -f commit_id="<headRefOid>"
```

- **`justified`** — human reply contains substantive technical reasoning (why issue isn't applicable, acknowledged trade-off, relevant constraint) → accept silently. Add to SKIP SET. Optionally resolve thread if still open.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The classification logic has no fallback for ambiguous human replies that do not clearly match any category. Without a default, the bot may misclassify and take the wrong action. Add a fallback (e.g., treat ambiguous replies as justified and add to SKIP SET) to err on the side of being non-adversarial.

— Claude Code

@claude
Copy link
Copy Markdown

claude bot commented Mar 27, 2026

  • unjustified_dismissal treats resolved-by-human threads as needing justification, which is adversarial — resolving a thread is a deliberate action that should be respected
    - Remove the thread resolved by human with no reply condition; only act on dismissive replies on unresolved threads
    - fix_claimed_but_code_unchanged will produce false positives when fixes are applied elsewhere (refactored, moved, fixed in a different layer)
    - Check whether flagged code still exists anywhere in the file, not just the same line
    - Default to silent acceptance when uncertain
    - No fallback for ambiguous human replies that do not match any classification category
    - Add a default: treat ambiguous replies as justified and add to SKIP SET

    Review by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants