Skip to content

[CXH-3051] Stop re-raising findings a human already closed - #156

Merged
FeliLucero1 merged 10 commits into
mainfrom
felipe/pr-review-stop-reposting-resolved
Oct 28, 2026
Merged

FeliLucero1 merged 10 commits into
mainfrom
felipe/pr-review-stop-reposting-resolved

Conversation

@FeliLucero1

@FeliLucero1 FeliLucero1 commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

A resolved review thread gets re-posted as a new inline thread on the next run, and again on the one after that. #147 ended with one finding spread across six threads, five of them redundant, out of 21 threads on the PR — roughly a quarter of its review conversation was the same accepted comment repeated.

Why it happens

The reviewer was not malfunctioning. It was following Step 3 of the base prompt literally, which gave it exactly two cases:

- `still present` — … If the existing thread is outdated (its line no longer
  matches the code), post a fresh inline comment at the current location; if
  the thread is still open and accurate, do not post a duplicate …

A resolved thread is neither. It falls through to "post a fresh comment".

And what makes it recur rather than happen once: an outdated thread stays outdated forever. Every later run sees the same permanently-outdated original, takes the same branch, and opens another thread — which then becomes another prior finding that can re-post in turn.

The tell that this is not ordinary anchor drift: one of the re-posts on #147 landed when action.yml had not changed at all — identical md5, three README-only commits in between — on a different line of the same untouched block. Each time, the bot explained itself accurately: "I'm re-posting only because the earlier thread is outdated."

flowchart TD
    T["Thread for finding X:<br/>resolved, and outdated because the code moved"]
    T --> S["Next run. Step 3 sees 'still present' and outdated,<br/>so it says post a fresh inline comment."]
    S --> N["A NEW thread for X.<br/>The original is still resolved AND still outdated."]
    N --> T2["The run after that sees both,<br/>and the original never stops being outdated."]
Loading

The fix

The rule now turns on whether a person has dealt with the thread, not on whether it is resolved.

Resolution alone does not say who did it: this action resolves its own outdated, reply-free threads on every run, so most resolved threads are its own doing rather than anyone's decision. Keying on that read a bot sweep as "someone closed this deliberately", while a thread a person had actually replied to — but not resolved — stayed outdated and unresolved forever and kept re-posting.

So collect_prior_findings reports human_engaged: a person wrote in the thread, or resolved it themselves. Both say "I have dealt with this", and neither should be answered by opening the same thread again. resolvedBy { login } is new in the query; everything else comes from comments it already fetched. The bot's own resolutions carry a BOT_LOGINS resolver, so they stay false and keep the path that re-anchors a finding whose thread was closed with no replacement posted.

Step 3's cases, in order:

human_engaged no new thread, whatever its state — report it in the summary
another open, accurate thread covers the same issue no duplicate
outdated and nothing open covers it post a fresh comment at the new location
otherwise (open and accurate) no duplicate

That second case is what stops accumulation by any route: prior findings are one entry per thread, not per issue, so after a re-post the old entry kept matching "post fresh" while the new one matched "do not duplicate", adding another thread every run.

The finding is never silenced — only its duplicate inline thread is. It is still audited against current code and still counted in the summary at its severity, so a resolved-but-unfixed blocker still blocks. Step 6 now points at these cases rather than restating them; two copies of one rule is how the original bug got here.

The prompt is also told about human_engaged and resolution_blocked, neither of which it previously saw — the second meaning the reviewer could not tell it was in the degraded mode where the token cannot resolve threads at all.

Tests

184, up from 175. New coverage: a reply counts as engagement, and still counts when a bot comment lands after it; a silent human resolution counts; a bot resolution does not, so the re-anchor path survives; an open thread's null resolver does not read as a person; the truncation boundary at 20-of-20 and 20-of-21, which is the shape comments(first: 20) can actually return; and the pre-mutation snapshot main() depends on, pinned because "report the state we just set" reads as an obvious cleanup and would silently turn every outdated finding into a summary line with the suite still green.

Prompt rules remain untested by construction — nothing in the repo asserts prompt content, and the functional change here lives in the prompt.

Scope

Deliberately not included: deduplicating the threads that already exist. This stops new ones accumulating; it does not retroactively collapse a PR that already has six.

🤖 Generated with Claude Code

A resolved review thread was re-posted as a new inline thread on the next
run, over and over. github-workflows#147 ended with ONE finding spread across
SIX threads, five of them redundant, out of 21 threads on the PR.

The reviewer was not malfunctioning; it was following Step 3 literally. The
`still present` branch gave it two cases — thread outdated, so post a fresh
comment, or thread open and accurate, so do not duplicate — and said nothing
about a thread that is resolved. A resolved thread falls through to "post a
fresh comment".

What makes it recur rather than happen once: an outdated thread stays outdated
forever. Every later run saw the same permanently-outdated original, took the
same branch, and opened another thread. Each new thread then became another
prior finding that could re-post in turn. One of the re-posts on #147 landed
when action.yml had not changed at all — identical md5, three README-only
commits in between — which is the tell that this is not anchor drift.

So Step 3 now has a third case: a resolved thread does not get a new one,
whether or not it is outdated. The finding is still audited against current
code and still counted in the summary as "Prior — still present" — only the
duplicate inline thread is suppressed. Re-opening a closed question belongs in
the summary, where it is said once, not in a thread the author already
answered.

collect_prior_findings now also reports has_author_reply, computed from the
comments the thread query already fetches, so no extra API calls. It is not
evidence about the code and is documented as such: it distinguishes a human
engaging with a finding from the bot talking to itself.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit 9efc349ba422

General PR Review: Stop re-raising findings a human already closed

Blocking Issues: 1 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: full
View review run

Review Summary

This PR changes the Step 3 prior-findings audit so a resolved thread, whether outdated or not, is still verified and counted but never re-posted as a new inline thread. It also adds a has_author_reply field to each entry from collect_prior_findings, computed from comments already fetched, plus 4 unit tests. I scanned the full PR diff (3 files) for security and correctness and checked the downstream consumer review-reset.py, which only needs excerpt and severity, so the new field is compatible. No repo-local criteria loaded, so only the base criteria applied. The fix is incomplete: Step 6 of the same prompt still gives the old re-post instruction.

Security Issues

None found.

Correctness Issues

  • New (high confidence) .github/actions/pr-review/prompts/base-pr-review.md:266-269 — Step 6 still says "DO post a fresh inline comment when the old thread is outdated", with no exception for resolved threads. For a resolved+outdated thread this contradicts the new Step 3 rule (lines 151-158). The PR's own diagnosis is that the reviewer follows this kind of instruction literally, so the re-post loop can keep happening.

Suggestions

  • New (medium confidence) .github/actions/pr-review/scripts/resolve-outdated-threads.py:209-212 — The query fetches only comments(first: 20), so a human reply after the 20th comment makes has_author_reply a false negative. is_resolvable already treats totalCount != len(nodes) as unknown; this field could do the same.
  • New .github/actions/pr-review/scripts/test_verdict_scaffolding.py:1829 — A shell-quoting artifact (prompt'"'"'s) was left in a test comment.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `.github/actions/pr-review/prompts/base-pr-review.md`:
- Around line 266-269: Step 6 still says "DO post a fresh inline comment when the old thread is outdated and no longer points at the code", which contradicts the new Step 3 rule that resolved threads (outdated or not) must never get a new inline thread. Change it to post a fresh inline comment only when the old thread is outdated AND not resolved, and say explicitly that resolved threads are reported as "Prior — still present" in the summary only.

## Suggestions

In `.github/actions/pr-review/scripts/resolve-outdated-threads.py`:
- Around line 209-212: The thread query fetches only `comments(first: 20)`, so `has_author_reply` can miss a human reply after the 20th comment. When `thread["comments"]["totalCount"] != len(comments)`, treat the value as unknown (e.g. set it to True or None) instead of returning a false negative, matching how `is_resolvable` handles truncation.

In `.github/actions/pr-review/scripts/test_verdict_scaffolding.py`:
- Around line 1829: Replace the shell-quoting artifact `prompt'"'"'s` with `prompt's` in the comment.

Comment thread .github/actions/pr-review/scripts/resolve-outdated-threads.py Outdated
Comment thread .github/actions/pr-review/scripts/test_verdict_scaffolding.py Outdated
Comment thread .github/actions/pr-review/prompts/base-pr-review.md Outdated
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit 71b5be449e83

General PR Review: Stop re-raising findings a human already closed

Blocking Issues: 1 | Suggestions: 2 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: full
View review run

Review Summary

This PR changes the Step 3 prior-findings audit so a resolved thread, whether outdated or not, is still verified and counted but never re-posted as a new inline thread. It also adds a has_author_reply field to each entry from collect_prior_findings, computed from comments already fetched, plus 4 unit tests. I scanned the full PR diff (3 files) for security and correctness and checked the downstream consumer review-reset.py, which only needs excerpt and severity, so the new field is compatible. No repo-local criteria loaded, so only the base criteria applied. The fix is incomplete: Step 6 of the same prompt still gives the old re-post instruction.

Security Issues

None found.

Correctness Issues

  • New (high confidence) .github/actions/pr-review/prompts/base-pr-review.md:266-269 — Step 6 still says "DO post a fresh inline comment when the old thread is outdated", with no exception for resolved threads. For a resolved+outdated thread this contradicts the new Step 3 rule (lines 151-158). The PR's own diagnosis is that the reviewer follows this kind of instruction literally, so the re-post loop can keep happening.

Suggestions

  • New (medium confidence) .github/actions/pr-review/scripts/resolve-outdated-threads.py:209-212 — The query fetches only comments(first: 20), so a human reply after the 20th comment makes has_author_reply a false negative. is_resolvable already treats totalCount != len(nodes) as unknown; this field could do the same.
  • New .github/actions/pr-review/scripts/test_verdict_scaffolding.py:1829 — A shell-quoting artifact (prompt'"'"'s) was left in a test comment.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `.github/actions/pr-review/prompts/base-pr-review.md`:
- Around line 266-269: Step 6 still says "DO post a fresh inline comment when the old thread is outdated and no longer points at the code", which contradicts the new Step 3 rule that resolved threads (outdated or not) must never get a new inline thread. Change it to post a fresh inline comment only when the old thread is outdated AND not resolved, and say explicitly that resolved threads are reported as "Prior — still present" in the summary only.

## Suggestions

In `.github/actions/pr-review/scripts/resolve-outdated-threads.py`:
- Around line 209-212: The thread query fetches only `comments(first: 20)`, so `has_author_reply` can miss a human reply after the 20th comment. When `thread["comments"]["totalCount"] != len(comments)`, treat the value as unknown (e.g. set it to True or None) instead of returning a false negative, matching how `is_resolvable` handles truncation.

In `.github/actions/pr-review/scripts/test_verdict_scaffolding.py`:
- Around line 1829: Replace the shell-quoting artifact `prompt'"'"'s` with `prompt's` in the comment.

Reviewed commit: 9efc349ba422

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking issues found — see the full review report

…swered

Step 6 restates the prior-findings rule for the reviewer's final pass, and it
still said to post a fresh inline comment whenever the old thread is outdated,
with no exception for resolved ones. That is the instruction this branch set
out to remove, left intact two hundred lines below the place it was removed —
so a resolved and outdated thread could still be re-raised, and the fix was
incomplete. Both steps now say the same thing.

has_author_reply also read a truncated thread as unanswered. The query takes
comments(first: 20), so on a longer thread a human reply can sit past the cut,
and False there is the one direction that makes the audit MORE willing to
re-raise a finding. Truncation now counts as replied-to, on the grounds that a
thread that long has been engaged with. is_resolvable treats the same
truncation as a reason not to act; both are the same instinct — do not act on
what was not read.

Also removes a shell-quoting artifact (`prompt'"'"'s`) that leaked out of the
heredoc used to write the test comment. Swept the action directory; it was the
only one.

181 tests, up from 179.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit 71b5be449e83

General PR Review: Stop re-raising findings a human already closed

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: incremental since 9efc349
View review run

Review Summary

The PR changes the reviewer prompt so that a prior finding on a resolved thread is still audited and counted as "Prior — still present", but no new inline thread is opened for it. Only outdated threads that are still unresolved get a fresh comment. It also adds has_author_reply to collect_prior_findings. The new commits do three things: Step 6 now follows the Step 3 rule, a thread truncated by the 20-comment fetch counts as replied-to (with two new tests), and a shell-quoting artifact in a test comment is fixed. I scanned the full PR diff for security and correctness. Threads are captured before auto-resolution runs (resolve-outdated-threads.py:256,309), so thread_resolved shows the state at the start of the run. No repo-local criteria loaded, so only the base criteria applied.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New (high confidence) .github/actions/pr-review/scripts/resolve-outdated-threads.py:213 — The new comment refers to is_resolvable above, but no function by that name exists. The matching truncation check is in should_resolve (line 139).

Resolved prior findings

  • Step 6 contradicted the new Step 3 rule for resolved+outdated threads. Fixed: base-pr-review.md:266-271 now says "never open a new thread for one whose thread is resolved" and only re-posts when a thread is "outdated, unresolved".
  • has_author_reply could be a false negative past comments(first: 20). Fixed: resolve-outdated-threads.py:216-222 treats totalCount != len(comments) as replied-to, and test_truncated_thread_counts_as_replied_to covers it.
  • The shell-quoting artifact prompt'"'"'s in a test comment. Fixed: test_verdict_scaffolding.py:1829 now reads prompt's.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `.github/actions/pr-review/scripts/resolve-outdated-threads.py`:
- Around line 213: The comment says "(is_resolvable above treats the same truncation ...)", but no function is named is_resolvable. Change it to "should_resolve", the function at line 130 that checks totalCount != len(comments).

Comment thread .github/actions/pr-review/scripts/resolve-outdated-threads.py Outdated
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit b111dcd98c63

General PR Review: Stop re-raising findings a human already closed

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: incremental since 9efc349
View review run

Review Summary

The PR changes the reviewer prompt so that a prior finding on a resolved thread is still audited and counted as "Prior — still present", but no new inline thread is opened for it. Only outdated threads that are still unresolved get a fresh comment. It also adds has_author_reply to collect_prior_findings. The new commits do three things: Step 6 now follows the Step 3 rule, a thread truncated by the 20-comment fetch counts as replied-to (with two new tests), and a shell-quoting artifact in a test comment is fixed. I scanned the full PR diff for security and correctness. Threads are captured before auto-resolution runs (resolve-outdated-threads.py:256,309), so thread_resolved shows the state at the start of the run. No repo-local criteria loaded, so only the base criteria applied.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New (high confidence) .github/actions/pr-review/scripts/resolve-outdated-threads.py:213 — The new comment refers to is_resolvable above, but no function by that name exists. The matching truncation check is in should_resolve (line 139).

Resolved prior findings

  • Step 6 contradicted the new Step 3 rule for resolved+outdated threads. Fixed: base-pr-review.md:266-271 now says "never open a new thread for one whose thread is resolved" and only re-posts when a thread is "outdated, unresolved".
  • has_author_reply could be a false negative past comments(first: 20). Fixed: resolve-outdated-threads.py:216-222 treats totalCount != len(comments) as replied-to, and test_truncated_thread_counts_as_replied_to covers it.
  • The shell-quoting artifact prompt'"'"'s in a test comment. Fixed: test_verdict_scaffolding.py:1829 now reads prompt's.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `.github/actions/pr-review/scripts/resolve-outdated-threads.py`:
- Around line 213: The comment says "(is_resolvable above treats the same truncation ...)", but no function is named is_resolvable. Change it to "should_resolve", the function at line 130 that checks totalCount != len(comments).

Reviewed commit: 71b5be449e83

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No blocking issues found — see the full review report

@github-actions
github-actions Bot dismissed their stale review October 21, 2026 07:53

Superseded by a completed review with no blocking issues; current audit evidence persisted in CI.

@FeliLucero1 FeliLucero1 self-assigned this Oct 21, 2026
…lved

The first pass fixed one path into the loop and left two open, because it
keyed on the wrong signal.

should_resolve auto-resolves every outdated, reply-free bot thread on each
run, so thread_resolved is usually this action's own doing rather than a
human decision — "Someone closed it deliberately" was false for the majority
case. Meanwhile a thread a person REPLIED to but did not resolve stays
outdated and unresolved forever, which is the branch that re-posts, so the
most common human response to a finding kept looping. And where the token is
denied resolveReviewThread — which this script documents as common — nothing
is ever resolved and the rule could not fire at all.

has_author_reply separates those populations exactly, and until now nothing
branched on it. It does now: a thread a person has written in never gets a new
one, whatever its resolution state; a thread that is only this bot talking to
itself keeps the old behaviour, which is also what recovers a finding whose
thread was auto-resolved before a replacement was posted.

Added the case that stops accumulation regardless of path: do not open a new
thread when another open, accurate thread already covers the same issue.
prior_findings is one entry per thread, not per issue, so after any re-post
the old entry kept matching the post-fresh branch while the new one matched
do-not-duplicate — each run adding another. Also scoped the whole rule to the
flagged location, since a distinct occurrence elsewhere is a New finding.

Step 6 now points at Step 3's cases instead of restating them, keeping only
the invariant it genuinely adds: every still-present issue is counted
whatever happens to its thread. Two copies of one rule is how this bug got
here.

The prompt now also sees has_author_reply and resolution_blocked, neither of
which it was told about — the second meaning the reviewer could not tell it
was in the degraded mode where outdated threads never get cleared.

Script: BOT_LOGINS comes from _review_state like everywhere else rather than a
third private copy, where drift would be fail-open (an unlisted bot login
reads as a human reply and suppresses re-raising). The truncation guard is
`>` not `!=`, and its comment no longer claims True is the conservative
direction — it buys silence, which for a reviewer is the unsafe one; it is
chosen anyway and now says why. The cross-reference named is_resolvable, which
does not exist; the function is should_resolve.

Tests: the truncation fixture used one node with totalCount 25, a shape
comments(first: 20) cannot return, and neither boundary was covered — now 20
of 20 and 20 of 21. And the pre-mutation snapshot that main() depends on to
re-anchor outdated findings is documented and pinned, because "report the
state we just set" reads as an obvious cleanup and would silently turn every
outdated finding into a summary line with the suite still green.

181 tests, up from 179.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit b111dcd98c63

General PR Review: Stop re-raising findings a human already closed

Blocking Issues: 1 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: incremental since 71b5be4
View review run

Review Summary

The new commits change what Step 3 branches on. Before, it was thread_resolved ("resolved → never re-post"). Now it is an ordered four-case list keyed on has_author_reply: a person replied → no new thread; another open thread covers it → no new thread; outdated with nothing open → re-post; otherwise → no new thread. Step 6 now points back to Step 3 instead of restating the rules. In resolve-outdated-threads.py, BOT_LOGINS now comes from _review_state. The script runs by path (action.yml:92), so the import resolves. The truncation check changed from != to >, and a comment plus a test now pin the order in main() that reports thread state from before this run resolved anything. I scanned the full PR diff (3 files) for security and correctness and found no security issues. No repo-local criteria loaded, so only the base criteria were applied.

Security Issues

None found.

Correctness Issues

  • New (high confidence) .github/actions/pr-review/prompts/base-pr-review.md:158: when a person resolves a thread without replying, the finding gets re-posted, possibly forever. Take an outdated thread a person resolved silently (has_author_reply false). It skips cases 1 and 2 and hits case 3, so a new thread is posted. If the person resolves that one too, nothing open covers the issue. The original thread stays outdated forever, so case 3 fires again on every later run. That is the recurrence the PR describes for [CXH-2780] Let CI submit an approval, not only a block (verdict_mode) #147. The PR body still says "a resolved thread does not get a new one, whether or not it is outdated", which the current prompt no longer does. Threads resolved by should_resolve are handled correctly. The gap is that the inputs cannot tell a silent human resolution from an automatic one.

Suggestions

None.

Resolved prior findings

  • The comment referred to is_resolvable above. Fixed: resolve-outdated-threads.py:225 now says should_resolve above, which matches the function at line 134.
  • Step 6 contradicted Step 3 for resolved+outdated threads. Obsolete: Step 6 (base-pr-review.md:281-285) no longer restates any thread rules and defers to the Step 3 cases. The resolved-thread rule it conflicted with was replaced by the has_author_reply ordering, which is where the new correctness issue above comes from.
  • Shell-quoting artifact prompt'"'"'s in a test comment. Fixed: test_verdict_scaffolding.py:1842 reads prompt's, and no artifact remains in the file.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `.github/actions/pr-review/prompts/base-pr-review.md`:
- Around line 151-166: A thread a person resolved without replying (has_author_reply false, resolved, outdated) falls into case 3 ("outdated and nothing open covers the issue"), so it is re-posted. If the person resolves the re-post, the original outdated thread triggers case 3 again on every later run, which brings back the loop this PR is meant to remove. Fix: in `.github/actions/pr-review/scripts/resolve-outdated-threads.py`, add `resolvedBy { login }` to LIST_THREADS_QUERY and emit a field such as `resolved_by_human` (true when isResolved and resolvedBy.login is not in BOT_LOGINS) from collect_prior_findings. Then add a Step 3 case before case 3: "resolved by a person (resolved_by_human true) → do not open a new thread; report as Prior — still present". Add tests for the new field, and update the PR description to match the final behavior.

Comment thread .github/actions/pr-review/prompts/base-pr-review.md Outdated
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit 2e9a39a189d4

General PR Review: Stop re-raising findings a human already closed

Blocking Issues: 1 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: incremental since 71b5be4
View review run

Review Summary

The new commits change what Step 3 branches on. Before, it was thread_resolved ("resolved → never re-post"). Now it is an ordered four-case list keyed on has_author_reply: a person replied → no new thread; another open thread covers it → no new thread; outdated with nothing open → re-post; otherwise → no new thread. Step 6 now points back to Step 3 instead of restating the rules. In resolve-outdated-threads.py, BOT_LOGINS now comes from _review_state. The script runs by path (action.yml:92), so the import resolves. The truncation check changed from != to >, and a comment plus a test now pin the order in main() that reports thread state from before this run resolved anything. I scanned the full PR diff (3 files) for security and correctness and found no security issues. No repo-local criteria loaded, so only the base criteria were applied.

Security Issues

None found.

Correctness Issues

  • New (high confidence) .github/actions/pr-review/prompts/base-pr-review.md:158: when a person resolves a thread without replying, the finding gets re-posted, possibly forever. Take an outdated thread a person resolved silently (has_author_reply false). It skips cases 1 and 2 and hits case 3, so a new thread is posted. If the person resolves that one too, nothing open covers the issue. The original thread stays outdated forever, so case 3 fires again on every later run. That is the recurrence the PR describes for [CXH-2780] Let CI submit an approval, not only a block (verdict_mode) #147. The PR body still says "a resolved thread does not get a new one, whether or not it is outdated", which the current prompt no longer does. Threads resolved by should_resolve are handled correctly. The gap is that the inputs cannot tell a silent human resolution from an automatic one.

Suggestions

None.

Resolved prior findings

  • The comment referred to is_resolvable above. Fixed: resolve-outdated-threads.py:225 now says should_resolve above, which matches the function at line 134.
  • Step 6 contradicted Step 3 for resolved+outdated threads. Obsolete: Step 6 (base-pr-review.md:281-285) no longer restates any thread rules and defers to the Step 3 cases. The resolved-thread rule it conflicted with was replaced by the has_author_reply ordering, which is where the new correctness issue above comes from.
  • Shell-quoting artifact prompt'"'"'s in a test comment. Fixed: test_verdict_scaffolding.py:1842 reads prompt's, and no artifact remains in the file.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Correctness Issues

In `.github/actions/pr-review/prompts/base-pr-review.md`:
- Around line 151-166: A thread a person resolved without replying (has_author_reply false, resolved, outdated) falls into case 3 ("outdated and nothing open covers the issue"), so it is re-posted. If the person resolves the re-post, the original outdated thread triggers case 3 again on every later run, which brings back the loop this PR is meant to remove. Fix: in `.github/actions/pr-review/scripts/resolve-outdated-threads.py`, add `resolvedBy { login }` to LIST_THREADS_QUERY and emit a field such as `resolved_by_human` (true when isResolved and resolvedBy.login is not in BOT_LOGINS) from collect_prior_findings. Then add a Step 3 case before case 3: "resolved by a person (resolved_by_human true) → do not open a new thread; report as Prior — still present". Add tests for the new field, and update the PR description to match the final behavior.

Reviewed commit: b111dcd98c63

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Blocking issues found — see the full review report

Third time this fix has been too narrow, each time by reading the signal I had
already looked at rather than the one that decides.

has_author_reply only saw replies. A person who resolves a thread without
writing anything — far more common than replying — left it false, so a
resolved, outdated, reply-free thread fell through to the post-fresh branch.
Resolve the re-post too and nothing open covers the issue, so the original
re-posts again next run, and the one after. That is the #147 loop by another
route, and it contradicted this PR's own description.

The query now asks for resolvedBy, and the field becomes human_engaged: a
person wrote in the thread, or resolved it. Both are ways of saying "I have
dealt with this", and neither should be answered by opening the same thread
again.

resolvedBy is also what separates the two populations the earlier version
conflated. should_resolve closes this reviewer's own reply-free threads on
every run, and those carry a BOT_LOGINS resolver, so they stay false and keep
the re-anchor path that recovers a finding whose thread was closed with no
replacement posted. Verified against the live API before relying on it:
resolvedBy returns the resolving login and is null on an open thread.

Renamed because "author" was wrong twice over — it was never limited to the PR
author, and it is no longer limited to replies.

184 tests, up from 181: silent human resolution engages, bot resolution does
not, and an open thread's null resolver does not read as a person.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit 2e9a39a189d4

General PR Review: Stop re-raising findings a human already closed

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: incremental since b111dcd
View review run

Review Summary

The new commit renames the prior-findings field has_author_reply to human_engaged. It also widens the field: it is now true when a person replies, when the thread is truncated, or when resolvedBy.login (newly added to LIST_THREADS_QUERY) is a non-bot, non-empty login. Step 3 now branches on human_engaged, so a thread someone resolved without replying no longer goes down the outdated re-post path. Threads that should_resolve resolved still use the re-anchor path: those have a github-actions[bot] resolver or a null one, and both callers pass secrets.GITHUB_TOKEN. Three new tests cover the change: a person resolving, the bot resolving (both bot logins), and a null resolver on an open thread.

I scanned the full PR diff (3 files, nothing dropped or truncated) for security and correctness problems and found none. No repo-local criteria loaded, so only the base criteria were applied.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New (medium confidence) PR description: the description no longer matches the code. It still describes has_author_reply and says "179, up from 175" tests. The final behavior is different: human_engaged also counts a person clicking Resolve, and there are three more tests (test_silent_human_resolution_counts_as_engagement, test_bot_resolution_is_not_engagement, test_unresolved_thread_has_no_resolver). Reviewers who read the description will get the wrong idea of what the field means. A smaller point: the test names test_bot_only_thread_has_no_author_reply and test_author_reply_survives_a_later_bot_comment (test_verdict_scaffolding.py) still use the old term.

Resolved prior findings

  • A thread a person resolved silently was re-posted through case 3 (report finding base-pr-review.md:158 plus the open thread at :160). Fixed: resolve-outdated-threads.py:38 fetches resolvedBy { login }, and :242-247 sets human_engaged when the resolver is a person. Step 3's first case (base-pr-review.md:152-156) then stops a new thread from being opened. Tests are at test_verdict_scaffolding.py:1869-1895.
  • Step 6 contradicted Step 3 for resolved+outdated threads. Obsolete: Step 6 (base-pr-review.md:286-290) now just defers to the Step 3 cases and repeats no thread rules.
  • The stale is_resolvable above cross-reference. Fixed: the comments at resolve-outdated-threads.py:197 and :231-232 say should_resolve above, and is_resolvable no longer appears anywhere.
  • The shell-quoting artifact in a test comment. Fixed: no '"'"' sequences remain in test_verdict_scaffolding.py.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In the PR description (and `.github/actions/pr-review/scripts/test_verdict_scaffolding.py`):
- Update the PR body so it describes the `human_engaged` field instead of `has_author_reply`. Say that a person clicking Resolve (detected via `resolvedBy.login` not in BOT_LOGINS) also counts as engagement, and give the current test count. Optionally rename `test_bot_only_thread_has_no_author_reply` and `test_author_reply_survives_a_later_bot_comment` so they use the "engagement" wording, to match `test_reply_counts_as_engagement`.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit b677541b8458

General PR Review: Stop re-raising findings a human already closed

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: incremental since b111dcd
View review run

Review Summary

The new commit renames the prior-findings field has_author_reply to human_engaged. It also widens the field: it is now true when a person replies, when the thread is truncated, or when resolvedBy.login (newly added to LIST_THREADS_QUERY) is a non-bot, non-empty login. Step 3 now branches on human_engaged, so a thread someone resolved without replying no longer goes down the outdated re-post path. Threads that should_resolve resolved still use the re-anchor path: those have a github-actions[bot] resolver or a null one, and both callers pass secrets.GITHUB_TOKEN. Three new tests cover the change: a person resolving, the bot resolving (both bot logins), and a null resolver on an open thread.

I scanned the full PR diff (3 files, nothing dropped or truncated) for security and correctness problems and found none. No repo-local criteria loaded, so only the base criteria were applied.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New (medium confidence) PR description: the description no longer matches the code. It still describes has_author_reply and says "179, up from 175" tests. The final behavior is different: human_engaged also counts a person clicking Resolve, and there are three more tests (test_silent_human_resolution_counts_as_engagement, test_bot_resolution_is_not_engagement, test_unresolved_thread_has_no_resolver). Reviewers who read the description will get the wrong idea of what the field means. A smaller point: the test names test_bot_only_thread_has_no_author_reply and test_author_reply_survives_a_later_bot_comment (test_verdict_scaffolding.py) still use the old term.

Resolved prior findings

  • A thread a person resolved silently was re-posted through case 3 (report finding base-pr-review.md:158 plus the open thread at :160). Fixed: resolve-outdated-threads.py:38 fetches resolvedBy { login }, and :242-247 sets human_engaged when the resolver is a person. Step 3's first case (base-pr-review.md:152-156) then stops a new thread from being opened. Tests are at test_verdict_scaffolding.py:1869-1895.
  • Step 6 contradicted Step 3 for resolved+outdated threads. Obsolete: Step 6 (base-pr-review.md:286-290) now just defers to the Step 3 cases and repeats no thread rules.
  • The stale is_resolvable above cross-reference. Fixed: the comments at resolve-outdated-threads.py:197 and :231-232 say should_resolve above, and is_resolvable no longer appears anywhere.
  • The shell-quoting artifact in a test comment. Fixed: no '"'"' sequences remain in test_verdict_scaffolding.py.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In the PR description (and `.github/actions/pr-review/scripts/test_verdict_scaffolding.py`):
- Update the PR body so it describes the `human_engaged` field instead of `has_author_reply`. Say that a person clicking Resolve (detected via `resolvedBy.login` not in BOT_LOGINS) also counts as engagement, and give the current test count. Optionally rename `test_bot_only_thread_has_no_author_reply` and `test_author_reply_survives_a_later_bot_comment` so they use the "engagement" wording, to match `test_reply_counts_as_engagement`.

Reviewed commit: 2e9a39a189d4

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No blocking issues found — see the full review report

@github-actions
github-actions Bot dismissed their stale review October 21, 2026 09:13

Superseded by a completed review with no blocking issues; current audit evidence persisted in CI.

@linear-code

linear-code Bot commented Oct 21, 2026

Copy link
Copy Markdown

CXH-3051

Comment thread .github/actions/pr-review/prompts/base-pr-review.md Outdated
Comment thread .github/actions/pr-review/scripts/resolve-outdated-threads.py Outdated
The covering-thread case required another OPEN thread, which leaves the loop
running through the bot's own superseded originals:

  1. Code moves, thread A goes outdated. should_resolve closes A in the same
     run where the review re-anchors the finding as thread B.
  2. A person resolves B without replying.
  3. Next run: B is correctly skipped, human_engaged is true. But A is still
     in prior_findings — human_engaged false, because the bot resolved it,
     and outdated forever. Nothing OPEN covers the issue any more, since B is
     resolved. So A matches the outdated case and posts C.
  4. The person resolves C. Same again next run, and the run after.

Widened to "open and accurate, OR human_engaged" — one person dealing with any
thread for an issue now settles every entry of that issue, including the bot's
own superseded ones, which stay in prior_findings for the life of the PR.

Also removed commentary that was about this PR's own iterations rather than
about the code. It reads oddly once merged, and the history belongs in the
description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit 9f395db1d3ae

General PR Review: [CXH-3051] Stop re-raising findings a human already closed

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: incremental since b677541
View review run

Review Summary

Overall, the PR changes collect_prior_findings to report human_engaged for each prior finding. The value is true when a person replied in the thread, a person resolved it (taken from the new resolvedBy field), or the thread has more comments than the 20 fetched. Step 3 of the prompt now uses this value to decide whether to open a new inline thread. A still-present finding is always counted in the summary, whatever happens to its thread.

The new commit widens the Step 3 "duplicate" case. Another thread for the same issue now covers it when that thread is open and accurate, or when a person engaged with it. Older bot-resolved threads for that issue can then no longer reach the "outdated, post fresh" case. The commit also tightens the wording of a docstring and a comment.

I scanned the full PR diff for security and correctness issues. from _review_state import BOT_LOGINS resolves because action.yml:92 runs the script by its path, and the shared set has the same values as the copy it replaces. No repo-local criteria loaded, so only the base criteria were applied. The PR description now matches the code: it explains human_engaged, and its count of 184 tests equals 175 plus the 9 tests this PR adds.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

Resolved prior findings

  • Step 6 contradicted Step 3 for resolved+outdated threads: obsolete. Step 6 (base-pr-review.md:291-295) now defers to the four cases in Step 3 and no longer has its own "post fresh when outdated" rule.
  • Re-posting a thread that a person resolved without replying: fixed. resolve-outdated-threads.py:38 fetches resolvedBy { login }, and lines 242-245 set human_engaged for a resolver who is a person. The first case in Step 3 (base-pr-review.md:152-156) then stops a new thread. test_silent_human_resolution_counts_as_engagement and test_bot_resolution_is_not_engagement cover this.
  • Stale is_resolvable above cross-reference: fixed. The docstring and comments now say should_resolve above (resolve-outdated-threads.py:197, :228), and is_resolvable no longer appears under .github/actions/pr-review.
  • Shell-quoting artifact in a test comment: fixed. No '"'"' sequence remains in test_verdict_scaffolding.py.
  • PR description out of date (earlier summary item): fixed. The body no longer mentions has_author_reply, describes human_engaged including the resolver and truncation cases, and states 184 tests.

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit a5dbd76b7017

General PR Review: [CXH-3051] Stop re-raising findings a human already closed

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: incremental since b677541
View review run

Review Summary

Overall, the PR changes collect_prior_findings to report human_engaged for each prior finding. The value is true when a person replied in the thread, a person resolved it (taken from the new resolvedBy field), or the thread has more comments than the 20 fetched. Step 3 of the prompt now uses this value to decide whether to open a new inline thread. A still-present finding is always counted in the summary, whatever happens to its thread.

The new commit widens the Step 3 "duplicate" case. Another thread for the same issue now covers it when that thread is open and accurate, or when a person engaged with it. Older bot-resolved threads for that issue can then no longer reach the "outdated, post fresh" case. The commit also tightens the wording of a docstring and a comment.

I scanned the full PR diff for security and correctness issues. from _review_state import BOT_LOGINS resolves because action.yml:92 runs the script by its path, and the shared set has the same values as the copy it replaces. No repo-local criteria loaded, so only the base criteria were applied. The PR description now matches the code: it explains human_engaged, and its count of 184 tests equals 175 plus the 9 tests this PR adds.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

Resolved prior findings

  • Step 6 contradicted Step 3 for resolved+outdated threads: obsolete. Step 6 (base-pr-review.md:291-295) now defers to the four cases in Step 3 and no longer has its own "post fresh when outdated" rule.
  • Re-posting a thread that a person resolved without replying: fixed. resolve-outdated-threads.py:38 fetches resolvedBy { login }, and lines 242-245 set human_engaged for a resolver who is a person. The first case in Step 3 (base-pr-review.md:152-156) then stops a new thread. test_silent_human_resolution_counts_as_engagement and test_bot_resolution_is_not_engagement cover this.
  • Stale is_resolvable above cross-reference: fixed. The docstring and comments now say should_resolve above (resolve-outdated-threads.py:197, :228), and is_resolvable no longer appears under .github/actions/pr-review.
  • Shell-quoting artifact in a test comment: fixed. No '"'"' sequence remains in test_verdict_scaffolding.py.
  • PR description out of date (earlier summary item): fixed. The body no longer mentions has_author_reply, describes human_engaged including the resolver and truncation cases, and states 184 tests.

Reviewed commit: 9f395db1d3ae

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No blocking issues found — see the full review report

@sergiocorral-conductorone sergiocorral-conductorone left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Automated review. drafted 1 → confirmed 0, low-confidence 1, dropped 0, added 0 (panel: opus-5.5 / gpt-6-astra / gemini-4-argon, all high). The logic is sound and tests cover the new paths; one suggestion inline. Linked ticket CXH-3051 body could not be read, so its Fix-list coverage was not checked.

Comment thread .github/actions/pr-review/scripts/resolve-outdated-threads.py Outdated
human_engaged treated every author outside BOT_LOGINS as a person, and
BOT_LOGINS lists only this reviewer. Every other app that writes on these PRs
— Copilot, CodeRabbit, linear-code, dependabot — therefore counted as human
engagement, as did a resolvedBy that was any of them. Once that happened the
finding was never re-anchored inline again, however far the code moved.

Not hypothetical: linear-code comments on the PRs in this very series, and
copilot-pull-request-reviewer has reviewed connector PRs alongside this bot.

Now keyed on GraphQL's __typename: every GitHub App is a Bot, a person is a
User. Matching on login instead would need a list of every app anyone ever
installs and would miss them by default, which is the direction that silently
retires a finding. Checked against the live API first — a reply from
github-actions comes back as Bot, and its login carries no "[bot]" suffix,
which is exactly why BOT_LOGINS has to list both spellings and why an
endswith check would not work.

BOT_LOGINS stays as it is and stays narrow. _review_state uses it as a trust
allowlist for summary authorship, which is a different question from whether a
commenter is a human, and widening it for this would weaken that.

186 tests, up from 184: three apps counted as not-a-person for both replying
and resolving, a human login that looks bot-ish still a person, and the
Actions bot still a bot despite the missing suffix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@FeliLucero1
FeliLucero1 requested a review from a team October 22, 2026 13:42
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit a5dbd76b7017

General PR Review: [CXH-3051] Stop re-raising findings a human already closed

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: incremental since 9f395db
View review run

Review Summary

The PR changes Step 3 of the base prompt so a still-present prior finding gets no new inline thread when a person has dealt with that thread. A person counts as having dealt with it if they replied or resolved it. The finding is still counted in the summary. collect_prior_findings now outputs human_engaged, and the prompt is now told about resolution_blocked.

The new commit (a5dbd76) changes how a person is detected: it now checks GraphQL __typename == "User" on comment authors and on resolvedBy, instead of checking the login against BOT_LOGINS. It also adds tests for third-party apps and for the Actions bot login. I scanned the full PR diff for security and correctness. No repo-local criteria loaded, so only the base criteria apply.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New (medium confidence) — resolve-outdated-threads.py:259: GraphQL introspection shows PullRequestReviewThread.resolvedBy is type User, not Actor. A non-null resolver therefore always has __typename == "User", so it can never come back as a Bot.
    • The re-anchor path only works if GitHub returns null for bot or app resolutions. The commit dropped the login check that guarded this case.
    • The comment saying bot resolutions "come back as a Bot" is wrong.
    • test_bot_resolution_is_not_engagement and test_engagement_is_decided_by_node_type_not_login build a resolvedBy shape the API can't return, so they don't actually protect this path.

Resolved prior findings

  • Step 6 contradicting Step 3 (prior-0): obsolete. Step 6 (base-pr-review.md:291-295) now defers to the four cases in Step 3 instead of having its own "post fresh when outdated" rule.
  • Re-posting a thread a person resolved silently (prior-1): fixed. resolvedBy is fetched at resolve-outdated-threads.py:38, and a person resolver sets human_engaged at :259. The first case in Step 3 (base-pr-review.md:152-157) then blocks a new thread.
  • Stale is_resolvable above reference (prior-2): fixed. The docstring now says should_resolve above (resolve-outdated-threads.py:208).
  • Shell-quoting artifact in a test comment (prior-3): fixed. No '"'"' sequence remains in test_verdict_scaffolding.py.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `.github/actions/pr-review/scripts/resolve-outdated-threads.py`:
- Around line 256-259: `PullRequestReviewThread.resolvedBy` has GraphQL type `User` (not `Actor`), so a non-null resolver always has `__typename == "User"` and can never be a `Bot`. Keep a login guard for the resolver, e.g. `_is_person(r) and (r or {}).get("login", "") not in BOT_LOGINS`, so a bot resolution reported as a User still keeps the re-anchor path. Replace the "whose resolutions come back as a Bot" comment with an accurate one: app resolutions are expected to return null.

In `.github/actions/pr-review/scripts/test_verdict_scaffolding.py`:
- Around lines 1775 and 1905: The fixtures set `resolvedBy` to `{"__typename": "Bot"}`, which the API cannot return. Model bot resolutions as `resolvedBy: None`, and add a case for a User-typed resolver whose login is in BOT_LOGINS so the guard above is tested.

Comment thread .github/actions/pr-review/scripts/resolve-outdated-threads.py Outdated
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit c8a748338eb7

General PR Review: [CXH-3051] Stop re-raising findings a human already closed

Blocking Issues: 0 | Suggestions: 1 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: incremental since 9f395db
View review run

Review Summary

The PR changes Step 3 of the base prompt so a still-present prior finding gets no new inline thread when a person has dealt with that thread. A person counts as having dealt with it if they replied or resolved it. The finding is still counted in the summary. collect_prior_findings now outputs human_engaged, and the prompt is now told about resolution_blocked.

The new commit (a5dbd76) changes how a person is detected: it now checks GraphQL __typename == "User" on comment authors and on resolvedBy, instead of checking the login against BOT_LOGINS. It also adds tests for third-party apps and for the Actions bot login. I scanned the full PR diff for security and correctness. No repo-local criteria loaded, so only the base criteria apply.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • New (medium confidence) — resolve-outdated-threads.py:259: GraphQL introspection shows PullRequestReviewThread.resolvedBy is type User, not Actor. A non-null resolver therefore always has __typename == "User", so it can never come back as a Bot.
    • The re-anchor path only works if GitHub returns null for bot or app resolutions. The commit dropped the login check that guarded this case.
    • The comment saying bot resolutions "come back as a Bot" is wrong.
    • test_bot_resolution_is_not_engagement and test_engagement_is_decided_by_node_type_not_login build a resolvedBy shape the API can't return, so they don't actually protect this path.

Resolved prior findings

  • Step 6 contradicting Step 3 (prior-0): obsolete. Step 6 (base-pr-review.md:291-295) now defers to the four cases in Step 3 instead of having its own "post fresh when outdated" rule.
  • Re-posting a thread a person resolved silently (prior-1): fixed. resolvedBy is fetched at resolve-outdated-threads.py:38, and a person resolver sets human_engaged at :259. The first case in Step 3 (base-pr-review.md:152-157) then blocks a new thread.
  • Stale is_resolvable above reference (prior-2): fixed. The docstring now says should_resolve above (resolve-outdated-threads.py:208).
  • Shell-quoting artifact in a test comment (prior-3): fixed. No '"'"' sequence remains in test_verdict_scaffolding.py.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `.github/actions/pr-review/scripts/resolve-outdated-threads.py`:
- Around line 256-259: `PullRequestReviewThread.resolvedBy` has GraphQL type `User` (not `Actor`), so a non-null resolver always has `__typename == "User"` and can never be a `Bot`. Keep a login guard for the resolver, e.g. `_is_person(r) and (r or {}).get("login", "") not in BOT_LOGINS`, so a bot resolution reported as a User still keeps the re-anchor path. Replace the "whose resolutions come back as a Bot" comment with an accurate one: app resolutions are expected to return null.

In `.github/actions/pr-review/scripts/test_verdict_scaffolding.py`:
- Around lines 1775 and 1905: The fixtures set `resolvedBy` to `{"__typename": "Bot"}`, which the API cannot return. Model bot resolutions as `resolvedBy: None`, and add a case for a User-typed resolver whose login is in BOT_LOGINS so the guard above is tested.

Reviewed commit: a5dbd76b7017

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No blocking issues found — see the full review report

The previous commit moved both halves of human_engaged onto __typename. That
is right for comment authors and wrong for the resolver, and the wrong half
silently disabled the re-anchor path.

PullRequestReviewThread.resolvedBy is the User OBJECT, not the Actor
interface — introspection returns {"kind":"OBJECT","name":"User"} — so an app
that resolves a thread is coerced into a User and reports __typename "User"
like anyone else. Every bot resolution therefore read as a person, and a
finding whose thread the bot closed was never re-anchored again.

It is not hypothetical and GitHub does not return null for it either:
c1-squire-dev[bot] resolved 37 threads on baton-datadog#40, each reported as
User. The tests did not catch it because they asserted a shape the API never
returns, {"__typename": "Bot"} on resolvedBy.

So the two halves now use different tests, because the schema types them
differently: authors by node type, resolvers by login. Under the User
coercion the login keeps the "[bot]" suffix that the Bot node omits —
github-actions/Bot as an author, c1-squire-dev[bot]/User as a resolver —
which is also the reason BOT_LOGINS has to list both spellings. Both facts
are checked against the live API and written down beside the code, since the
asymmetry invites exactly the simplification that caused this.

187 tests, up from 186. The resolver cases now use real shapes and cover the
app that was observed doing it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit c8a748338eb7

General PR Review: [CXH-3051] Stop re-raising findings a human already closed

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: incremental since a5dbd76
View review run

Review Summary

The new commit splits the "is a person" check into two parts. _author_is_person still uses the GraphQL node type, because comment authors are typed as the Actor interface. The new _resolver_is_person uses the login instead, because resolvedBy is typed as the User object, so an app resolver also comes back as "User". It rejects any login in BOT_LOGINS and any login ending in [bot] (resolve-outdated-threads.py:181-210, :278). The test fixtures now build resolvedBy in the User-typed shape the API actually returns, and a new table test covers c1-squire-dev[bot], both github-actions spellings, and a human login (test_verdict_scaffolding.py:1775-1780, :1909-1925).

This fixes the medium-confidence issue the previous summary raised about resolvedBy typing. I scanned the full PR diff for security and correctness issues. I also rechecked the Step 3 rules in base-pr-review.md:131-177 and found nothing new. No repo-local criteria loaded, so only the base criteria were applied. The incremental diff is complete: nothing was dropped or truncated.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

Resolved prior findings

  • Bot resolutions read as a person (previous summary, resolvedBy typed as User): fixed. _resolver_is_person decides by login, not node type (resolve-outdated-threads.py:207-210). The fixtures now model the real User-typed shape, and test_resolver_engagement_cannot_use_node_type covers bot and human logins.
  • Step 6 contradicted Step 3 for resolved+outdated threads (prior-0): obsolete. Step 6 no longer has its own "post fresh when outdated" rule and defers to the Step 3 cases (base-pr-review.md:152-177).
  • A thread a person resolved silently got re-posted (prior-1): fixed. resolvedBy { login __typename } is fetched (resolve-outdated-threads.py:38), and a human resolver sets human_engaged (:278). The first Step 3 case (base-pr-review.md:152-157) then suppresses a new thread.
  • Stale is_resolvable above reference (prior-2): fixed. The docstring now says should_resolve above (resolve-outdated-threads.py:229).
  • Shell-quoting artifact in a test comment (prior-3): fixed. No '"'"' sequence remains in test_verdict_scaffolding.py.

@github-actions

github-actions Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit 12b1212e7087

General PR Review: [CXH-3051] Stop re-raising findings a human already closed

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base e6432834cb21.
Review mode: incremental since a5dbd76
View review run

Review Summary

The new commit splits the "is a person" check into two parts. _author_is_person still uses the GraphQL node type, because comment authors are typed as the Actor interface. The new _resolver_is_person uses the login instead, because resolvedBy is typed as the User object, so an app resolver also comes back as "User". It rejects any login in BOT_LOGINS and any login ending in [bot] (resolve-outdated-threads.py:181-210, :278). The test fixtures now build resolvedBy in the User-typed shape the API actually returns, and a new table test covers c1-squire-dev[bot], both github-actions spellings, and a human login (test_verdict_scaffolding.py:1775-1780, :1909-1925).

This fixes the medium-confidence issue the previous summary raised about resolvedBy typing. I scanned the full PR diff for security and correctness issues. I also rechecked the Step 3 rules in base-pr-review.md:131-177 and found nothing new. No repo-local criteria loaded, so only the base criteria were applied. The incremental diff is complete: nothing was dropped or truncated.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

Resolved prior findings

  • Bot resolutions read as a person (previous summary, resolvedBy typed as User): fixed. _resolver_is_person decides by login, not node type (resolve-outdated-threads.py:207-210). The fixtures now model the real User-typed shape, and test_resolver_engagement_cannot_use_node_type covers bot and human logins.
  • Step 6 contradicted Step 3 for resolved+outdated threads (prior-0): obsolete. Step 6 no longer has its own "post fresh when outdated" rule and defers to the Step 3 cases (base-pr-review.md:152-177).
  • A thread a person resolved silently got re-posted (prior-1): fixed. resolvedBy { login __typename } is fetched (resolve-outdated-threads.py:38), and a human resolver sets human_engaged (:278). The first Step 3 case (base-pr-review.md:152-157) then suppresses a new thread.
  • Stale is_resolvable above reference (prior-2): fixed. The docstring now says should_resolve above (resolve-outdated-threads.py:229).
  • Shell-quoting artifact in a test comment (prior-3): fixed. No '"'"' sequence remains in test_verdict_scaffolding.py.

Reviewed commit: c8a748338eb7

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No blocking issues found — see the full review report

Comment thread .github/actions/pr-review/prompts/base-pr-review.md
FeliLucero1 and others added 2 commits October 26, 2026 08:28
The case list decided per entry, and nothing stopped one issue from getting
several fresh comments in a single run. An issue with two stale threads: the
first entry finds its sibling open but outdated, so "open and accurate" does
not match and it posts; the second entry finds the first sibling resolved and
posts too. Two threads for one issue in one run, four after the next change at
that spot.

Under resolution_blocked it is the same shape and permanent, since nothing is
ever auto-resolved and the stale threads only pile up — the mode the prompt
was just taught to recognise without being told what to do differently in it.

Third clause on the covering case: do not post if you already posted for this
issue earlier in this audit. No new data from the script — the audit knows
what it has already done. The three clauses now cover the three ways the same
issue gets raised twice: something live covers it, a person dealt with it, or
you just covered it yourself.

Not a regression — main behaves this way too — but the description claimed
that case stops accumulation by any route, and now it does.

Merging main also picks up scripts/test-check-image-locks.py from #157, which
the required image-locks check runs and this branch predated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Superseded — see the current review report for commit 12b1212e7087

General PR Review: [CXH-3051] Stop re-raising findings a human already closed

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base 27d6d5c288ea.
Review mode: full
View review run

Review Summary

resolve-outdated-threads.py now fetches resolvedBy and the author __typename, and adds a human_engaged flag to each prior finding. The flag is set when a User-typed author replied, when a resolver is not a [bot] login or listed in BOT_LOGINS, or when the thread was cut off at 20 comments (resolve-outdated-threads.py:273-279). BOT_LOGINS now comes from _review_state (the same set as before, so no behavior change; the import resolves because action.yml:92 runs the script by path). The prompt's Step 3 now uses four ordered thread cases, and Step 6 points to them instead of repeating its own conflicting rule. I scanned the full diff for security and correctness, and nothing turned up.

I traced the convergence cases and found none that adds a thread on every run:

  • Bot-resolved outdated thread plus an accurate re-post: case 2 covers it.
  • Several stale threads for one issue: the "already posted this audit" clause keeps it to one new thread per run.
  • Outdated thread where another app replied: it is never auto-resolved, but case 2 stops new threads once a re-post exists.

There are no repo-local criteria, so the base criteria plus the SDK/shared-library compatibility checks were applied. The prior_findings JSON gains one field and nothing is removed, so existing readers are unaffected. I did not run the tests, because this review is read-only.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

Resolved prior findings

  • Step 6 contradicted Step 3 for resolved+outdated threads (prior-0): obsolete. Step 6 no longer has its own "post fresh when outdated" rule. It now just points to the Step 3 cases (base-pr-review.md:297-301).
  • A person's silent resolution got re-posted (prior-1): fixed. resolvedBy { login __typename } is now fetched (resolve-outdated-threads.py:38), and a human resolver sets human_engaged (:278). The first Step 3 case then skips the new thread.
  • Stale is_resolvable above reference (prior-2): fixed. The docstring now names should_resolve above (resolve-outdated-threads.py:229), and is_resolvable appears nowhere in scripts/.
  • Shell-quoting artifact in a test comment (prior-3): fixed. No '"'"' sequence remains in test_verdict_scaffolding.py.

@github-actions

github-actions Bot commented Oct 26, 2026 •

Copy link
Copy Markdown

General PR Review: [CXH-3051] Stop re-raising findings a human already closed

Blocking Issues: 0 | Suggestions: 0 | Threads Resolved: 0
Criteria: Criteria status: none loaded - .claude/skills/ci-review.md was not found at trusted base 27d6d5c288ea.
Review mode: full
View review run

Review Summary

resolve-outdated-threads.py now fetches resolvedBy and the author __typename, and adds a human_engaged flag to each prior finding. The flag is set when a User-typed author replied, when a resolver is not a [bot] login or listed in BOT_LOGINS, or when the thread was cut off at 20 comments (resolve-outdated-threads.py:273-279). BOT_LOGINS now comes from _review_state (the same set as before, so no behavior change; the import resolves because action.yml:92 runs the script by path). The prompt's Step 3 now uses four ordered thread cases, and Step 6 points to them instead of repeating its own conflicting rule. I scanned the full diff for security and correctness, and nothing turned up.

I traced the convergence cases and found none that adds a thread on every run:

  • Bot-resolved outdated thread plus an accurate re-post: case 2 covers it.
  • Several stale threads for one issue: the "already posted this audit" clause keeps it to one new thread per run.
  • Outdated thread where another app replied: it is never auto-resolved, but case 2 stops new threads once a re-post exists.

There are no repo-local criteria, so the base criteria plus the SDK/shared-library compatibility checks were applied. The prior_findings JSON gains one field and nothing is removed, so existing readers are unaffected. I did not run the tests, because this review is read-only.

Security Issues

None found.

Correctness Issues

None found.

Suggestions

None.

Resolved prior findings

  • Step 6 contradicted Step 3 for resolved+outdated threads (prior-0): obsolete. Step 6 no longer has its own "post fresh when outdated" rule. It now just points to the Step 3 cases (base-pr-review.md:297-301).
  • A person's silent resolution got re-posted (prior-1): fixed. resolvedBy { login __typename } is now fetched (resolve-outdated-threads.py:38), and a human resolver sets human_engaged (:278). The first Step 3 case then skips the new thread.
  • Stale is_resolvable above reference (prior-2): fixed. The docstring now names should_resolve above (resolve-outdated-threads.py:229), and is_resolvable appears nowhere in scripts/.
  • Shell-quoting artifact in a test comment (prior-3): fixed. No '"'"' sequence remains in test_verdict_scaffolding.py.

Reviewed commit: 12b1212e7087

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No blocking issues found — see the full review report

@FeliLucero1
FeliLucero1 merged commit fb19446 into main Oct 28, 2026
7 checks passed
@FeliLucero1
FeliLucero1 deleted the felipe/pr-review-stop-reposting-resolved branch October 28, 2026 12:56
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.

6 participants