Enforce cross-model review coverage on AI-authored PRs - #2552
Merged
Merged
Conversation
The review-coverage check moves from report to enforce, and to a trusted host so enforcement means something: it runs on pull_request_target and checks out the base commit, so a PR cannot edit the check that gates it. Nothing from the PR is executed — coverage reads the event payload and the file list comes from the API. Two changes to what it measures: AI-authorship is now detected by generator signature as well as by the HEG footer fields. Over the last two weeks of harper and harper-pro PRs, 14 of the 15 member PRs carrying no HEG field carried "Generated with Claude Code", so keying on the fields alone exempted most of the population the gate is for. `Complexity: easy` waives the second review leg on a narrow diff — at most 150 changed lines and 10 files, and at least one outside review still reported. The diff has to corroborate the claim: the field is written by the agent grading its own work, and pr-conventions guarantees it may only raise the diff-derived grade, so letting `easy` waive a gate unconditionally would invert that incentive. The `@ <sha>` pin on the footers is reported but never gated on. The question is whether two outside models looked at the change, not whether the footer was re-materialized after the last amend. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Code Review
This pull request introduces a "Complexity: easy" waiver mechanism to the review coverage GitHub Action, allowing AI-authored PRs to waive one required review leg if the diff is small (based on customizable line and file thresholds). It also enhances AI-authorship detection, blanks fenced code blocks to prevent self-matching, and adds comprehensive tests for these features and workflow security. There are no review comments to address, and the implementation aligns with the repository's style guidelines.
Contributor
|
Reviewed; no blockers found. |
dawsontoth
approved these changes
Sep 10, 2026
kriszyp
added a commit
to HarperFast/harper-pro
that referenced
this pull request
Sep 10, 2026
…r intact The enforce flip rewrote this file's header and dropped two things harper-pro's main has carried since #706: the job's `name:`, which is the published check context (`review-coverage` — without it the context becomes the job id, `coverage`), and the pin's trailing `# main` comment. Renovate's github-actions manager takes the tracked ref from that comment and needs a single bare token, so the prose that replaced it left the pin as an unversioned reference: never tracked, never bumped. Both are restored. The header keeps the new no-checkout invariant and states what the check actually validates, which the flip had replaced with Actions mechanics, and records why `edited` belongs in the trigger list. Merge ordering against HarperFast/harper#2552 moves to a `Depends-on:` marker in the PR body, which holds the required `companion-check` status pending until the companion merges, rather than to a pin comment nothing enforces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
kriszyp
added a commit
to HarperFast/harper-pro
that referenced
this pull request
Sep 11, 2026
* Enforce cross-model review coverage on AI-authored PRs harper-pro had no review-coverage check at all, so the two-cross-model-review policy was only visible on harper. Pin harper's public copy of the action by sha and run it in enforce mode. Coverage only: format_mode stays off, so this reports and gates the `Review-Coverage:` footer without also gating description structure. Runs on pull_request_target with a base-ref action pin, so a PR cannot edit the check that gates it; nothing from the PR is executed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * Keep the review-coverage check's published context and Renovate marker intact The enforce flip rewrote this file's header and dropped two things harper-pro's main has carried since #706: the job's `name:`, which is the published check context (`review-coverage` — without it the context becomes the job id, `coverage`), and the pin's trailing `# main` comment. Renovate's github-actions manager takes the tracked ref from that comment and needs a single bare token, so the prose that replaced it left the pin as an unversioned reference: never tracked, never bumped. Both are restored. The header keeps the new no-checkout invariant and states what the check actually validates, which the flip had replaced with Actions mechanics, and records why `edited` belongs in the trigger list. Merge ordering against HarperFast/harper#2552 moves to a `Depends-on:` marker in the PR body, which holds the required `companion-check` status pending until the companion merges, rather than to a pin comment nothing enforces. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This was referenced Sep 12, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The
review-coveragecheck moves from report to enforce, and to a trusted host so that enforcement means something: it runs onpull_request_targetand checks out the base branch, so a PR cannot edit the check that gates it. An AI-authored, non-draft, member PR changing more than two lines now goes red unless its description reports two distinct outside-model review families.For the human reviewer
agy(Gemini) login: 28 of the 52 sub-threshold AI PRs reportblocked=gemini(auth)or a sibling failure. This is deliberate — you asked for red as the forcing function to get people authenticated — but it is a large first-day number and it is the thing to overrule if it is too much. Reverting ismode: report.Review-Coverage:footer satisfies enforce. A hand-written## Review coveragesection is still counted and shown, but never accepted: it is written from memory, and it cannot exclude the authoring family when the body carries no generator signature, so a Claude PR listing "Claude: clean" scored itself. Only 1 of the 163 PRs relies on prose, so the blast radius is small — but there is no deprecation window.skills-internalgets a red check they cannot self-service from the tooling. Mitigated by putting the footer's grammar in the check text and the README so it can be written by hand; the alternative is enforcing inharper-profirst and leaving core at report.easywaiver is size-only here, deliberately. The pre-push gate additionally refuses the waiver when the diff touches a risk surface; CI cannot compute that without the file list, whichharper-prodoes not collect. So a 149-line change tosecurity/waives its second lens in CI. The caps are action inputs, so tightening is config; removing the waiver is not.ref: base.ref, notbase.sha. The action tracks the base branch tip rather than the commit the PR forked from, so a long-lived PR gets the current check rather than a stale one — at the cost of the action moving under it. One line either way.Changes
Trusted host.
.github/workflows/review-coverage.ymlswapspull_requestforpull_request_targetand checks out the base branch ref, never the PR head, withmode: enforceon the action. No PR code executes: coverage reads the event payload and the file list comes from the API. Permissions stay read-only. A new test inprFormat.test.mjsasserts that pairing directly — drop theref:line and the job would check out fork-PR head and run attacker-suppliedci-review-coverage.mjsunder the base repo's token, and nothing else in the suite would notice.AI-authorship detection.
isAiAuthoredinprExemption.mjsrecognizes aComplexity: easy|medium|complicatedgrade and the two footer fields, or a line beginningGenerated with/by <tool>or aCo-Authored-By:trailer naming one. The signature forms are line-anchored so ordinary prose does not gate a hand-written PR, and they do not route throughgeneratorFamily, which returns nothing unless exactly one family matches — "Generated with Cursor Grok" names two and must still count.Complexity:requires a real grade, so an index PR whose description saysComplexity: O(n log n)is not classified as an agent PR.Fence handling. Both markers-readers now share
stripFencedBlocksfromprFormatLinks.mjs— the coverage evaluator strips fences the same way —, which normalizes CRLF and handles~~~, 4+-backtick, indented and blockquoted fences. The hand-rolled three-backtick regex it replaces was a no-op on the CRLF bodies GitHub's web editor produces, so a PR documenting this convention had its own example read as live coverage.Last-wins, consistently. The coverage footer and the
Complexity:field are both read last-first, matchingHuman-Review-Need. Readingcountoff the prose scan's first match while gating on the last footer produced a red check whose summary said two reviews ran, and a green one whose detail said none did. A footer with no recognizedauthored=is not counted at all — without it the parser cannot exclude the authoring family, soran=claude,codexon a Claude PR scored two.The waiver.
easyDiffWaivercorroboratesComplexity: easyagainst the diff: at mosteasy_max_lines(150) andeasy_max_files(10), both failing closed when the measurement is missing, and it names which cap bound. It waives one leg rather than all but one, so a consumer settingrequired: 3still gets two. The caps are action inputs; setting either to0disables the waiver. The remediation text and the README both carry the footer's grammar, so an org member without the private skills checkout can act on a red check.Verification
Route: unit tests over the evaluator and the CLI entry point, plus a replay of the real PR population. This workflow cannot check itself — swapping the trigger means
pull_requestis no longer registered for this head and the base branch's copy still sayspull_request, so no run appears until after merge.mainhas no required status checks, so nothing waits on it.node --test .github/actions/review-coverage/*.test.mjs— 101/101, with 20 new tests covering two live footers in both orders, the authorless footer, the last-Complexity:-wins case,Complexity: O(n log n)staying human-authored, the waiver arithmetic atrequired: 3, both fence-form bypasses, CRLF bodies, and theINPUT_EASY_MAX_*wiring.npx prettier --checkandnpx oxlinton every changed file — clean. Prettier caught a real break: an unquoted`Complexity: easy`inside a YAML description madeaction.ymlunparseable.evaluateCiCoverageandclassifyPullRequestatmode: enforceover all 163 harper and harper-pro PRs created since 2026-08-26, fetched from the API with realadditions/deletions/changed_files/author_association. Result is the 61-red breakdown in item 1; theeasywaiver rescues 13. Script kept at~/dev/scripts/review-coverage-audit.mjs.Three rounds of cross-model review ran on this branch. Round 2's major was the weak fence regex; round 3 found no majors. Remaining minors are the ledger items above.
Complexity: complicated
Review-Coverage: authored=claude; ran=gemini,cursor-grok,codex; adjudicated=domain; declined=cursor-composer; rounds=3 @ 61c53b0
Human-Review-Need: 4 @ 61c53b0