Skip to content

Block the Review Loop on a Shape the Reader Does Not Know - #608

Merged
ptr727 merged 7 commits into
developfrom
feat/review-coverage-of-the-diff
Aug 8, 2026
Merged

Block the Review Loop on a Shape the Reader Does Not Know#608
ptr727 merged 7 commits into
developfrom
feat/review-coverage-of-the-diff

Conversation

@ptr727

@ptr727 ptr727 commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Closes #607.

What was wrong

A Copilot review body states how many of the pull request's changed files it read, and nothing parsed that line. A round that read part of the diff carries the correct commit.oid, raises no inline threads, and reports "generated no comments", so it is the clean pass byte for byte in everything the loop checks, and status printed review_on_head=yes over it and exited 0.

Measured over 332 Copilot review bodies on this repository, five rounds across three pull requests reported reading fewer files than were changed, and all three merged. #592 is the sharpest: three changed files, one never read, across both rounds, both reporting no comments.

What changed, and why it is wider than the issue asked

Partial coverage is the third instance of a shape this script already answers twice, and the generalization is the point. Every reader here keys on a structural marker, so a marker that changes spelling is a section the reader stops finding and reports as absent. All three failures on record have that shape:

Drift Result
Suppressed heading reworded suppressed=0 over a body carrying findings
Suppressed section moved inside another wrapper suppressed=0 again
Coverage line never parsed a partial round reported as a covered head

Each was found by the maintainer after it had landed, rather than by the gate. So the digest now vets the reviewer's output as a whole and fails closed:

  • coverage=full / PARTIAL / unstated, with exit 42 on a partial round.
  • shapes=ok / UNRECOGNIZED, with exit 43 on any heading, <summary>, metadata label, coverage wording or reviewer login the script has no vetted spelling for. It outranks 42, because a reader that does not understand the output cannot be believed about what it read of the diff.

The 43 message states the remedy in two parts: file an issue on the repository hosting the reader, quoting the body the shape came from, and the merge decision is the maintainer's. An unrecognized shape does not say the pull request is bad, only that nothing here can vouch for the review of it.

The inventory is measured, not imagined

With fenced blocks dropped and text reduced to ASCII, all 332 bodies reduce to 7 headings, 6 <summary> texts and 3 metadata labels, and every body carries at least one. Counts normalize to (N) and the verdict headings' colored circle is dropped before comparing, since both change on every review without the section changing, and dropping the emoji is also what keeps the source inside the charset rule.

Two exemptions, both required by the corpus:

  • A body stating no coverage reads as unstated, never as pass or failure. 28 of the 332 are an overview and a change list, that shape is current and interleaves with the counted one, and one pull request carries both across its two rounds. Failing on it would cry wolf on about one review in twelve.
  • A refusal is exempt, being a bare paragraph by design and already classified. The exemption is the pattern rather than a carve-out, so a refusal reworded stops being exempt and blocks, which is the refusal check's own failure mode caught one rewording later.

The quietest reading is the reviewer login: a rename leaves every filter here matching nothing, so a review that landed reads as rounds=0 and a wait polls out its timeout against it.

Evidence

Both readers were swept over the full corpus before this was written, and over it again after:

166 tests pass, up from 132. The two fixtures the issue named as unasserted filler are promoted to assertions, and a case reads the vetted coverage spellings out of the runbook and hands them to the script's own parser, so the pair fails in both directions on drift. The old fixtures crafted review bodies with no heading, which no real body has, so they were made realistic rather than the check loosened.

Also carried

  • .github/copilot-instructions.md - the verify step checked commit.oid only, which is what this shows to be insufficient, plus a new section stating that an unrecognized shape blocks and earns an issue.
  • GOVERNANCE.md - merge-gate precondition 4, and the "all four preconditions" restatement swept to five.
  • scripts/README.md - both readings.
  • TODO.md - a measure cluster to survey whether GitHub publishes anything structured about a Copilot review, since this whole design rests on prose being the only surface. Introspection says it is: PullRequestReview exposes body, bodyText and bodyHTML and no field naming a finding or a file count, and the only Copilot-named GraphQL types configure review-on-push in a ruleset.

The accepted cost

The shape scan reads every round rather than the head's, so once Copilot changes format, every open pull request blocks at once until the inventory is updated. That is deliberate and the maintainer's call: it is well balanced against a reviewer silently missing a raised finding.

🤖 Generated with Claude Code

A Copilot review states how many of the changed files it read, and nothing
parsed that line. A round that read part of the diff carries the right
commit, raises no threads, and reports "generated no comments", so it is the
clean pass byte for byte in everything the loop checks. Measured over 332
review bodies on this repository, five such rounds landed across three pull
requests and all three merged.

The line is the third instance of a shape already answered twice here, and
generalizing is the point: every reader keys on a structural marker, so a
marker that changes spelling is a section the reader stops finding and
reports as absent. The suppressed heading was reworded and the count went to
zero, the section then moved inside another wrapper and it went to zero
again. Each was found after it had landed rather than by the gate.

So the digest now vets what the reviewer sent against an inventory measured
from those bodies, seven headings, six summaries and three metadata labels,
and blocks on anything outside it, a body with no heading and a reviewer
login that is not the one every query filters on among them. The remedy is
stated as filing an issue where the reader lives, and whether to merge
regardless is the maintainer's decision rather than the agent's.

Closes #607

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 8, 2026 01:06

Copilot AI 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.

Pull request overview

Tightens the repository’s Copilot review gate by teaching scripts/pr_review.py to (1) detect partial file coverage in a formal review and (2) fail closed when the reviewer output contains unrecognized structural markers, so the review loop cannot silently pass on a misread review body.

Changes:

  • Extend scripts/pr_review.py to parse per-round “files reviewed” coverage lines, emit coverage=... in the digest, and return blocking exit codes for partial/unvetted coverage and unrecognized shapes.
  • Add/expand unit tests to cover full/partial/unstated coverage, shape inventory enforcement, and new exit-code behavior for status/wait.
  • Update governance/runbook documentation to incorporate the new merge-gate precondition and the “block on unknown shapes” remedy.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
TODO.md Adds a “measure” backlog cluster to survey whether structured (non-prose) Copilot review data exists.
scripts/test_pr_review.py Adds coverage/shapes test coverage and new exit-code assertions for status/wait.
scripts/README.md Documents the new coverage and “unrecognized shapes” behaviors/exit codes.
scripts/pr_review.py Implements coverage parsing, shape inventory checks, digest fields, and new blocking exit codes.
GOVERNANCE.md Updates the merge gate to require tooling-recognized reviewer output shapes.
.github/copilot-instructions.md Updates the review runbook with coverage-of-diff guidance and the “unrecognized shape blocks” procedure.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread .github/copilot-instructions.md Outdated
Comment thread scripts/README.md
Comment thread scripts/test_pr_review.py Outdated
Three findings from round 1, each correct. The digest renamed the no-count
coverage state from `unknown` to `unstated` so it could not be read as the
separate `shapes` field, and the runbook and the script README were both
written before that rename and still named the old value. The runbook also
described 43 as a coverage code, where it is now the unrecognized-shape code
that an unreadable coverage wording exits through.

One test signature dropped its `-> None` to fit the line, so the name is
shortened rather than the annotation left off.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 8, 2026 01:11

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (3)

scripts/pr_review.py:453

  • coverage_statements() currently requires the literal substring "changed file" even for the - **Files reviewed:** ... spelling. If GitHub keeps the Files reviewed bullet but drops the trailing "changed files" text (a plausible wording drift), this function will silently ignore the coverage line and the run will fall back to coverage=unstated instead of failing closed / reporting an unrecognized coverage shape.
def coverage_statements(body: str) -> list[str]:
    """The lines this round states its file coverage on, quotations excluded."""
    return [ln.strip() for ln in FENCE.sub('', body or '').splitlines()
            if COVERAGE_OPENS.match(ln) and 'changed file' in ln.lower()]

scripts/pr_review.py:462

  • read_coverage() returns parsed counts without validating basic invariants. If the line parses but the counts are inconsistent (e.g. "reviewed 8 out of 7"), coverage_of() currently treats that as FULL due to >=, which fails open on an obviously invalid coverage statement. This should instead be treated as an unvetted/unrecognized coverage line so the run blocks (exit 43).
    m = COVERAGE_COUNTS.search(line)
    if not m:
        return None
    reviewed, changed = (m.group(1), m.group(2)) if m.group(1) else (m.group(3), m.group(4))
    return int(reviewed), int(changed)

scripts/pr_review.py:95

  • COVERAGE_COUNTS only matches "changed files" (plural). If the reviewer emits the grammatically singular form (e.g. "1 out of 1 changed file"), read_coverage() will treat it as unvetted and block the review loop even though the meaning is unchanged.

This issue also appears in the following locations of the same file:

  • line 450
  • line 458
COVERAGE_COUNTS = re.compile(
    r'reviewed\s+(\d+)\s+out of\s+(\d+)\s+changed files'
    r'|\*\*Files reviewed:\*\*\s*(\d+)\s*/\s*(\d+)\s+changed files', re.IGNORECASE)

Round 2's suppressed findings, each correct and each the same shape as the
bug this change exists to fix, one level down in the reader itself.

The bullet spelling required the words after its counts, so a bullet that
dropped them read as no statement at all and the round reported `unstated`
rather than blocking. Its label is the marker, so the two openers now carry
different text requirements: the bullet needs none, and the sentence keeps
one because prose opens a line with the reviewer's name too.

A pair of counts that cannot both be true read as full coverage through a
`>=`. A round claiming it read more files than the pull request changed is
one this script is parsing wrongly, so it now reads as unvetted and blocks,
which is what every other unreadable wording does.

The counts required the plural, so a one-file round saying `1 changed file`
would stop the fleet over a grammatical agreement carrying no meaning. The
opener already accepted the singular, so the two disagreed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 8, 2026 01:18
@ptr727

ptr727 commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

Round 2: three suppressed findings, all three correct, all three fixed in cf2b892

Answered here rather than in a thread, since a suppressed finding reaches none. Each one is the same shape as the bug this pull request exists to fix, one level down in the reader itself, which is a fair thing for the round to have caught.

1. coverage_statements() required the literal "changed file" even for the - **Files reviewed:** spelling.

If GitHub keeps the Files reviewed bullet but drops the trailing "changed files" text (a plausible wording drift), this function will silently ignore the coverage line and the run will fall back to coverage=unstated instead of failing closed.

Reproduced before fixing: coverage_statements('- **Files reviewed:** 4/4') returned [], and the vetted-label check does not catch it either, since Files reviewed is itself a vetted label. So the round would have reported coverage=unstated over a round that stated its coverage plainly.

The two openers now carry different text requirements, because the text each needs beside it differs. The bullet's own label is the marker, so it qualifies whatever follows the counts. The sentence opener is the reviewer's name, which prose also opens a line with, so that one keeps the requirement.

2. read_coverage() validated no invariant, so "reviewed 8 out of 7" read as FULL through the >=.

This should instead be treated as an unvetted/unrecognized coverage line so the run blocks (exit 43).

Correct, and the >= was a deliberate simplification that was simply wrong. A round claiming it read more files than the pull request changed is not a round that read them all, it is a line this script is parsing wrongly, and reading it as full coverage fails open on the very statement saying something is off.

read_coverage() now returns None for a pair that cannot both be true, which is one point of truth rather than two: coverage_of() reads it as UNVETTED and unrecognized_in() lists it, so the same line takes the same remedy on both paths. The comparison in coverage_of() is == accordingly.

3. COVERAGE_COUNTS required the plural "changed files".

If the reviewer emits the grammatically singular form (e.g. "1 out of 1 changed file"), read_coverage() will treat it as unvetted and block the review loop even though the meaning is unchanged.

Correct, and this one is the cry-wolf direction rather than the fail-open one, which makes it worth naming separately: a one-file round saying 1 changed file means exactly what it says, and stopping every open pull request in the fleet over a grammatical agreement is the guard an agent learns to work around. The opener already accepted the singular through a substring test, so the two halves of the reader disagreed with each other. Both now read changed files?.

Evidence. All three reproduced against the code before the fix and verified after. The corpus sweep is unchanged at 0 unrecognized items across all 332 review bodies and full 299 / unstated 28 / partial 5, so none of the three widened anything into a false positive. Four cases were added, one per finding plus one holding the sentence opener to its text requirement, and the suite is at 170.

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

scripts/pr_review.py:103

  • The bullet form is treated as a coverage line based on its label alone (see the comment above COVERAGE_BULLET and is_coverage_line()), but COVERAGE_COUNTS currently requires the trailing "changed file(s)" words for the bullet alternative. If GitHub ever emits a shorter bullet like - **Files reviewed:** 4/4 (no suffix), this will be classified as an unrecognized coverage line and block the loop unnecessarily.

Consider parsing the X/Y counts for the bullet form even when the trailing words are missing, while still requiring the words for the sentence form to avoid false positives.

COVERAGE_COUNTS = re.compile(
    r'reviewed\s+(\d+)\s+out of\s+(\d+)\s+changed files?'
    r'|\*\*Files reviewed:\*\*\s*(\d+)\s*/\s*(\d+)\s+changed files?', re.IGNORECASE)

Round 3, and the finding is the complement of the previous round's fix. The
bullet became a coverage line on its label alone, correctly, but the counts
pattern still required the words after them, so a bare `4/4` was detected
and then failed to parse. That turned a silent `unstated` into a block on a
line the script can read perfectly well, which is the cry-wolf direction
rather than a fix.

The trailing words are now optional for the bullet, for the same reason its
label identifies the line, and required for the sentence, whose words are
what separate it from prose. What is left blocking is a bullet carrying no
counts, which genuinely states no coverage.

The case that missed this asserted detection on the bare form and parsing
only on the suffixed one, so it now asserts both on all three spellings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 8, 2026 01:23
@ptr727

ptr727 commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

Round 3: one suppressed finding, correct, fixed in f604f65

The bullet form is treated as a coverage line based on its label alone [...] but COVERAGE_COUNTS currently requires the trailing "changed file(s)" words for the bullet alternative. If GitHub ever emits a shorter bullet like - **Files reviewed:** 4/4 (no suffix), this will be classified as an unrecognized coverage line and block the loop unnecessarily.

Correct, and it is the exact complement of what round 2 asked for and I half-delivered. Making the bullet a coverage line on its label alone moved the failure from a silent unstated to a block, rather than making the line readable. Reproduced before fixing:

detected: ['- **Files reviewed:** 4/4']
parsed  : None
verdict : ('unvetted', '- **Files reviewed:** 4/4')

Blocking there is the cry-wolf direction: 4/4 is unambiguous, and stopping every open pull request over a suffix the script does not need is the guard an agent learns to work around. The trailing words are now optional for the bullet, for the same reason its label identifies the line, and stay required for the sentence, whose words are what separate it from prose.

The class of bug is worth naming, since it is the third instance in this pull request: detection and parsing keyed on different things, so a line one half accepted the other half rejected. After this the two disagree only where the line genuinely carries no readable count, and a bullet like - **Files reviewed:** all of them still blocks, correctly.

The case that missed it is the more useful finding. It asserted detection on the bare form and parsing only on the suffixed form:

self.assertEqual(['- **Files reviewed:** 4/4'], pr_review.coverage_statements('- **Files reviewed:** 4/4'))
self.assertEqual((4, 4), pr_review.read_coverage('- **Files reviewed:** 4/4 changed file'))

Two assertions written as if they covered one thing, split across two different inputs, so the gap sat exactly between them. It now asserts detection, parsing and the verdict across all three spellings, plus a case holding the no-counts bullet to blocking.

Corpus sweep unchanged: 0 unrecognized items across all 332 bodies, full 299 / unstated 28 / partial 5. Suite at 171.

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

.github/copilot-instructions.md:173

  • This exemption bullet calls the no-coverage shape "unknown", but the tooling/docs in this PR (and scripts/pr_review.py status) use the literal coverage=unstated for that case. Using a different term here makes it harder to map the runbook guidance to the actual digest output.
- **Exempt: a body stating no coverage at all.** 28 of those 332 bodies are an overview and a change list and nothing more. That shape is current, interleaves with the counted one throughout, and one pull request carries both across its two rounds, so treating it as a failure cries wolf on about one review in twelve and teaches an agent to work around the gate. It reads as **unknown**, never as a pass and never as a failure.

Round 4 found the exemption bullet in the runbook still calling the state
"unknown" where the digest prints `coverage=unstated`. Round 1 raised the
same mismatch two lines below it and the sweep that answered it stopped at
the line the finding named.

The constant carried the mismatch too. `UNKNOWN` printed as `unstated`
through the field map, so the name in the source and the word in the output
were two different words for one state, which is what the field was renamed
to avoid in the first place. It is `UNSTATED` now, and the field map is back
to being about casing alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 8, 2026 01:30
@ptr727

ptr727 commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

Round 4: one suppressed finding, correct, fixed in b538613

This exemption bullet calls the no-coverage shape "unknown", but the tooling/docs in this PR (and scripts/pr_review.py status) use the literal coverage=unstated for that case.

Correct. Round 1 raised the same mismatch two lines below this one, and the sweep that answered it stopped at the line the finding named instead of grepping the term. That is the recurring failure in this repository's own rules, so it is worth stating plainly rather than quietly fixing: a finding names an instance, and the fix is for the term.

Grepping unknown across the four documents this pull request touches turns up exactly one more stale use, this one, now reading `coverage=unstated` in the literal the digest prints. The other hits are unrelated and correct: answered_outside_review=unknown is a different field that really does print that word, charset-unknown is another gate's finding name, and commit unknown is the suppressed block's marker for a null commit.

The constant carried the same mismatch and is worth more than the doc line. UNKNOWN = 'unknown' printed as unstated through COVERAGE_FIELD, so the name in the source and the word in the output were two different words for one state, which is precisely what renaming the field was meant to stop. Anyone reading coverage_of() had to map one onto the other. It is UNSTATED = 'unstated' now, and COVERAGE_FIELD is back to being about upper-casing the two blocking states and nothing else.

Corpus sweep after the rename: 0 unrecognized items across all 332 bodies, full 299 / unstated 28 / partial 5. Suite at 171, prose gate, markdownlint and editorconfig-checker all clean.

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (4)

TODO.md:322

  • Same issue as above: [scripts/pr_review.py][scripts] resolves to ./scripts/README.md (the [scripts] reference), not to scripts/pr_review.py. Using a code span for the file path avoids a misleading link.
  - **Checked** - `develop` at `20916ad` on 2026-08-07, reading the live GraphQL schema by introspection and one review over REST, against the reader in [`scripts/pr_review.py`][scripts].

TODO.md:317

  • The reference label [scripts] points to ./scripts/README.md, so linking scripts/pr_review.py with [scripts] is misleading (the link text looks like the file path but resolves to the README). Use an inline code span for the file path and keep [scripts] as the separate reference to the scripts README.

This issue also appears on line 322 of the same file.

One pull request, after a survey, deciding whether anything stands between this fleet's review loop and the raw prose of a Copilot review. Today [`scripts/pr_review.py`][scripts] reads the review body as text and holds a vetted inventory of the headings, collapsed sections, metadata labels and coverage wordings it recognizes, blocking on anything it does not. That design is correct for a prose surface and it carries a cost the maintainer has accepted deliberately: a wording change at GitHub blocks every open pull request in the fleet at once, until the inventory is updated. The cost is worth paying against a reviewer silently missing a raised finding, which is the failure it replaces, but it is worth paying only for as long as prose is the only surface on offer.

**State** `measure`. **Touches** [`scripts/pr_review.py`][scripts] and the runbook section in [`.github/copilot-instructions.md`][copilot-instructions], once the survey says whether there is anything to move to. **Cost** a survey first, then either nothing or a rewrite of the reading layer, which is the larger of the two outcomes and the reason the survey comes first.

scripts/test_pr_review.py:659

  • This test name still uses unknown even though the implementation and assertions use the coverage=unstated spelling. Renaming the test improves consistency and avoids confusion with the separate answered_outside_review=unknown state.
    def test_a_round_stating_no_coverage_at_all_is_unknown_rather_than_either_verdict(self) -> None:

scripts/test_pr_review.py:704

  • This docstring uses "Unknown" for the no-coverage case, but the code models that state as coverage=unstated. Using the same term as the digest output makes the test intent clearer.
        """Unknown is the absence of a statement rather than a bad one, so it loses to a count."""

Round 5. The backlog entry linked `scripts/pr_review.py` through the
`[scripts]` label, which resolves to `scripts/README.md`, so the link text
named one file and the link went to another. The reader that follows it
lands on the README looking for a reader that is not there. Those two are
code spans now, and the one place the entry does mean the README keeps the
label.

Two test names still spelled the no-count state `unknown`, which is the
rename from the round before reaching the cases it also described.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 8, 2026 01:36
@ptr727

ptr727 commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

Round 5: four suppressed findings, all correct, all fixed in 2c3e12d

1 and 2. [scripts/pr_review.py][scripts] resolves to scripts/README.md.

The reference label [scripts] points to ./scripts/README.md, so linking scripts/pr_review.py with [scripts] is misleading (the link text looks like the file path but resolves to the README).

Correct, and worth more than a formatting nit: the link text names one file and the link goes to another, so a reader following it lands on the README looking for a reader that is not there. Both are code spans now. The one place the entry does mean the README, the Open bullet about the standard-library constraint, keeps the label, since that one is about what scripts/README.md records.

3 and 4. Two test names and a docstring still spelled the state unknown.

This test name still uses unknown even though the implementation and assertions use the coverage=unstated spelling.

Correct. This is the previous round's rename reaching the cases that describe the state rather than assert on it, which the grep behind that round did not cover because it was scoped to the documents. grep -in unknown scripts/test_pr_review.py now returns only the unrelated and correct uses: the answered_outside_review=unknown cases, the window-blindness cases, and the commit unknown marker for a null commit on a suppressed block.

Three of the last four rounds have found an instance of this same rename, each in a surface the previous sweep did not cover, which says the sweep kept being scoped to where the finding pointed rather than to the term. Recorded here because it is the repository's own documented failure mode and the record is worth more than the four one-word fixes.

Corpus sweep unchanged: 0 unrecognized items across all 332 bodies, full 299 / unstated 28 / partial 5. Suite at 171, prose gate, markdownlint and editorconfig-checker clean.

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

scripts/pr_review.py:1211

  • wait only calls report_verdict(final) when reviewed_head(final) is true. That prevents the new unrecognized-shape gate (exit 43) from ever triggering in cases where the review did land but reviewed_head stays false (e.g. reviewer-login drift, which unrecognized_shapes() is explicitly meant to catch). This can make wait time out and exit as PENDING/30 instead of exiting 43 as documented.
    if reviewed_head(final):
        # Coverage of the head is what this returns 0 on.
        # Coverage of the diff is a second reading it used to take on trust.
        # A round can carry the head and have read only part of it.
        return report_verdict(final)

Round 6, and the finding is the best of the run. `wait` called the verdict
only where `reviewed_head` was true, and `reviewed_head` filters on the
reviewer's login, so a renamed login left it false. The one drift the login
check exists to catch was therefore the one case where the check could not
reach an exit code: the digest printed `shapes=UNRECOGNIZED` and the wait
returned 30, which is the digest disagreeing with the code, and a reader
settles that by believing the code.

The verdict is no longer gated on that reading. The loop stops on a drifted
login too, since no amount of polling makes a login this cannot match
appear, and the liveness query already carries the authors, so the reading
costs the loop nothing. Without that the wait would have run its whole
timeout out against a review sitting in plain sight, which is the failure
the source comment beside it claims to prevent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 8, 2026 01:44
@ptr727

ptr727 commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

Round 6: one suppressed finding, correct, and the best of the run. Fixed in 467ade5

wait only calls report_verdict(final) when reviewed_head(final) is true. That prevents the new unrecognized-shape gate (exit 43) from ever triggering in cases where the review did land but reviewed_head stays false (e.g. reviewer-login drift, which unrecognized_shapes() is explicitly meant to catch). This can make wait time out and exit as PENDING/30 instead of exiting 43 as documented.

Correct, and it is the sharpest kind of finding: the gate could not fire for the one drift it was written to catch. reviewed_head filters on the reviewer's login, so a renamed login leaves it false, and gating the verdict behind it meant the login check was structurally unreachable. Reproduced against a payload whose only difference is the login:

shapes seen  : ['reviewer login: copilot-code-review-agent, where every query here filters on copilot-pull-request-reviewer']
reviewed_head: False
wait exit    : 30
digest said  : ... review_on_head=NO coverage=unstated shapes=UNRECOGNIZED ...

The digest printing shapes=UNRECOGNIZED while the code returns 30 is the digest disagreeing with the exit code, which this same file already names as its own failure mode, in the comment above the final read: "a reader resolves that by believing the code, dropping the review it was just shown." The gate had reintroduced it one branch further down.

The verdict is no longer gated on that reading: if unrecognized_shapes(final) or reviewed_head(final). After the fix, exit 43 with the status line.

The loop had a second half of the same bug. With the exit code fixed, a drifted login still polled the full 45-minute timeout before reaching it, because done can never become true. That would have made the source comment beside READS_AS_REVIEWER false about its own code, since it claims a drift is what leaves "a wait polling out its whole timeout against a review sitting in plain sight". reviewer_login_drift() is factored out and read from the authors alone, so the liveness query answers it and the loop terminates on it at no extra call. Verified: with --timeout 600 the wait now exits 43 with slept.call_count == 0.

Three cases added: the drift reaching 43 without polling, the same gap through the body reader on a round covering no head, and an ordinary pending round still returning 30, so the gate cannot swallow the common case. Suite at 174.

Corpus sweep unchanged: 0 unrecognized items across all 332 bodies, full 299 / unstated 28 / partial 5.

Copilot AI 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.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

@ptr727
ptr727 merged commit 530cf71 into develop Aug 8, 2026
7 checks passed
@ptr727
ptr727 deleted the feat/review-coverage-of-the-diff branch August 8, 2026 01:55
ptr727 added a commit that referenced this pull request Aug 8, 2026
… that blocks on what it cannot read (#609)

Promotes 14 commits from `develop`. Merge with a **merge commit** (`gh
pr merge --merge`), never a squash, and **without `--delete-branch`**,
since this pull request's head is `develop` itself.

Closes #607 through the closing keyword already carried in `530cf71`,
which is why it is not repeated here.

## The prose backlog, cleared end to end

`#600`, `#604`, `#605`, `#606` took the tree from **557 findings across
45 files to 0 across 0**, in four batches ordered by surface: snippets,
comments, hub-only Markdown, then the carried files. Each batch measured
the checker's own exemption against the live corpus *before* sweeping,
and twice the measured answer was **"do not change the checker"**, which
is a result of that pass rather than a skipped one.

`#594` added the floor that makes those numbers trustworthy: a
diff-scoped run now asserts what it actually scanned, since a check
whose scan matches nothing reports zero findings and reads exactly like
a pass.

One finding from that work is worth carrying up: an exemption that is
too **loose** produces silence rather than false positives. #519
recorded the governance files as clean; today's checker reports 38
findings against those same files as they stood at the commit that
measured them.

## A review loop that fails closed

`#599`, `#601`, `#602`, `#603` and `#608` are one arc on
`scripts/pr_review.py`, each removing a shape in which the loop reported
a clean pass over a review it had misread:

- **`#599`** removed the shape a reply kept failing in, by taking the
thread's *words* rather than an id, so there is no argument a hand-typed
`PRRT_...` fits in.
- **`#602`** made `claims` resolve what a description points at rather
than what it looks like.
- **`#603`** gave a disproved claim a home the next round reads.
- **`#608`** reads the file-coverage line, and then generalizes: every
reader keys on a structural marker, so a marker that changes spelling is
a section the reader stops finding and reports as absent. The digest now
vets headings, `<summary>` texts, metadata labels, coverage wordings and
the reviewer login against an inventory measured from **332 review
bodies**, and **blocks on anything outside it**, exit `43`, with the
remedy stated as filing an issue on the hub. Whether to merge regardless
is the maintainer's decision.

`GOVERNANCE.md` merge gate went from four preconditions to **five**
accordingly.

## Governance and tooling

- **`#593`** states which checkout an agent works in and what the hub
is, which is the host-wide routing the repositories that most need it
cannot carry.
- **`#596`** gates the pattern-detectable half of the
representative-data rule, honest that no pattern closes the name-shaped
case.
- **`#598`** declares where a repository states what CI cannot verify.
- **`#592`** regrouped `TODO.md` by what ships rather than by what it
touches, so a `###` heading is one pull request.
- **`#601`** ended a `gh push` argument list at a newline rather than
only at `&&`, fixing a write-guard over-block.

## Verification

Run on `develop` at `530cf71` immediately before opening this:
`test_pr_review.py` (174), `test_prose_lint.py`, `test_repo_gate.py`,
`spec/audit.py --selftest`, `gh-write-guard.py --selftest`,
`spec/validate.py`, `repo_gate.py`, the prose gate in both CI
invocations, markdownlint and editorconfig-checker. All clean.

## Not carried by this promotion

- **#519 is complete and still open.** `TODO.md` holds its closing
evidence under "Verified Complete, Awaiting Close". Closing it is the
maintainer's call, so no keyword for it appears here.
- **The re-vendor debt is now nine files.** `#606` queued seven, and
`#608` changed `GOVERNANCE.md` "PR Review Etiquette" (`verbatim`) and
`.github/copilot-instructions.md` (`intent`) on top. The `intent` half
produces no hash and therefore no audit finding, which is why the Fleet
Sweeps entry names those files by hand.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
ptr727 added a commit that referenced this pull request Aug 8, 2026
The branch was parked on 2026-08-06 pending the GitHub Actions outage,
and develop moved 20 commits past it. Six regions conflicted, and one of
them was not a text collision.

Exit 42 now means two things. This branch defined it as the review loop
closed over a check in a shape no wait clears, and #607, #608 and #613
have since defined 42 as a round that read fewer files than the pull
request changed, with 43 as a shape the reader does not know.

The check reading becomes 44, ranked under both. 43 says no field here
can be believed, so it outranks everything. 42 says part of the diff has
no review. 44 says the review itself is sound and a required check is
wedged, which is only worth reporting once the two above are clear, so
`wait` returns report_verdict's code where it has one and reads the
checks only where it does not.

The digest prints both sets of blocks, with the unrecognized-shape block
still first, since it says how far the rest of them can be trusted.

Resolved from develop's text rather than this branch's wherever develop
had edited the same line, which the copilot-instructions bullet needed:
the prose batch replaced its semicolons, and taking this branch's older
copy would have reverted that and failed the gate that now checks it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Aug 8, 2026
`mergeStateStatus` reports `BLOCKED` for a failed check, a required check nothing is running, an unresolved thread, and a missing approval alike, and the digest printed that word and stopped there.

Found by running into it. A run polled `BLOCKED` for twenty-five minutes on a pull request whose only unfinished check was an aggregator job GitHub dispatched and never assigned a runner, and the cause came from the maintainer rather than from any field in the digest. That is the defect: the digest named a state it could not explain.

This is the refusal defect of #584 one gate along. There, a review that covered nothing rendered as coverage. Here, a check nothing is running renders as patience, and both read as a pull request worth waiting on.

## What the digest says now

Reconstructed from the observed job states of the run this hit, rather than described:

```
repo=ptr727/ProjectTemplate pr=590 head=aaaaaaaa rounds=1 review_on_head=yes refusal=no
threads=0 unresolved=0 suppressed=0 (on_head=0 earlier=0) answered_outside_review=no
requested=no merge=BLOCKED checks=2/3 stuck=NOT_PICKED_UP
  CHECK NOT PICKED UP ('Check pull request workflow status job', queued 15m with no runner
  assigned): nothing here starts it, because the runner pool is GitHub-hosted, so re-run the
  workflow or wait on that capacity
```

The rollup rides the existing full query, so reading the checks costs no extra round-trip, which is the reason this script exists at all.

## Three shapes, because each wants the opposite response

- **`NOT_PICKED_UP`** is a job GitHub dispatched and assigned no runner. Read from the queued state rather than from a runner name, which GraphQL does not carry, and the state suffices because a job held behind a `needs:` dependency does not enter the rollup until that dependency finishes, so there is no dependency-blocked queue to mistake for a starved one. Nothing agent-side starts it, since the pool is hosted, so the remedy is a re-run or that capacity, and **not** a re-request, a rebase, or an empty commit.
- **`RUNNING_LONG`** is deliberately the weaker reading and its wording says so, since duration alone cannot separate a hung job from a slow one. This repository's lint job legitimately runs nine to eleven minutes while its aggregator is a single shell conditional, so the threshold is generous, the elapsed time prints for the reader to judge against what the job costs, and nothing asserts a fault.
- **`FAILED`** is a verdict rather than a stuck check, reported so no reader deduces a red check from `BLOCKED`.

## Exit 44, and the boundary that makes it mean something

`wait` gains exit `44` for a review loop that closed against a check in one of those shapes, because `0` was saying the loop closing is the merge gate.

A check **merely still running normally is not 44 and exits 0.** That boundary is the whole design: the wait returns the moment coverage lands, which on almost every pull request is mid-CI, so taking `42` for a pending check would make `42` the ordinary outcome, and a code that fires always carries nothing. The grace is the pickup grace's five minutes for the reason that one is, and the stall is thirty because a fleet repository building and testing .NET runs longer than this one. Both are flags, and a negative value for either is rejected by name rather than rendering a digest that reports every check stuck from its first read.

## Two defects this caught in itself

- A finished check carrying **no conclusion yet** fell through to the unknown-conclusion branch and reported `FAILED`, inventing a red check out of a race in the API. Caught by the suite, which is also what said the first test helper was at fault rather than the code, since it failed eight cases at once.
- **`EXPECTED`** was missing from the unstarted set, so a required status nobody has posted reached that same branch and reported that same false failure. Caught reading the diff back.

Both have cases now. `SKIPPED` and `NEUTRAL` count as passes, since the fleet aggregator pattern skips the conditional jobs and four of the six checks on a green pull request here are skips. An unrecognized conclusion **is** reported, because a new enum member read as a pass is a red check rendering as a green digest.

## Docs

The runbook's `BLOCKED` bullet said the cause is "usually" unresolved review threads, which is true and is exactly what misled the run. The qualifier stays and the counter-case joins it rather than replacing it. `GOVERNANCE.md` merge-gate condition 1 now requires the reason to be **read** rather than inferred, and a new paragraph states that `BLOCKED` is no more self-explaining than `CLEAN` is sufficient, including that hosted-runner capacity is not a reason to weaken a gate.

`GOVERNANCE.md` is verbatim-carried, so a **fleet re-vendor is owed**, and the `TODO.md` re-vendor entry now names these sections.

## Verification

461 cases pass across the three `scripts/` suites, 217 of them in `test_pr_review.py`, which is 41 more than `develop` carries. The count read 291 when this was written and the suites have grown on both sides since, so it is restated against the merge rather than left as a number measured on a tree that no longer exists. `prose_lint.py --diff origin/develop`, `repo_gate.py`, `spec/validate.py`, and `markdownlint-cli2` over all 44 files are clean. A grep for restatements of the changed merge-gate wording across every markdown file found none stale, and the "all four preconditions" sentence still holds since condition 1 was amended rather than added to. The digest was also run live against a real pull request, where it reports `checks=6/6` and no stuck field.

## Renumbered from 42 to 44 on the forward-merge

This branch was parked on 2026-08-06 pending the GitHub Actions outage, and `develop` took exit `42` in the meantime for a round that read fewer files than the pull request changed, with `43` for a shape the reader does not know, both from #607, #608 and #613. The check reading here is `44`, ranked under both: `43` says no field can be believed, `42` says part of the diff has no review, and only once those are clear is a wedged required check the thing worth reporting. `wait` returns `report_verdict`'s code where it has one and reads the checks only where it does not.
ptr727 added a commit that referenced this pull request Aug 8, 2026
Adds the missing half of the selection procedure's integrity check, and amends one cluster with an upstream reference.

## The gap

Step 1 confirms every open **issue** appears in this file. Nothing asked the same of an open **pull request**, so a pull request whose blocker has passed is invisible to the one procedure that would catch it. Nothing selects it, nothing closes it, and `develop` moves underneath it.

[#591](#591) is the worked example and is carried in the step as its evidence. It was parked correctly on 2026-08-06, during a GitHub Actions major outage, with the reason written on the pull request. The reason then expired quietly. Three days later it was 20 commits behind `develop`, conflicting in six regions, and its central exit code had come to mean something else, because #607, #608 and #613 had taken `42` for a different reading in the meantime.

The failure is not specific to this repository. The same shape was reported on Blog, where two pull requests were left open through the same outage and a day of new work landed on top of them.

## The rule

Step 2 asks that every open pull request carries a **stated active blocker**: stated where the pull request itself carries it rather than held in a session that has ended, and active only while the thing it names is still true. A landed review round, a merged dependency and a passed outage each stop being one, and what they leave is a forgotten pull request rather than a parked one. The remedy is to finish it, close it, or write the current blocker down.

## Measured before writing, not after

The lesson this repository keeps relearning about checkers is to measure the live corpus before shipping a rule, so it flags what it is for rather than the routine traffic:

| Reading | Result |
| --- | --- |
| Open pull requests right now | **1** (#591) |
| Dependabot pull requests, open to merged | ~**1 minute** (#611 and #612 both `02:24` to `02:25`) |

The merge-bot takes bot traffic inside a minute, so it never sits long enough to owe a blocker, and the rule's working set is the handful of human pull requests that actually linger.

## Second disposition in this change

**Amends "A Programmatic Reading of a Copilot Review"**. That cluster's open question is whether GitHub publishes anything but prose to read a Copilot review from, and its `Settled` line records that the public API does not. The ask is now filed upstream as [GitHub community discussion 204320](https://github.com/orgs/community/discussions/204320), which requests a versioned machine-readable schema carrying severity, category, suggestion and resolution state. It is unanswered, so the entry records it as a place to watch rather than a dependency to wait on.

## Verification

`prose_lint.py` clean including `sentence-split`, `editorconfig-checker` exit 0, `markdownlint-cli2` 0 issues across 44 files, `TODO.md` at 497 of 497 CRLF lines. The renumbering was checked against the file's own cross-references, and the only one that names a position is step 1 calling itself first, which it still is.
ptr727 added a commit that referenced this pull request Aug 8, 2026
…request sweep (#619)

Promotes `develop` to `main`, carrying two merged pull requests.

## What is being promoted

- **[#591](#591
`07ed74a`, reading why a merge is blocked instead of reporting one word.
`scripts/pr_review.py` gains the check rollup, the four stuck shapes it
tells apart, and exit `44` for a review loop that closed against a check
no waiting clears, plus the [`GOVERNANCE.md`](./GOVERNANCE.md) and
runbook wording that says `BLOCKED` never names its own cause.
- **[#618](#618
`dd5fc90`, the open pull request sweep in [`TODO.md`](./TODO.md)'s
selection procedure, plus an amendment recording the upstream ask for a
machine-readable Copilot review schema.

## The exit code, since it changed meaning between branches

#591 was authored before the outage of 2026-08-06 and defined exit `42`
for its check reading. #607, #608 and #613 took `42` for a round that
read fewer files than the pull request changed, and `43` for a shape the
reader does not know, while it sat. The forward-merge renumbered the
check reading to **44** and ranked it under both: `43` says no field can
be believed, `42` says part of the diff has no review, and only once
those are clear is a wedged required check the thing worth reporting.
`wait` returns the coverage and shape verdict where it has one and reads
the checks only where it does not.

## Review state

Both were reviewed and merged on their own pull requests, so this
promotion carries no unreviewed change. #591 ran 18 rounds across its
life, 5 of them after the revival, and #618 ran 5. Every finding was
accepted except one on #614, which was declined with evidence and
recorded under "Disproved Claims" in
[`.github/copilot-instructions.md`](./.github/copilot-instructions.md).

## Why #591 was open long enough to need reviving

It was parked correctly during the GitHub Actions major outage, with the
reason written on the pull request, and the reason then expired quietly.
Three days later it was 20 commits behind `develop`, conflicting in six
regions, and carrying an exit code that meant something else. #618 is
the procedural answer to that, and it is in this same promotion.

## Merge shape

This is a promotion, so it merges as a **merge commit** rather than a
squash, per [`GOVERNANCE.md`](./GOVERNANCE.md) "Branching Model". Its
head is `develop` itself, so it must **not** be merged with
`--delete-branch`.
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.

2 participants