Skip to content

pr_review.py reports a Copilot too-many-files refusal as a clean pass #581

Description

@ptr727

pr_review.py reports a Copilot refusal as a clean pass

pr_review.py status|wait treats a review carrying the head SHA as coverage. Copilot delivers its too-many-files refusal as exactly that: a formal review, state: COMMENTED, with the correct commit.oid, whose body is

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

and zero inline threads. So the digest reads:

pr=108 head=9f56a472 rounds=1 review_on_head=yes threads=0 unresolved=0 suppressed=0 answered_outside_review=no requested=no merge=CLEAN

That is byte-for-byte the shape of a genuine clean pass, and wait exits 0. Observed on a real pull request of 301 changed files, which was one file over the limit. The review never happened and the pull request was one command from merging on the strength of it.

Why the existing guard does not cover it

scripts/README.md reasons carefully about the adjacent case and exit 40: "a comment carries no commit, so it satisfies no coverage check whatever it says". That reasoning assumes a refusal arrives as a comment. This one arrives as a review, with a commit, so it satisfies every coverage check.

The 40 documentation also states the useful principle, that the test is the shape of the answer rather than its cause, and that a refusal is terminal because no review follows it and re-requesting does not clear it. All of that applies here: re-requesting is futile, because the file count has not changed. Only the delivery mechanism differs, and it is the mechanism the guard keys on.

It also interacts badly with the documented rule that "a review with no inline comments is still a completed review ... the clean-pass success case". That rule is correct and is exactly what makes this indistinguishable without reading the body.

Suggested handling

Treat a review whose body matches a refusal as a terminal error, not coverage. A distinct exit code alongside 40 would fit the existing vocabulary, and the digest should print the body whole, as 40 already does, because the wording is what separates a file-count refusal (fix by splitting the pull request) from a quota or rate-limit refusal (fix by waiting) from an ordinary empty review.

Matching on the body is unavoidable here, and the README's own guidance for the suppressed-findings matcher applies: match an alternation rather than one phrasing, and have a case assert the script's pattern is the one the runbook publishes rather than a copy that can drift.

Worth noting the reviewer-side fact for the docs too, since it is what an agent hitting this needs to know: the limit is 300 changed files, and the answer is to split the pull request rather than to retry. A repo committing binary data alongside code crosses it easily.

Second, smaller issue: --repo defaults to this repository

ap.add_argument('--repo', default='ptr727/ProjectTemplate'). A downstream run that omits it silently digests this repository's pull request of the same number and reports a state belonging to a different repo entirely. My first run reported threads=1 unresolved=0 while the repo I was actually working in had three unresolved threads, and nothing in the output indicated the mismatch.

Given the script's own standard, that a wrong answer is worse than a crash, defaulting to the current checkout's remote (or requiring the flag) would be safer than defaulting to a fixed repository.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions