Skip to content

feat(#106): reconcile findings maintainers have already declined - #709

Draft
ralphbean wants to merge 2 commits into
mainfrom
agent/106-trusted-maintainer-declines
Draft

feat(#106): reconcile findings maintainers have already declined#709
ralphbean wants to merge 2 commits into
mainfrom
agent/106-trusted-maintainer-declines

Conversation

@ralphbean

Copy link
Copy Markdown
Member

Summary

Addresses #106: the review agent re-raises a finding on every subsequent push even after a maintainer explicitly replied declining it (e.g. "pre-existing pattern, out of scope for this PR").

  • New step 2a-1 in the pr-review orchestrator: on re-review, fetch reply threads on the agent's own prior inline findings via gh api pulls/{pr}/comments, and identify explicit declines.
  • Trust boundary: a decline only counts when the replying user's author_association is OWNER/MEMBER/COLLABORATOR (falling back to the collaborator permission API for write+ access) — the same tier .github/scripts/check-e2e-authorization.sh already uses elsewhere in this repo. Replies from anyone else, including a PR author without maintainer status, are inert. This is specifically to prevent an untrusted commenter from forging a "not a bug, dismissing this" reply to suppress a real finding.
  • New check in step 6e: a finding matching a trusted decline, on code unchanged since the decline, is downgraded to info severity with a note rather than dropped — stays visible for transparency, doesn't re-inflate the verdict, and reverts to full re-evaluation the moment the code changes.

This is explicitly marked experimental in the skill doc. It only covers the "declined via an explicit reply" case from #106 — not findings silently dropped across rounds, not self-contradictory reconciliation, not non-reply dismissals (e.g. a wontfix label). The intent is to try this narrower behavior, see whether it actually improves the review experience in practice, and use that to decide whether/how to broaden it. #106 should stay open until the fuller picture is addressed.

Test plan

SKILL.md is a prompt document with no automated test harness (unlike scripts/*.sh, which have *-test.sh counterparts), so this can't be exercised with a unit test.

  • pre-commit run --files skills/pr-review/SKILL.md passes
  • Observe behavior on a real PR where a maintainer declines a finding via reply, across 2+ subsequent pushes, and confirm the finding is downgraded rather than re-raised at its original severity
  • Confirm a decline reply from a non-maintainer (e.g. external contributor) is not honored

🤖 Generated with Claude Code

Adds an experimental step to the pr-review orchestrator: on re-review,
fetch reply threads on the agent's own prior inline findings and check
whether a repo maintainer explicitly declined one as out of scope.
Declines are trusted only from users with OWNER/MEMBER/COLLABORATOR
association (or write+ via the collaborator permission API) — the same
tier .github/scripts/check-e2e-authorization.sh already uses — so an
untrusted commenter cannot forge a dismissal to suppress a real
finding. Matched, unchanged findings are downgraded to info with a
note rather than dropped, so they stay visible without re-inflating
the verdict on every push.

This narrows one specific case of #106 (declined-via-reply) and is
explicitly marked experimental in the skill doc — it doesn't address
findings dropped silently or non-reply dismissals. Intended as a first
iteration to observe before deciding whether to expand it.

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
@ralphbean
ralphbean requested a review from a team as a code owner August 6, 2026 18:56
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Honor maintainer-declined prior findings on re-review (experimental)

✨ Enhancement 📝 Documentation 🕐 20-40 Minutes

Grey Divider

AI Description

• Add an experimental re-review step to detect maintainer replies declining prior findings.
• Define a maintainer-only trust boundary using author association / collaborator permissions.
• Downgrade unchanged, declined findings to info to avoid re-inflating verdicts.
Diagram

graph TD
  A["Re-review run"] --> B{"Prior review\napp-verified?"} --> C["Fetch inline threads\n(gh api comments)"] --> D{"Root comment\nfrom review app?"} --> E{"Reply by\nmaintainer?"} --> F[("DECLINED_FINDINGS")]
  B -->|"No"| Z["Skip decline logic"]
  D -->|"No"| Z
  E -->|"No"| Z
  F --> G{"Finding matches\nfile+line & unchanged?"} --> H["Downgrade to info\n(actionable=false)"]
  G -->|"No"| I["Normal re-evaluation"]

  subgraph Legend
    direction LR
    _proc["Process"] ~~~ _dec{"Decision"} ~~~ _data[("Recorded state")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use GitHub thread resolution state as the signal
  • ➕ Leverages a first-class GitHub concept (resolved/unresolved) instead of parsing reply text
  • ➕ Doesn’t require inferring intent from natural language decline phrases
  • ➖ Resolution can be toggled by people other than maintainers depending on repo settings
  • ➖ Resolution doesn’t necessarily mean “declined”; it can mean “fixed” or “acknowledged”
2. Persist decline decisions in a machine-readable store (labels/check-run artifact)
  • ➕ More deterministic than NLP over reply bodies; easier to test and audit
  • ➕ Can encode richer policy (scope, expiration, owner) without guessing intent
  • ➖ Adds operational overhead (maintainers must apply labels or interact with a bot UI)
  • ➖ Requires additional plumbing/state management outside the PR comment stream

Recommendation: The PR’s approach is a good first iteration because it targets a narrow, high-friction case (#106) while keeping a clear trust boundary (maintainer-only) and preserving transparency (downgrade to info rather than suppress). If false positives/negatives from reply-text interpretation become a problem, consider moving to a more explicit, machine-readable signal (e.g., label/artifact) while retaining the same maintainer trust gating.

Files changed (1) +92 / -0

Enhancement (1) +92 / -0
SKILL.mdDocument experimental maintainer-decline reconciliation for re-reviews +92/-0

Document experimental maintainer-decline reconciliation for re-reviews

• Adds step 2a-1 describing how to fetch inline review comment threads, verify they originated from the review app, and detect explicit maintainer declines using a strict trust boundary. Extends step 6e to downgrade unchanged findings that match a trusted decline to info severity (actionable: false) while re-evaluating normally once code changes.

skills/pr-review/SKILL.md

@qodo-code-review

qodo-code-review Bot commented Aug 6, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Decline key too broad ✓ Resolved 🐞 Bug ≡ Correctness
Description
Step 6e downgrades findings based only on file/line membership in DECLINED_FINDINGS, which can
incorrectly downgrade a different-category finding at the same location even though step 6c
explicitly preserves distinct-category findings at the same file/line. This can suppress legitimate
medium/high findings on re-review and distort the final verdict.
Code

skills/pr-review/SKILL.md[R1130-1133]

+- If the file is unchanged and the finding's `file`/`line` matches an
+  entry in `DECLINED_FINDINGS` (step 2a-1), downgrade the finding to
+  `info` severity and prepend to its description: "Previously raised
+  and declined as out of scope by @<login> (<author_association>) —
Relevance

●●● Strong

Clear correctness inconsistency: decline matching should include category/description as earlier
step states.

PR-#49

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The workflow records declines with file/line but then applies them to any matching
file/line, while the doc explicitly supports multiple findings with different categories at the
same location; this creates an over-broad match that can downgrade the wrong finding.

skills/pr-review/SKILL.md[874-904]
skills/pr-review/SKILL.md[306-312]
skills/pr-review/SKILL.md[1122-1136]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new trusted-decline mechanism records declines without a stable discriminator (it stores only `file`/`line` + decliner metadata), but later applies the decline to any finding at the same `file`/`line`. This is incorrect because the orchestrator explicitly allows multiple distinct findings (different categories) at the same location.

### Issue Context
- Step 6c keeps different-category findings separate even when they reference the same code.
- Step 2a-1 says to identify the corresponding prior finding using “matching category/description”, but the recorded `DECLINED_FINDINGS` entry does not retain category/identity.
- Step 6e applies the downgrade using only `file`/`line` equality.

### Fix Focus Areas
- skills/pr-review/SKILL.md[306-312]
- skills/pr-review/SKILL.md[1122-1136]
- skills/pr-review/SKILL.md[895-904]

### Concrete fix
- Extend `DECLINED_FINDINGS` entries to include at least `category` (and ideally a stable identifier such as the root comment `id`, plus a short hash of the prior finding’s description).
- In step 6e, require `file` + `line` + `category` (and/or the stable identifier) to match before downgrading.
- If multiple prior findings exist at the same location, only downgrade the one(s) explicitly declined.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Protected skills file modified ✗ Dismissed 📜 Skill insight § Compliance
Description
This PR modifies skills/pr-review/SKILL.md, which is a protected governance/infrastructure path
and must not be auto-approved. Ensure explicit human review/approval is required before merge.
Code

skills/pr-review/SKILL.md[R253-256]

+### 2a-1. Trusted dismissals of prior findings (re-reviews)
+
+**Status: experimental.** This step narrows one specific case of
+[agents#106](https://github.com/fullsend-ai/agents/issues/106) — a
Relevance

●● Moderate

Protected-path gating is policy-like; prior similar notes exist but outcomes were undetermined.

PR-#59
PR-#157

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The compliance checklist requires raising a finding whenever protected governance/infrastructure
paths (including skills/) are modified to ensure the PR is not auto-approved. The diff shows new
content added to skills/pr-review/SKILL.md, which is under the protected skills/ path.

skills/pr-review/SKILL.md[253-256]
Skill: pr-review

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
This PR modifies a protected path (`skills/`), which must not be auto-approved.

## Issue Context
Protected governance/infrastructure paths require explicit human review/approval before merge.

## Fix Focus Areas
- skills/pr-review/SKILL.md[253-256]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Unbounded comments fetch ✓ Resolved 🐞 Bug ☼ Reliability
Description
Step 2a-1 aggregates all PR review comments across all pages into a single JSON blob and suggests
per-user permission fallback checks without any deduping/caching guidance. On PRs with large
review-comment histories or many replies requiring fallback checks, this can significantly increase
API calls and payload size, making re-reviews slower and more failure-prone.
Code

skills/pr-review/SKILL.md[R271-273]

+REVIEW_COMMENTS=$(gh api "repos/${REPO_FULL_NAME}/pulls/${PR_NUMBER}/comments?per_page=100" \
+  --paginate | jq -s 'add // []')
+```
Relevance

●● Moderate

Perf/scaling guidance for paginated gh api fetch is plausible but no close accepted/rejected
precedent.

PR-#172

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new instructions explicitly fetch all review comments with pagination and then introduce a
collaborator-permission fallback path; without guidance to filter fields or dedupe calls, this can
scale poorly on PRs with many comments/replies.

skills/pr-review/SKILL.md[268-273]
skills/pr-review/SKILL.md[285-293]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The new step fetches the full review-comment payload for the entire PR (`--paginate` + `jq -s 'add'`), which can be large, and it introduces potential repeated collaborator-permission lookups for multiple replies from the same user.

### Issue Context
This is an orchestrator/runtime reliability concern: large API payloads and redundant follow-up API calls can increase latency and raise the chance of hitting rate limits or tool/output constraints in long-lived PRs.

### Fix Focus Areas
- skills/pr-review/SKILL.md[268-273]
- skills/pr-review/SKILL.md[285-293]

### Concrete fix
- Reduce payload size by using `--jq` (or a `jq` projection) to keep only the needed fields (e.g., `id`, `in_reply_to_id`, `path`, `line`, `user.login`, `author_association`, `body`, `performed_via_github_app.slug`).
- Ensure you only consider replies whose thread root is from the app before doing any collaborator-permission API calls.
- Cache/dedupe collaborator-permission lookups per `login` (one call per unique user per run).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context used
✅ Compliance rules (platform): 55 rules
✅ Skills: 4 invoked
  code-review
  code-implementation
  pr-review
  docs-review

Grey Divider

Tip of the day
💡 Did you know, you can reply 'qodo' on any finding to push back, ask questions, or dig deeper

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread skills/pr-review/SKILL.md
Comment thread skills/pr-review/SKILL.md Outdated
Comment thread skills/pr-review/SKILL.md

@waynesun09 waynesun09 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Review sweep: 9 findings from an independent review pass (1 critical, 2 high, 6 medium), verified against live GitHub API data and the PR's own cited motivating example (agents PR #90). None overlap in substance with the existing qodo-code-review bot comments on this PR (protected-path, decline-key category collision, unbounded comments fetch) — see inline comments for details.

Comment thread skills/pr-review/SKILL.md Outdated
Comment thread skills/pr-review/SKILL.md Outdated
Comment thread skills/pr-review/SKILL.md Outdated
Comment thread skills/pr-review/SKILL.md Outdated
Comment thread skills/pr-review/SKILL.md Outdated
Comment thread skills/pr-review/SKILL.md Outdated
Comment thread skills/pr-review/SKILL.md Outdated
Comment thread skills/pr-review/SKILL.md Outdated
Comment thread skills/pr-review/SKILL.md
silently truncates file lists at 300 files), treat all files as
changed — no anchoring for this run.

### 2a-1. Trusted dismissals of prior findings (re-reviews)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

MEDIUM: Experimental decline-detection heuristics merge into the live pipeline with the PR's own manual-verification checkboxes unchecked

The PR's test plan states SKILL.md has no automated test harness and lists two unchecked, manual-only items: "Observe behavior on a real PR where a maintainer declines a finding via reply, across 2+ subsequent pushes..." and "Confirm a decline reply from a non-maintainer... is not honored." Both boxes remain unchecked on the current PR body. This step merges directly into SKILL.md, which every future pr-review invocation on every repo executes live, with no feature flag or dry-run gate beyond the "experimental" prose label. Notably, the first unchecked item -- observing behavior on a real PR -- is exactly the kind of manual check that would have surfaced the performed_via_github_app bug identified in this same review before merge.

Suggestion: hold this PR until at least one real-world round-trip is observed and documented (which would likely have caught the performed_via_github_app issue), or land it behind an explicit opt-in/kill-switch so a bad decline-classification can be disabled without reverting the whole change.

@ralphbean

Copy link
Copy Markdown
Member Author

Marking this as draft until fullsend-ai/fullsend#6045 is resolved.

The trusted-decline mechanism from 5393b60 never activated: the
pulls/comments endpoint doesn't populate performed_via_github_app, so
the app-identity gate never matched and DECLINED_FINDINGS stayed
empty. Switch the gate to user.login/user.type, which the endpoint
actually returns (verified against this repo's own review comments).
Filed fullsend-ai/fullsend#6045 to expose the harness slug as a
FULLSEND_SLUG env var so this can stop hardcoding the app's identity.

Also, since exact file/line matching plus a file-level
changed-since-prior guard couldn't survive the line drift a declined
finding's location goes through across rounds (the motivating PR #90
case this feature targets), switch decline matching to file + category
+ presence of the actual declined code, dropping the round-boundary
check entirely — a decline now expires exactly when its code changes,
not when some unrelated line in the file does.

Smaller fixes bundled in:
- exclude the PR author from qualifying as a decliner of their own
  findings
- most recent qualifying reply in a thread wins, so a maintainer can
  reverse an earlier decline
- fall back to original_line/original_start_line when GitHub nulls
  line on outdated comments
- reword the author_association fallback to match what
  check-e2e-authorization.sh actually documents (defense-in-depth for
  membership-visibility edge cases, not an API bug)
- project the review-comments fetch to needed fields and cache
  collaborator-permission lookups per login

Assisted-by: Claude Opus 4.6 <noreply@anthropic.com>
Signed-off-by: Ralph Bean <rbean@redhat.com>
@fullsend-ai-review

fullsend-ai-review Bot commented Aug 10, 2026

Copy link
Copy Markdown

🤖 Finished Review · ✅ Success · Started 3:13 PM UTC · Completed 3:34 PM UTC

Commit: 7e98ad7 · View workflow run →

@ralphbean

Copy link
Copy Markdown
Member Author

These three are the same items flagged in the inline threads — replied and resolved there (protected-path warning is intentional; decline-key and unbounded-fetch are fixed in 7e98ad7).

@fullsend-ai-review

Copy link
Copy Markdown

Review

Findings

Medium

  • [logic-error] skills/pr-review/SKILL.md:327 — Step 2a-1 matches dismissed comment threads to prior findings using "same file, same line, matching category/description," but the step 6e declined-findings check deliberately avoids line-number matching ("not line number, since a finding's line can drift"). This asymmetry means line drift at the 2a-1 stage can prevent a finding from entering DECLINED_FINDINGS, so the resilient file+category match in 6e never fires. The failure mode is conservative (findings re-raised rather than suppressed), but the feature will silently fail to recognize valid declines in common rebase/amend scenarios.
    Remediation: Relax the 2a-1 cross-reference to match on file + category + description similarity (consistent with step 6e), or document the line-drift gap as a known limitation of the experimental iteration.

  • [protected-path] skills/pr-review/SKILL.md — This PR modifies a file under the protected skills/ path. The PR links to issue Review agent re-raises pre-existing findings after author explicitly declines scope #106 and explains the rationale. Human approval is required for protected-path changes regardless of context.

Low

  • [edge-case] skills/pr-review/SKILL.md:1136 — If a maintainer declines a protected-path finding, the declined-findings mechanism would downgrade it to info severity. The existing "never approve with protected-path findings" constraint independently prevents an approve outcome, so no schema violation occurs — but a clarifying note in the declined-findings section would avoid reader confusion about this interaction.

  • [rbac-violation] skills/pr-review/SKILL.md:285 — The hardcoded bot identity check (fullsend-ai-review[bot]) is fragile if the harness slug changes. The PR acknowledges this limitation and links to fullsend#6045 for a fix via FULLSEND_SLUG env var. Failure mode is fail-closed.

  • [pattern-inconsistency] skills/pr-review/SKILL.md:253 — Issue references use Markdown hyperlinks ([agents#106](https://...)) while the existing file consistently uses plain GitHub shorthand references (e.g., fullsend-ai/fullsend#2096). Consider using the established shorthand format.

  • [pattern-inconsistency] skills/pr-review/SKILL.md — The **Status: experimental.** marker on step 2a-1 includes a period inside the bold text, while the Declined findings section reads **Status: experimental** without. These should be internally consistent.

  • [edge-case] skills/pr-review/SKILL.md:269 — The --paginate loop on the gh api call has no upper bound or timeout. PRs with very many review comments could cause excessive API calls. Failure mode is fail-closed (no dismissals applied).

  • [rbac-violation] skills/pr-review/SKILL.md:295 — The PR author exclusion relies on PR_META.user.login, which reflects the current author. In the rare case of PR authorship transfer, the exclusion may not cover the original author. Extremely narrow edge case.

  • [edge-case] skills/pr-review/SKILL.md:296 — Bot-authored PRs (renovate, dependabot) are excluded from self-dismissing findings by the author exclusion rule. A human maintainer must dismiss on their behalf. Safe default.


Labels: PR modifies the review agent skill (skills/pr-review/SKILL.md) to add trusted-dismissal behavior.

@fullsend-ai-review fullsend-ai-review 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.

See the review comment for full details.

Comment thread skills/pr-review/SKILL.md

For each thread whose most recent qualifying reply declines the
finding, resolve the root comment's location — falling back to
`original_line`/`original_start_line` when `line`/`start_line` are null

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] logic-error

Step 2a-1 matches dismissed comment threads to prior findings using 'same file, same line, matching category/description,' but step 6e's declined-findings check deliberately avoids line-number matching ('not line number, since a finding's line can drift'). This asymmetry means line drift at the 2a-1 stage can prevent a finding from entering DECLINED_FINDINGS, so the resilient file+category match in 6e never fires. The failure mode is conservative (findings re-raised rather than suppressed), but the feature will silently fail to recognize valid declines in common rebase/amend scenarios.

Suggested fix: Relax the 2a-1 cross-reference to match on file + category + description similarity (consistent with step 6e), or document the line-drift gap as a known limitation.

Comment thread skills/pr-review/SKILL.md
@@ -1052,6 +1136,46 @@ attention.
If no protected files are modified, do not add a `protected-path`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] edge-case

If a maintainer declines a protected-path finding, the declined-findings mechanism would downgrade it to info severity. The existing 'never approve with protected-path findings' constraint independently prevents an approve outcome, so no schema violation occurs, but a clarifying note would avoid reader confusion about this interaction.

Comment thread skills/pr-review/SKILL.md
`id`, built from `REVIEW_COMMENTS`) until reaching a comment with no
`in_reply_to_id`. Skip the thread unless the root comment's `login` is
`fullsend-ai-review[bot]` and `user_type` is `Bot`. (The
`pulls/.../comments` endpoint does not populate

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] rbac-violation

The hardcoded bot identity check (fullsend-ai-review[bot]) is fragile if the harness slug changes. The PR acknowledges this limitation and links to fullsend#6045 for a fix via FULLSEND_SLUG env var. Failure mode is fail-closed.

Comment thread skills/pr-review/SKILL.md
silently truncates file lists at 300 files), treat all files as
changed — no anchoring for this run.

### 2a-1. Trusted dismissals of prior findings (re-reviews)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] pattern-inconsistency

Issue references use Markdown hyperlinks while the existing file consistently uses plain GitHub shorthand references (e.g., fullsend-ai/fullsend#2096). Consider using the established shorthand format.

Suggested fix: Use plain-text shorthand format: #106 and fullsend-ai/fullsend#6045.

Comment thread skills/pr-review/SKILL.md
finding history to check dismissals against).

Fetch the PR's inline review comment threads, projecting to only the
fields this step needs (the unfiltered payload can get large on

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] edge-case

The --paginate loop on the gh api call has no upper bound or timeout. PRs with very many review comments could cause excessive API calls. Failure mode is fail-closed (no dismissals applied).

Comment thread skills/pr-review/SKILL.md
var so this can reference that instead of a literal string.)

**Trust boundary — only maintainer replies from someone other than the
PR author count as a dismissal.** A reply only qualifies when the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] rbac-violation

The PR author exclusion relies on PR_META.user.login, which reflects the current author. In the rare case of PR authorship transfer, the exclusion may not cover the original author. Extremely narrow edge case with no practical attack vector.

Comment thread skills/pr-review/SKILL.md

**Trust boundary — only maintainer replies from someone other than the
PR author count as a dismissal.** A reply only qualifies when the
replying user is not the PR author (`PR_META.user.login`, step 2), and

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] edge-case

Bot-authored PRs (renovate, dependabot) are excluded from self-dismissing findings by the author exclusion rule. A human maintainer must dismiss on their behalf. This is a safe default.

@fullsend-ai-review fullsend-ai-review Bot added requires-manual-review Review requires human judgment review-agent labels Aug 10, 2026
@ralphbean

Copy link
Copy Markdown
Member Author

Need to wait for the next release after 0.35.1 before I can use FULLSEND_SLUG and FULLSEND_ROLE.

@ben-alkov ben-alkov self-assigned this Aug 26, 2026
guyoron1 added a commit to guyoron1/agents that referenced this pull request Aug 27, 2026
…issed

Picks up ralphbean's fullsend-ai#709 at his invitation on the review-economy thread
("please take over ... or start it from scratch and we can discard
mine"). The design is his; this keeps it, closes three of the gaps its
own non-goals list, and moves the forge calls where they now belong.

Kept from fullsend-ai#709, unchanged in substance: the two-tier trust gate
(author_association in OWNER/MEMBER/COLLABORATOR, falling back to the
collaborator permission API for admin/maintain/write, the same pair
check-e2e-authorization.sh uses); the PR author never dismissing their own
findings even holding a qualifying role; keying the dismissal to whether
the dismissed code is still present rather than to a round boundary;
matching on file + category, never line; downgrading to info +
actionable:false rather than dropping; and the experimental framing with
explicit non-goals.

Split across the forge boundary. fullsend-ai#709 puts a `gh api` call in the shared
SKILL.md. That skill is now forge-abstracted — it delegates every fetch
to "the forge-specific review skill's <section>" and pr-review/github and
pr-review/gitlab supply the commands — so the dismissal fetch follows
suit: the shared skill carries the semantics, github/SKILL.md carries the
GraphQL query, and step 2a-1 skips when a forge has no such section. That
keeps GitLab on today's behavior instead of breaking it.

What this adds:

1. Non-reply dismissals. fullsend-ai#709's non-goals name these; they are the two
   things people actually reach for when a finding is not worth a
   sentence. Resolving the conversation and a thumbs-down on the bot's
   comment now count, and both are trust-gated exactly like a reply. One
   GraphQL reviewThreads query replaces the paginated REST call and
   carries all three signals: resolution is GraphQL-only, reactions cost
   a request per comment over REST, and threads arrive pre-grouped, so
   the in_reply_to_id chain walk goes away with it.

2. A critical carve-out. As fullsend-ai#709 stands, any severity downgrades to info,
   critical included, and stays there for as long as the code is
   unchanged — which is the case where it should stay unchanged. Critical
   findings are now emitted at critical with the dismissal noted
   alongside. info + actionable:false resolves to `approve` in 6f, and
   that is the one outcome a critical finding must not produce.
   Refutation still downgrades a critical finding, because that is a
   verified judgment about the code rather than a dismissal of it.

3. Disputes, engaged exactly once. "This isn't a bug, because X" is not a
   decline, so under fullsend-ai#709 it falls through and the finding re-raises
   verbatim next push — fullsend-ai#106 wearing a different hat. The argument is
   judged on its merits and is deliberately not trust-gated (the PR
   author is usually the one making it, and correctness is not a
   permission). Refuted, it downgrades; not refuted, the finding stands
   with one sentence engaging it, and the exchange is over.

4. A fail-closed trust boundary, because the collaborator-permission
   fallback does not work from where this runs. GitHub rejects that
   endpoint without push access ("Must have push access to view
   collaborator permission"), and the review agent is deliberately
   read-only — readonly_repo: true, providers/github-ro.yaml, and a
   policy whose own comment says "No write access to GitHub". So the
   second tier generally 403s in the sandbox. Any error is treated as
   not trusted: the dismissal does not count and the finding is emitted
   normally.

   That has a consequence worth stating rather than leaving to be
   discovered: on a private organization, where a real admin's
   association reports as CONTRIBUTOR, tier one under-reports and tier
   two cannot compensate. Rather than leave the new signals dead on
   arrival — resolvers and reactors carry no association at all — a
   middle tier looks the login up among the associations the same query
   already returned for this PR's thread comments, which covers the
   common case at no extra request. Closing the gap properly means
   resolving trust on the runner, where a write-scoped token exists, and
   passing the result in; that is a separate change.

Two corrections to fullsend-ai#709's text, both verified rather than assumed:

- fullsend#6045 has shipped, so the review app's identity no longer has
  to be a literal. FULLSEND_SLUG is exported into the sandbox from the
  harness identity and is in reservedSandboxKeys so env.sandbox cannot
  shadow it. The configured login stays only as the fallback for a
  harness that declares no slug.
- The bot's login has two spellings and the query returns both at once.
  GraphQL reports a Bot-typed author without the [bot] suffix — the form
  FULLSEND_SLUG holds, so it compares directly — while REST's user.login
  and a bot appearing under resolvedBy (typed User, not Bot) both carry
  it. fullsend#6456 corrected this same mismatch in another skill.

Verified against live data rather than from the schema: every field in
the query — isResolved, resolvedBy, authorAssociation, diffHunk,
reactionGroups.reactors, the pageInfo flags — was run against real review
threads on fullsend-ai#1003, and the snippet was executed exactly
as it appears in github/SKILL.md (exit 0). Three behaviours worth knowing
came out of that and are documented next to the query: reviewThreads
returns oldest-first, so it uses last: 100 while comments within a thread
stay first: 50 so nodes[0] is the root; `line` comes back null with
originalLine set once a comment's diff position goes stale, which on a
re-review is the common case; and reactionGroups returns all eight
contents even at zero, so totalCount must be checked before reading
reactors.

Not included, and neither omission is a shortcut:

An eval case is blocked twice over. eval/review/cases/*/input.yaml
expresses only forge, seed_issues and fixture — there is no way to seed a
prior review, review threads, replies, resolution or reactions, and the
case lifecycle is a single agent run. Even given that, the runner reuses
GH_TOKEN as REVIEW_TOKEN, so a seeded review comment would carry no
performed_via_github_app.client_id, PRIOR_REVIEW_PROVENANCE would be
unverifiable-no-app, and step 2a-1 would skip by design. That is agents#245.
Worth revisiting when fullsend-ai#245 lands, because this is behaviour that will rot
silently.

GitLab parity is left explicitly unimplemented rather than guessed. The
signals exist there — discussions carry resolved and resolved_by,
award_emoji carries the reaction — but the trust boundary has no verified
field mapping and I have no live instance to check one against. That is
the part that must not be approximated, so gitlab/SKILL.md says so and
step 2a-1 skips when a forge provides no section.

Signed-off-by: guy oron <goron@redhat.com>
guyoron1 added a commit to guyoron1/agents that referenced this pull request Aug 27, 2026
…iliation

Five of the eight findings on this PR were real. Taking them in order of
how badly they broke things.

The GitLab guard did not guard. Step 2a-1 said it skips when the forge
skill "has no Review thread dismissals section" — and then gitlab/SKILL.md
was given a section with exactly that heading to explain that GitLab is
unimplemented. So the check passed, and a GitLab re-review would have
entered the mechanism looking for commands that are not there. The
condition now keys on whether the forge supplies a fetch, not on whether a
heading exists, and the GitLab section says in its first line that it
supplies none. Self-inflicted in the forge split; the whole point of that
split was that GitLab keeps working.

Thread replies were truncated without anyone noticing. comments(first:50)
has to stay `first` so nodes[0] is the root, but "the most recent
qualifying reply wins" needs the tail, and on a thread past 50 comments
the tail is exactly what is missing — including a reversal like "actually,
let's fix this after all". The query now selects comments.pageInfo, and a
thread whose own hasNextPage is true is treated as undetermined and
dismisses nothing. Acting on a truncated view could have honored a
dismissal the author had already withdrawn.

File + category could silence a finding nobody dismissed. One file often
holds several findings of one category; dismissing one of them matched all
of them, because the "is the code still present" check asks about the file,
not about the finding. A match now also requires the finding to be about
the dismissed code — the recorded snippet covers its location, or it
describes the same defect in the same construct — and anything that cannot
be established that way is emitted normally. This one is inherited from
fullsend-ai#709 rather than introduced here, but it is the failure that ends in a
wrongly clean approval.

Disputing a high-severity defect twice stopped it blocking. The
engage-once rule downgraded anything below critical once the single
exchange was spent, so a real high finding could stop blocking without
ever being refuted — the ping-pong cure reintroducing the disease one
notch down. The stop now ends the argument, not the finding: critical and
high keep their severity however often they are disputed, only medium and
below downgrade, and a reply that actually refutes is honored at any
severity because refutation is judged on the code and is never used up.

The no-slug fallback pointed at nothing. It said to fall back to "the
configured review-app login" without naming a source, which is not
actionable in the one case it exists for. Since there is no reliable way
to tell this agent's threads from another bot's without the slug, that
case now skips the step. harness/review.yaml declares
slug: fullsend-ai-review, so it is always set here.

Not changed, with reasons:

- reactors is correct and `users` does not exist. Live introspection of
  ReactionGroup returns content, createdAt, reactors, subject,
  viewerHasReacted — no users field — and the query in this PR was run
  against real review threads for exactly this reason. Nothing to fix.
- reviewThreads(last:100) and reactors(first:10) both truncate, and both
  fail closed: a dismissal that is not read is a dismissal not honored,
  and the finding is emitted. The pagination note already says a thread's
  absence must not be read as the absence of a dismissal. Fail-open
  truncation would deserve a fix; this does not.

Signed-off-by: guy oron <goron@redhat.com>
guyoron1 added a commit to guyoron1/agents that referenced this pull request Aug 27, 2026
…iliation

Five of the eight findings on this PR were real. Taking them in order of
how badly they broke things.

The GitLab guard did not guard. Step 2a-1 said it skips when the forge
skill "has no Review thread dismissals section" — and then gitlab/SKILL.md
was given a section with exactly that heading to explain that GitLab is
unimplemented. So the check passed, and a GitLab re-review would have
entered the mechanism looking for commands that are not there. The
condition now keys on whether the forge supplies a fetch, not on whether a
heading exists, and the GitLab section says in its first line that it
supplies none. Self-inflicted in the forge split; the whole point of that
split was that GitLab keeps working.

Thread replies were truncated without anyone noticing. comments(first:50)
has to stay `first` so nodes[0] is the root, but "the most recent
qualifying reply wins" needs the tail, and on a thread past 50 comments
the tail is exactly what is missing — including a reversal like "actually,
let's fix this after all". The query now selects comments.pageInfo, and a
thread whose own hasNextPage is true is treated as undetermined and
dismisses nothing. Acting on a truncated view could have honored a
dismissal the author had already withdrawn.

File + category could silence a finding nobody dismissed. One file often
holds several findings of one category; dismissing one of them matched all
of them, because the "is the code still present" check asks about the file,
not about the finding. A match now also requires the finding to be about
the dismissed code — the recorded snippet covers its location, or it
describes the same defect in the same construct — and anything that cannot
be established that way is emitted normally. This one is inherited from
fullsend-ai#709 rather than introduced here, but it is the failure that ends in a
wrongly clean approval.

Disputing a high-severity defect twice stopped it blocking. The
engage-once rule downgraded anything below critical once the single
exchange was spent, so a real high finding could stop blocking without
ever being refuted — the ping-pong cure reintroducing the disease one
notch down. The stop now ends the argument, not the finding: critical and
high keep their severity however often they are disputed, only medium and
below downgrade, and a reply that actually refutes is honored at any
severity because refutation is judged on the code and is never used up.

The no-slug fallback pointed at nothing. It said to fall back to "the
configured review-app login" without naming a source, which is not
actionable in the one case it exists for. Since there is no reliable way
to tell this agent's threads from another bot's without the slug, that
case now skips the step. harness/review.yaml declares
slug: fullsend-ai-review, so it is always set here.

Not changed, with reasons:

- reactors is correct; the finding has it backwards. `users` is the
  older ReactionGroup field, deprecated in favor of reactors — its own
  deprecation notice says "Use the `reactors` field instead" — and
  hidden from default introspection, which is likely where the
  confusion came from. The query as committed executes cleanly against
  this repo's own review threads, including the threads on this very
  PR. Nothing to fix.
- reviewThreads(last:100) and reactors(first:10) both truncate, and both
  fail closed: a dismissal that is not read is a dismissal not honored,
  and the finding is emitted. The pagination note already says a thread's
  absence must not be read as the absence of a dismissal. Fail-open
  truncation would deserve a fix; this does not.

Signed-off-by: guy oron <goron@redhat.com>
@guyoron1

guyoron1 commented Aug 27, 2026

Copy link
Copy Markdown

Heyyaa Ralph : )

Following up from the review thread — picked this up per your "take over #709 or start it from scratch": #1066.

Your design is the spine, kept nearly intact: the two-tier trust gate, PR-author exclusion, decline keyed to the code still being present, file+category matching, downgrade instead of drop. Added on top: resolve-thread and 👎 count as dismissals (one GraphQL reviewThreads query carries resolution + reactions + replies, none of which REST exposes), a critical carve-out, and disputes get engaged exactly once instead of re-raised verbatim.

One heads-up before you review: the collaborator-permission fallback 403s inside the review sandbox — the endpoint needs push access and the review agent is read-only by design. #1066 fails closed on any lookup error and recovers the common case from associations the same query already returned. Closing it fully needs a second, narrowly-scoped token role plus a runner-side lookup that passes the result in — an infra ask; happy to write the code once the role exists.

Two small text corrections along the way: fullsend#6045 shipped, so the bot identity reads FULLSEND_SLUG instead of a literal, and GraphQL reports bot logins without the [bot] suffix that REST keeps (fullsend#6456 fixed the same mismatch elsewhere).

Qodo and Wayne both did a round on #1066 already — everything addressed in-thread.

If the direction looks right, could you or another maintainer drop ok-to-test on #1066? This one can close whenever you're ready, or keep yours as the base and take mine as patches — either works.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants