Skip to content

Promote develop to main: the prose backlog cleared, and a review loop that blocks on what it cannot read - #609

Merged
ptr727 merged 15 commits into
mainfrom
develop
Aug 8, 2026
Merged

Promote develop to main: the prose backlog cleared, and a review loop that blocks on what it cannot read#609
ptr727 merged 15 commits into
mainfrom
develop

Conversation

@ptr727

@ptr727 ptr727 commented Aug 8, 2026

Copy link
Copy Markdown
Owner

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

  • prose_lint: the hub's own docs, spec, and catalog do not pass the gate it ships #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

ptr727 and others added 14 commits August 6, 2026 16:39
Carries the TODO.md cluster "Reaching the Hub in Your Own Checkout" whole.

Repository Boundaries and Write Safety gains the checkout-isolation rule, scoped
per task rather than per agent, and Verification Discipline gains the same rule
for reading, since a clone is whatever it last fetched rather than the branch it
names. Fleet Bootstrap names the hub as a defined term and adds the reach rule,
README.md defines the six terms a request is phrased in for a repository that
carries no instruction set, and the copilot instructions point a reviewer at the
hub scripts and correct the byte-locked section count to match spec/files.json.
The host-wide agent-safety blocks carry the same two rules.

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

Selecting the next work item meant re-deriving the grouping every
session. Entries were grouped by the surface they change, which is
#535's convention and answers what a change costs, while the working
question is what fits one pull request. That analysis is what the file
should carry forward and did not.

Three defects made it unsafe to select from, each verified against
`develop` at `3d1a0b1`:

- **Three open issues appeared nowhere in it.** A pass reading only this
file could not see #538, #550 or #557.
- **Two open issues were already fixed.** Both are now closed with their
evidence quoted, see below.
- **Two research artifacts contradicted the tree.** The re-vendor entry
called `repo-config/configure.sh` carried `verbatim` with `appliesTo:
"*"`, while `spec/files.json` no longer declares it and
`spec/divergences.json` dispositions it `retire`.
`reports/divergences.md` still rendered it under `re-vendor` across six
repos, so the committed report disagreed with its own ledger.

## What changed

A `###` heading under `Work Clusters` is one pull request. 20 clusters,
28 entries, after the follow-up commit retired one. The #535 rationale
is not discarded, it moves from an organizing axis to a per-cluster
`Touches` and `Cost` field, which states more than the old heading
implied: a cluster confined to one surface says so in a clause and a
cluster spanning two states both.

An entry carries `Blocked by`, `Issue` and `Checked` exactly once each,
in that order, and never omits one, because an omitted field reads as
unknown rather than as none. `Checked` is a per-entry freshness anchor
naming the branch, commit and date a claim was last read against the
tree, replacing a file-level anchor that already carried two commits for
two passes and could not carry a third. `Open` states a decision a
future session makes, `Settled` states a finding it does not re-derive,
and a `Settled` bullet carries a number, a proper name, or a rejected
alternative.

Two structural slots are new:

- **`Fleet Sweeps`** holds work landing on a downstream visit. It had no
home, which left #353, #379 and #489 under a heading promising a close
they explicitly could not take. Each item carries `Hub state`,
`Outstanding` and `Rides with`, so six repetitions of naming the same
visit become one manifest. It records the batching preference outright,
that the fleet is caught up periodically rather than after every hub
change, and names Blog as the pilot.
- **`Recorded for the Maintainer`** holds issue actions that are the
maintainer's to take, each carrying the evidence so it is one action
rather than a re-derivation.

`How to Select the Next Item` states the procedure. Step 1 is confirming
every open issue has an entry, which is the honest maximum a
hand-maintained file gives and the reason it comes first.

## Two overlaps folded in

- **#577 and the registry-description entry are one decision.**
Declaring the description in `registry/repos.json` makes every mirror
read a field rather than parse a paragraph, so taking #577 first means
writing an extraction rule the registry change then deletes. They are
now one cluster with the sequencing stated.
- **#365 and #483 are one host visit.** The rollout needs the tooling
matrix to be repeatable and the matrix is only worth filling if the
rollout uses it.

## A generated ledger was considered and rejected

`spec/divergences.json` earns its generator by joining a curated ledger
against live fleet reality and rendering `UNTRIAGED` for anything live
the ledger missed. A backlog has no such reality to join against, and
the only mechanizable join is issue open or closed state, which is step
1 of the procedure. What is given up is stated in the file rather than
glossed: no enum enforcement, and no automatic surfacing of an open
issue with no entry, which is exactly the #538, #550 and #557 failure.
The bold-label template stays parseable, so a later checker asserting
every `Checked` commit resolves is buildable without re-shaping the
file.

## Sweep obligations

`spec/readme-structure.md` said a `TODO.md` "opens with a `# TODO`
heading and one line saying what it holds, then a flat bullet list".
#535 already contradicted both clauses. It now states the minimum rather
than the shape, and says this repo's file is a worked example rather
than a template every repo adopts, since a five-line downstream backlog
needs no selection procedure. That file is not in `spec/files.json`, so
the edit costs no re-vendor.

`spec/section-model.md`, `STANDUP.md` and `README.md` describe the
file's purpose without asserting its shape, verified clean and recorded
here so the sweep is done rather than skipped.
`host-setup/agent-safety/README.md` ends the write-guard note with
"Tracked in `TODO.md`", still true.

## Ledger regenerated

`reports/divergences.md` now renders `repo-config/configure.sh` under
`retire` rather than `re-vendor`, resolving the contradiction.
Regenerating it also **disproved a claim written during this pass**:
Blog is one of two repos carrying `AGENTS.md` "Fleet Bootstrap", not the
only one. HomeAutomation-Config carries it too, and being `operational`
it exercises the direct-to-`develop` path rather than the pull request
one, which the pilot note now records as the second visit worth making
rather than the first.

A second correction, to #521's own claim: three merge-bot jobs carry
both `contents: write` and `pull-requests: write` and the fourth carries
`pull-requests: write` alone, so the issue's "all four" is one job wide.
The entry records this rather than repeating the issue.

## Issues closed

- **#552** closed. #572 landed `template_ref_outside_verbatim` in
`spec/audit.py`, excising verbatim regions by position before the
coordination-reference scan, with eight self-test cases. Positional
excision rather than content removal is required by the fenced-copy
case, since removing extracted text would delete a fenced copy along
with the real region and the scan would fail open.
- **#538** closed. Complete on all three points, checked one at a time:
the whole `parameters` object of every parameterized rule is compared
with keys and set-like arrays sorted on both sides, the rule-type set is
compared in both directions, and the header states what check verifies
and names what stays unaudited.

## Follow-up: the file's own procedure applied to itself

`6bed2d9` is the first exercise of step 9, and it is also what gave this
pull request the CI run it never had, since the whole thing opened
during the Actions outage and a rollup does not re-fire on its own.

`9d85941` merged "Reaching the Hub in Your Own Checkout" whole, so the
cluster is deleted rather than annotated and nothing moves out of it.
Each of its four entries was re-read against `develop` before the
deletion rather than trusted to the merge: the checkout-isolation rule
and the stale-clone read rule in `GOVERNANCE.md`, the hub definition in
`AGENTS.md` "Fleet Bootstrap", the six terms in `README.md`, and the
hub-script pointers in `.github/copilot-instructions.md`.

#557 and #579 take its place under "Verified Complete, Awaiting Close"
citing that commit, since both close on the `develop` to `main`
promotion rather than on the merge that completed them. The two
"Recorded for the Maintainer" comments they subsume go with them,
because a recorded action saying an issue stays open until a cluster
lands is a stale instruction once the cluster has landed. That section
now holds two items rather than four.

Two entries left the same section for the opposite reason. #509 and #490
were closed on 2026-08-03, three days before the clustering commit, so a
section whose entire subject is what still awaits a close was carrying
two entries that had already had one. Their link definitions stay, since
other clusters still cite both issues.

Four definitions the deletions orphaned go too, `issue-552`, `pr-572`,
`pr-588` and `workflow`, along with the pull request block left holding
none. MD053 is enabled by default, so an unused definition is a lint
failure rather than a spare, and the reference-and-definition audit in
the verification list above was re-run rather than assumed to still
hold.

## Verification

- `python3 scripts/prose_lint.py . --diff origin/develop` clean, and
`TODO.md` holds 0 violations under the default rules as it did before.
- `python3 spec/validate.py` reports 22 cataloged, 0 backlog.
- markdownlint-cli2 and editorconfig-checker clean on all three files,
CRLF preserved, and both re-run on `TODO.md` after `6bed2d9`.
- Every link reference is defined and every definition is cited, with
each block sorted by reference name.
- All 28 cluster entries carry the three mandatory fields in order,
checked mechanically.
- The selection procedure was walked end to end against the file, and
names a cluster from 5 KB of headings and `State` lines without opening
any research.

Note the file grew, 61 KB to 73 KB, rather than compressing. Three
issues that had no entry were added along with the procedure and the
per-entry anchors. The metric that moved is the one selection depends
on, which is the 5 KB a re-select actually reads.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Why

Every verdict a `--diff` run reports rests on a scope the run never
checked. A gate that finds nothing is indistinguishable from a gate with
nothing to find, and the exit code is the same either way.

Four routes to that false clean are already on record, each closed by a
guard written after a reviewer noticed it: an unresolvable base widening
to a whole-tree scan, a multi-line `paths` input read only to its first
newline, a diff taken in one repository while scanning another, and a
path under no repository at all. Per-route guards are the wrong shape
for a fifth, because the fifth is found by a reviewer or not at all.
`LEAST_PLAUSIBLE` is already the floor on a whole-tree sweep, so the
shape exists in the file and the `--diff` path is what lacked it.

Fresh evidence for the entry came from the session before this one,
where `prose_lint --diff --summary` printed `0 violation(s) across 0
file(s)` on a real change and distinguishing that from a scoping failure
took a hand-written call into `changed_lines()` to prove the scope was
non-empty.

## What changed

A run that resolves a non-empty diff and matches none of the files it
names now exits 2 naming them, instead of exiting 0.

Zero alone is not the test. A change touching only files the rules do
not read, an image or a lock file, legitimately matches nothing, so the
comparison is against the diff's own list of files this run could have
read. `unread_diff_files()` builds that list, and `asked_about()` keeps
a caller who narrowed the scan on purpose from being told the narrowing
is a defect.

The exit code is 2 rather than 1, matching the two guards beside it, so
a caller can tell "the gate could not run" from "the gate ran and found
something".

## The floor found the fifth route immediately

`git diff` reports repository-relative keys while discovery keys off the
directory the run started in. Run from a subdirectory, the intersection
is empty and the run reports clean. No existing guard covers it, and it
is why `unread_diff_files()` resolves keys against the repository top
level: reading them against the working directory would leave the list
empty in exactly the case it exists to catch.

Measured on this branch's own diff, run from `scripts/`, with the
previous revision extracted to a temporary path so both run against the
same tree. The error text is verbatim, wrapped here for width:

```text
$ git show HEAD~1:scripts/prose_lint.py > /tmp/prev_prose_lint.py
$ cd scripts && python3 /tmp/prev_prose_lint.py . --diff develop --check dupword
$ echo $?
0

$ cd scripts && python3 prose_lint.py . --diff develop --check dupword
error: the diff against 'develop' names 2 readable file(s) this run was asked about, and the
scan matched none of them: scripts/prose_lint.py, scripts/test_prose_lint.py. Refusing to
report a clean run, since a gate that read nothing is indistinguishable from a gate with
nothing to read. Check that the run starts at the repository top level and that the requested
paths cover the change.
$ echo $?
2
```

## Both directions are tested, and the second one is the larger half

Ten cases. Three fail against the previous revision, which is what
proves the floor bites. The other seven pass against both revisions on
purpose: they hold the direction that matters more, that a legitimate
zero stays a clean run. A gate that cries wolf on a commit adding a logo
is how a safety check stops being read.

The must-not-fire cases are a deliberate narrowing, an empty diff, an
excluded file, a generated tree, a path the diff names that no longer
exists, and a binary. Each was confirmed against a real run as well as a
mocked one, the binary case with an actual PNG committed in a scratch
repository.

## Verification

- `python3 scripts/test_prose_lint.py` - 173 pass, up from 163.
- `python3 spec/validate.py`, `python3 spec/audit.py --selftest`,
`python3 scripts/repo_gate.py` - all clean.
- `python3 scripts/prose_lint.py . --check charset --check dupword
--check spelling` - clean, the blocking CI run.
- `python3 scripts/prose_lint.py . --diff develop` - clean, and it
caught two comment blocks in this change breaking the
one-sentence-per-line rule, which are fixed.
- Re-run after rebasing onto `develop` at `3f91868` rather than only on
the base the work started from.

## Honest limits

An absolute path argument produces an empty list rather than a finding,
so the floor falls back to today's behavior there instead of
over-blocking. Deletions never enter the scope map at all, since a
deletion hunk yields an empty line range, so they cannot trigger it.

The CI jobs in this repo call `prose_lint.py` without `--diff`, so
nothing here changes what this repo's own pipeline reports. The consumer
is `.github/actions/prose-gate`, which passes `--diff` and runs from the
caller's repository root.

## Backlog

Carries the `TODO.md` cluster "The Prose Gate Scope Floor" whole. It
unblocks "The Representative-Data Path Check", which is worth having
only once the gate can prove it read something. Per selection step 9 the
cluster is deleted when this merges, which is a follow-up commit rather
than part of this diff.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
## Why

`GOVERNANCE.md` "Representative Data in Agent-Authored Text" says
agent-authored text illustrates with constructed data rather than data
observed in the maintainer's environment, and says in the same breath
that no pattern closes it. This gates the half a pattern can reach.

It is introduced as that half deliberately. The leak that motivated the
rule was name-shaped and sat in a pull request comment, which no
committed-file linter reads, so a check mistaken for the answer is what
stops anyone looking at the exposure the rule actually exists for.

Carries the `TODO.md` cluster "The Representative-Data Path Check",
which #595 released from `blocked` once the scope floor shipped in #594.

## The shapes were measured, not assumed

The backlog recorded four shapes, `/home/<name>`, `/Users/<name>`,
`C:\Users\<name>`, and a bare drive letter. Run against this repository,
**a bare drive letter matched 11 files and named a path in none of
them.**

An escaped newline after any word ending in a letter and a colon reads
as a drive letter, so a YAML fixture is full of them:

| Matched text | What it actually is |
| --- | --- |
| `e:\n    needs` | `jobs:\n    needs` inside a test fixture string |
| `s:\n  a` | `jobs:\n  a:` inside a test fixture string |
| `C:\tmp` (8 hits) | a temp path in fixtures for the string-literal
rule |

That shape is dropped. The one kept is a drive letter followed by
`Users`, which matches nothing in the tree today. This is narrower than
the committed prose, which says "an absolute home path or a drive
letter", so the narrowing is stated here rather than left for a reader
to discover.

## The exemption is built, not listed

A wrong exemption hands out a work list that damages correct documents,
and this rule has two self-referential problems that a file-name
allowlist would solve badly and briefly.

**The rule's own wording quotes the shapes it forbids.** Requiring a
real user segment solves it by construction: the documentation
placeholder is not a valid account name, so it describes the shape
without matching it. No exemption names `GOVERNANCE.md`, and none goes
stale when the text moves.

| Probe | Verdict |
| --- | --- |
| `C:\Users\<name>` in documentation | no match |
| `/home/<name>` in documentation | no match |
| `/home/vscode/.ssh` | exempt, container account |
| a real absolute home path | **flagged** |

**Container accounts are exempt by name.** Every home path in this
repository, all 15, is `/home/vscode` from the devcontainer snippets and
the doc describing them. Without that exemption the rule would open with
15 findings and no true positive among them.

**The gate reads its own source**, so the bait paths in the tests are
assembled from parts and a case asserts that neither `prose_lint.py` nor
its test file carries a literal. That is the approach the file already
takes for non-ASCII, which it writes as escapes for exactly this reason.

## Operational repositories are exempt, and told so

An operational repository's runbook carries the literal path an operator
types. That is the repository's own content rather than an agent quoting
an environment it observed, which is the distinction the rule is about.

The model is read from what the repository carries rather than from the
hub registry, which a downstream checkout cannot see: `spec/files.json`
declares `repo-config/operational/develop.json` for the operational
model and `repo-config/develop.json` for the release one. The hub
carries both, being the template for each, so it reads as release rather
than exempting itself from a rule it authors.

**The skip is announced on stderr**, because a rule that silently stops
running reads as a rule that passed, which is the same failure #594
closed on the other side.

## Scope note

Unlike every other prose rule, this one reads fenced blocks and
configuration values rather than stripped prose. A transcript pasted
from a terminal and a bind mount naming a real home are the exposure in
its most consequential forms, and neither is a sentence.

## Verification

- `python3 scripts/test_prose_lint.py` - 188 pass, up from 173. Of the
14 new cases, 10 fail against `develop` and 4 pass against both, holding
the must-not-fire direction.
- `python3 scripts/prose_lint.py . --check home-path` - clean across the
tracked tree.
- `python3 scripts/prose_lint.py . --summary` - 557 violations across 45
files, the #519 backlog figure unchanged, with `home-path` contributing
none. Adding a rule to `DEFAULT_RULES` did not move the number.
- `python3 spec/validate.py`, `python3 spec/audit.py --selftest`,
`python3 scripts/repo_gate.py` - clean.
- The blocking CI prose run - clean.

## Correction: this branch carries the TODO bookkeeping too

An earlier revision of this description said the `TODO.md` edits were
held back to avoid conflicting with #595. **That was wrong.** This
branch was cut from #595's branch rather than from `develop`, so it
already contained that commit, and the description claimed an absence a
reader could disprove from the diff.

The review then caught the real consequence: the cluster entry still
promised a bare drive letter after the implementation dropped that
shape. Correcting the bullet was the smaller half of the answer, since
this is the pull request that ships the cluster, so `c6fca9e` applies
step 9 and deletes it rather than leaving a corrected description of
finished work. The disproved claim is recorded here and in the commit
message, alongside the measurement that disproved it.

**This makes #595 redundant**, since every line of it is already in this
branch. Closing it as superseded is the maintainer's call rather than
something this branch does.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Answers #597, filed from a downstream repository after an agent skipped
a mandatory pre-pull-request gate while following every carried rule
correctly.

## The failure

A repository's `OPERATIONS.md` held a verification step CI structurally
cannot perform, under a heading of that repository's own invention. The
routing sentence in its `AGENTS.md` named the file by its most visible
function, which was a post-merge one. An agent deciding how to verify a
change before opening a pull request therefore had no reason to open the
file, and skipped the one check that could have caught the regression.
Nothing in that chain broke a rule, which is why it produced no signal.

## Why the existing rules do not reach it

"Verification Discipline" is the natural home and does not cover it.
Every bullet there assumes the agent already knows which gate to run: a
gate that stops gating, a filter that matches less, a green check that
is not evidence. This failure is upstream of all of them, because the
gate was never located, so there was no green to distrust.

## What lands

**The locating half, not the wording half.** Fixing the one repository's
routing sentence leaves the next repository to reinvent the same shape,
so the fix is a carried rule that points at a fleet-declared location.

- `GOVERNANCE.md` "Verification Discipline" gains a bullet, placed first
because the failure is upstream of the rest: a change's checks are read
from what the repository declares before any is run, CI's coverage is
not that list, and green is the precise signal that the unreachable half
was skipped. It names why a document's own description cannot be relied
on to find the check.
- `spec/section-model.md` and `STANDUP.md` declare `Local Verification`
as a sixth `OPERATIONS.md` heading, leading the file as the only
pre-merge one. Both restatements of "those five headings" move with it.
- This repository's `OPERATIONS.md` becomes the worked example. The
gates block moves under the new heading, and the section states the two
verifications a pull request runner here cannot perform, the live fleet
audit and `configure.sh check`, each of which reads another repository
over the API. `spec/audit.py --selftest` is all CI runs of the audit,
against fixtures.
- `TODO.md` indexes #597 under "Fleet Sweeps" rather than as complete,
since every repository still owes the heading, riding the pending
re-vendor visit.

## The gap this leaves, stated rather than hidden

The audit reports nothing here today, because `OPERATIONS.md` is
presence-checked only, so a repository using none of the declared
headings passes. The heading check is #523's cluster, and until it ships
the sweep is verified by reading each file rather than by a run.
`TODO.md` records that.

## Verification

Every gate in the runbook: the four self-test suites, `repo_gate.py`,
`spec/validate.py`, both `prose_lint.py` invocations,
`editorconfig-checker`, and markdownlint over 44 files with 0 issues.
The live audit runs clean at 0 defect findings, its drift output being
the already-recorded re-vendor backlog.

One note on the run itself, since this change is about false cleans.
`prose_lint.py . --diff origin/develop` reported 0 violations across **0
files**, because nothing was committed at that point, so the warn set
was re-run against the five paths explicitly. It reports 14 `semicolon`
findings in `GOVERNANCE.md`, identical to the count on the same file
with the change stashed, so this adds none.

## Review round: an unrunnable command, in the section about unrunnable
checks

Round one flagged that `configure.sh check` is not invokable from the
repository root, since the script lives at `repo-config/configure.sh`,
so a reader following the instruction gets `command not found`. Valid,
and pointed at a section whose whole subject is a check CI cannot run.

Fixed in `b9f9640`, and swept as a class rather than as an instance.
This branch adds three command references, and a second carried the same
defect: `spec/audit.py --selftest` was missing the `python3` prefix CI
actually invokes it with. Both now carry the path and the prefix the
runbooks below use, and the sentence points at those runbooks for the
argument lists rather than restating them. Round two against `b9f9640`
generated no new comments.

Worth recording from the loop itself: the `gh-write-guard` hook blocked
the resolve mutation because the thread id was hand-typed from the
previous query's output rather than re-captured live. That is the rule
catching a real instance rather than a hypothetical one, and the
mutation succeeded once the id came from a query in the same invocation.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements the decision recorded on #580, which closed with the
reply-and-resolve helper carried forward as its own `TODO.md` cluster
and the dependency it waited on already met.

## Why a helper rather than a firmer rule

A reply-and-resolve run by hand keeps failing the same way: a GitHub
node id typed into a mutation. Three instances are on record. The third
happened while preparing this cluster, when `gh-write-guard` refused a
`resolveReviewThread` mutation carrying an id hand-typed out of the
previous query's output, roughly an hour after the same session quoted
that exact rule in a pull request body.

The rule is stated in `GOVERNANCE.md` and again in the host-level
guidance, and the agents that broke it had read both. That says the
shape of the operation fails rather than the reader's knowledge of the
rule, so this changes the shape.

## What `reply` does

```sh
python3 scripts/pr_review.py reply <N> --repo <owner>/<name> \
  --match "<words from the finding>" --body "<answer>" --resolve
```

It queries the thread id itself and passes it straight to the mutation.
There is deliberately no argument an id fits in, and a case asserts the
id each mutation carries is the one that run just read.

- **Selects on the finding's own words, not a line number.** A fix push
moves the line, which is how three replies once posted against nothing
while the resolves still succeeded, closing the threads carrying no
answer. Matching is case-insensitive, since the text is quoted back out
of a digest by a reader.
- **Every failure stops rather than falls back**, because each
alternative closes a finding while leaving it unanswered. No match exits
`60` and prints the open threads, since a no-match and an
already-answered thread read identically from here. More than one match
exits `61` and prints both candidates rather than taking the first. A
reply whose response carries no `url`, or whose body came back empty,
exits `62` **without** resolving. A resolve that does not confirm exits
`63`, with the reply already posted.
- **`--resolve` is opt-in**, since a decline is resolved only once its
evidence is in the thread.
- **A target under another owner exits `64` before anything is read.**

## The trade this makes, stated rather than glossed

The script carried `test_no_mutation_reaches_this_script`, locking it
read-only so that mutations stayed visible to the `gh-write-guard`
PreToolUse hook. This reverses that, so the test is **narrowed rather
than deleted**: every other state-changing call is still banned
tree-wide and the mutation-document count is pinned at exactly two, so a
third arriving is a write nobody reviewed as one.

The hook genuinely cannot see a write this script performs, since it
sees `python3 pr_review.py reply` and no `gh` write. That is a real loss
of a second pair of eyes. It is taken because what the hook guards
against there is a fabricated id, and this removes that at the source
instead of catching it afterwards.

The guard's other rule is re-implemented rather than assumed. `in_scope`
refuses a target under an owner other than this checkout's, and it takes
**no `GH_WRITE_GUARD_ALLOW` escape**: a grant this process can be handed
is one the caller sets on the command that runs it, and a grant the
caller writes for itself is not a grant. The cross-owner case goes
through the runbook's explicit `gh` path, where the hook reads the
maintainer's grant from the session instead. It is honest in the docs
that this stops a mistake rather than a determined caller.

## Corrections carried in this change

- **The cluster's cost line was wrong.**
`.github/copilot-instructions.md` is carried at `intent` fidelity per
`spec/files.json`, so the runbook edit is not hub-only. A sixth `Detail`
under the Fleet Sweeps re-vendor entry records it, noting a repo still
on the old copy is not broken, since the mutations that copy documents
still work.
- **`OPERATIONS.md` scoped the `gh pr edit` breakage to `--base`**,
narrower than this repo's own runbook, which states the same failure
against `--title/--body`. The failure is in the mutation the command
builds rather than the field asked for. Widened, with the runbook
cross-referenced. Not exercised live here, since `gh pr edit` is a
write.
- **`TODO.md` carried a clause naming "The Prose Gate Scope Floor"**, a
cluster that shipped and was deleted; the phrase appeared nowhere else
in the tree.

`GOVERNANCE.md` is deliberately untouched. It owns the review contract
and routes mechanics to `.github/copilot-instructions.md`, so naming the
helper there would add a verbatim re-vendor for nothing.

## Verification

Run from this checkout on the branch head:

- `python3 scripts/test_pr_review.py` — 103 tests, OK. The suite gains
cases for selection without an id, cursor-followed pagination, ambiguity
and no-match refusal, reply confirmation before resolve, owner scope,
and argument validation.
- `python3 scripts/test_prose_lint.py` (190), `python3
scripts/test_repo_gate.py` (23), `python3 spec/audit.py --selftest`,
`python3 spec/validate.py`, `python3 scripts/repo_gate.py` — all pass.
- `python3 scripts/prose_lint.py . --check charset --check dupword
--check spelling` and `--diff origin/develop --summary` — both clean.
- markdownlint and `editorconfig-checker` over the tree — clean.
- `python3 scripts/pr_review.py status 588 --repo
ptr727/ProjectTemplate` — a live read confirming the transport refactor,
which routes `gql` through a shared `gh_graphql` that also checks the
`errors` key rather than reading the null it leaves.

**Not verified live:** every success path of `reply` posts a public
comment, so it has not been run against a real pull request. The logic
is covered by mocked cases only, and the two refusal paths that make no
network call were exercised directly. This pull request's own review
round is the natural first live exercise.

Closes the `TODO.md` cluster "The Reply-and-Resolve Helper", deleted
here per the file's own rule 9.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes part of #519: `catalog/snippets/` is now at zero prose findings.
The whole tree goes from **557 across 45 files** to **373 across 26**.

Snippets led because a non-conformant snippet seeds its violations into
every repo that adopts it, and the downstream repo is then flagged for
content it was handed. That is a seeding-risk ordering rather than a
size one: the three biggest files in the tree are not snippets at all.

## The exemptions were fixed before the corpus

The findings were read as a hypothesis, not a work list. `prose_lint`'s
comment rules ban a construction rather than a detectable subset of it,
so the exemptions carry the whole burden of encoding the legitimate
uses, and a stale one hands out a work list that damages correct files.
Three of the 184 snippet findings were exemption misses.

**A comment body that is one token closing on a colon is a key or a
heading.** `# ignore:` heading a commented-out block in `codecov.yml` is
disabled configuration, so the capitalization `comment-case` asked for
would corrupt the key a repo uncomments. Measured tree-wide, the shape
matches exactly two bodies, both a key or a heading. The broader reading
of "a body with no whitespace at all" covers 99 and sweeps in every
reference URI and `v3.2.0` marker with them, which is why the narrow
shape was taken.

**A label opening a definition names the thing being defined.** `#
publish - 'true' when ...` documents an output named `publish`, so
capitalizing it renames what the workflow declares. This is the comment
spelling of the `- **Label** - text` construct the dash rule already
exempts.

The first design here was wrong, and only the corpus said so. A bare
`^word - ` reading matches six comment bodies, but two of them are
wrapped continuations whose dash is parenthetical, which is precisely
the construction the dash rule exists to catch. Both continuations
follow a line that does not end a sentence, so scoping the exemption to
a line that *opens* a definition excludes them structurally. The corpus
chose the unit; a test locks that case.

**A definition list keyed on a code span needed no checker change at
all.** `- `latest` - the current stable release.` is flagged because
`LABEL_DASH` requires a `**bold**` label. Following the finding's own
message ("spaced hyphen -> a comma, two sentences, or parentheses")
would have destroyed the list. `- **`latest`** - text` is *already*
exempt and is the form `GOVERNANCE.md` and `docs/devcontainer.md` use,
so bolding the label is the fix and the code span survives inside it. 17
such lines exist tree-wide; the 2 in snippets are fixed here.

## Verdicts diffed in both directions, before any prose was touched

Whole-tree, old against new:

- **Went silent:** exactly the three misses above.
- **Newly reported:** nothing.

Five tests lock the two new arms, including
`test_a_continuation_dash_is_not_read_as_a_label`, which is the design
that was rejected. Verified against the pre-change checker in an
isolated copy: the new tests fail there and pass here.

## Cost, stated rather than left to be found

The label exemption costs a detection: a comment that genuinely opens a
sentence on one word followed by a spaced dash is no longer read as a
lowercase opening. `scripts/README.md` records this alongside both new
exemptions.

`comment-case` still reports a comment opening on a lowercase identifier
(`# isinstance guard first: ...`, `# consumerModel has no defaults
fallback ...`). That is 23 of the remaining findings and the rule
intends them restructured rather than exempted, so nothing here widens
to cover them.

## Two open questions on #519, answered

Neither answer was recorded on the issue, and both are now in `TODO.md`:

1. Is `reports/**` exempt? **Yes**, as a generated tree.
2. Are snippets higher priority? **Yes**, which is this pull request's
whole ordering.

The issue's own headline numbers (668 total, 119 hand-authored at
`69688ec`) are stale, computed before `comment-wrap`/`comment-case`
joined `DEFAULT_RULES` and before `reports/` became exempt.

## Verification

Every gate in `OPERATIONS.md` "Run the gates the way CI runs them", from
this checkout:

- `test_prose_lint.py` 195 tests OK, `test_repo_gate.py` 23 OK,
`test_pr_review.py` 103 OK
- `spec/audit.py --selftest` SELFTEST PASS, `spec/validate.py` OK,
`repo_gate.py` 0 issues
- Gating prose run (`charset`/`dupword`/`spelling`) exits 0
- `editorconfig-checker` clean, `jq empty` clean over every JSON glob
- `--diff origin/develop` exits 0, so nothing this branch touches
carries a finding

Line endings were checked explicitly rather than assumed: no file
changed EOL style and none has mixed endings. The `git diff --check`
trailing-whitespace warnings are pre-existing CR in CRLF files, which
`text: unset` leaves alone.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
The write-guard hook read a newline as whitespace, so it disappeared when tokenizing and every token on a later line became an argument of the first line's `git push`. A push and a `gh pr create` written as two lines resolved to seven push targets rather than one, `develop` among them, and the hook denied the push as a direct push to a protected branch while the push targeted an ordinary feature branch.

The direction is over-blocking, so this was a usability defect rather than a safety hole. It is worth fixing because the denial states a reason that is not true of the command, and a safety hook that cries wolf is one that stops being read.

A newline ends a command exactly as `;` and `&&` already did, so it is an operator character rather than whitespace. The operator set is stated once as a string, `shlex` takes it in that form, and the newline is removed from `shlex`'s whitespace. The degraded path, which only unbalanced quoting reaches, splits lines itself so it cannot lose the separator either. Backslash-newline continuations are folded to a space in `classify` before any of this runs, so a continued command still reads as one command.

Six self-test cases lock it. Against the pre-fix code the incident case regresses and the others pass unchanged, which is what makes them guardrails: a push on a later line, a second push on the next line, a continuation, a newline inside a quoted body, and a push on the second line of a command whose quoting cannot be parsed. Run over 179 commands, being every existing case, each again with a trailing newline, and eleven realistic multi-line shapes, the two versions differ in five decisions, all deny to allow, all the false-positive shape, with no allow becoming deny.

The self-test ran nowhere but at install time, where a regression surfaces as a broken machine rather than a failed check, so CI now runs it beside the audit self-test it matches: offline, standard library only. OPERATIONS.md carries the same line because that list mirrors what CI runs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two gates of the same shape, each confirming that a reference in a
committed artifact still resolves. Both failures are silent, and both
are caught by a reviewer or not at all. This clears the `Two Checks That
Read What a Claim Points At` cluster from `TODO.md`, whose two `Checked`
anchors were re-verified against `develop` at `dbd1cdc` before anything
was written: `check_sha_pin` still matched `^[0-9a-f]{40}$` and never
read the ref, and no subcommand read a description at all.

## `sha-pin` resolves the pin, not just its shape

Forty hex characters is a format any fabricated string satisfies, and an
agent hand-writing a plausible SHA into a workflow is a failure this
repo has seen. Resolving also catches the neighboring case, a pin whose
commit was reachable only from a branch since squashed and deleted,
which breaks a downstream gate long after the change that caused it. The
`gh-write-guard` hook cannot cover either, since it watches Bash and an
editor tool writing the same string into a file never reaches it.

Resolution is scoped to the scanned repository's own owner, which is
where the fleet's actions live and where that decay comes from. A
third-party tag is stable by comparison, and reading one would make
every local run of the gate depend on a stranger's repository answering.
**The cost is real and is stated rather than hidden**: a fabricated pin
on a third-party action is still only shape-checked. So every run prints
what the pass actually covered, because a scope that resolves nothing
prints the same `0 issue(s)` a full sweep does. On the hub that is zero
today, since all 45 pinned refs here are under other owners, and the
`ptr727` pins live in the downstream repos that consume `prose-gate`,
reached by running this gate from a hub checkout with `--root`.

A pin is a finding only where GitHub **answered** absent, meaning a 404
or a 422. An offline host, a missing `gh`, a rate limit and a narrow
token all report as unread and leave the pin on its shape, so the gate
stays usable with no network instead of failing a correct tree. A 404 is
confirmed against the repository itself before it becomes a finding,
since an absent commit and an unreadable repository are the same answer
from here and a repository-scoped CI token is narrower than the fleet.

## `pr_review.py claims` reads a description against its own branch

Three stale descriptions in one session generated six review findings
between them.

**Its first design scanned the body for bare SHAs, and the corpus
rejected that outright.** Over the 25 most recent merged pull requests
it raised four findings and every one was correct prose: a `develop`
commit named as history, a SHA inside a pasted digest, and two commits
in another repository written without a URL. Nothing in the *shape* of a
bare SHA separates those from a real claim, and separating them by
meaning is the similarity heuristic `spec/section-model.md` already
rules out, which is the same reason the entry put prose claims out of
scope. A path arm measured on the same corpus is worse, flagging 54 of
215 backticked candidates, nearly all bare basenames, `origin/develop`,
and other repositories.

What survives is the verb. A commit counts only where the body says it
was fixed, landed, shipped, added, introduced, corrected, resolved,
carried or amended in it. That alternation raises exactly one reference
over the same 25, and that one is true. It is an **inclusion** list, so
a phrasing nobody thought of costs a detection rather than inventing a
finding, and a claimed SHA still carries a digit as a backstop on the
list growing later.

A commit passes on **ancestry**, so a description may cite one it
inherited from the base branch, and the two failing shapes are named
apart: a commit the repository does not carry at all, and one this head
does not descend from. The `uses:` refs are matched against the whole
tree at head, as one archive rather than a request per file, because
this repo carries `uses:` lines in snippets and docs as well as under
`.github/`. Undecided is a third answer here too, and where every
reference is undecided the run exits `71` rather than printing the
`stale=0` a clean read prints.

## Evidence

- Both new gates run clean locally, and the whole suite passes: `python3
-m unittest discover -s scripts` reports 367 tests OK, up from 340.
- The resolver was exercised live before the tests were written: a real
commit resolves, a fabricated 40-hex does not, and an unreadable
repository reads as unknown rather than as a finding.
- `claims` run over the 25 most recent merged pull requests reports
`stale=0` on every one, with one commit reference actually read and
checked.
- No case reaches the network. The GitHub answers are replaced by a
table keyed on the API path, so a case proves the reading rather than
the fleet's current state.

## TODO.md

The cluster is deleted per the file's own step 9. The one thing it
surfaced that this does not carry becomes a new `decision` cluster:
whether the hub should consume its own `prose-gate` action, so its pins
have something under this owner to resolve against. That changes what a
workflow runs rather than what a gate reports, so it is the maintainer's
call rather than this pull request's.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Clears the `A Home for a Disproved Finding` cluster from `TODO.md`. Its
`Checked` anchor was re-verified against `develop` at `756a53e` before
anything was written: the file still says the reviewer is sometimes
factually wrong, still requires a decline to carry evidence, and still
keeps its list of known non-working request paths, with nowhere to put
the proof itself.

## The gap

A decline that carries proof proves something about this tree, and
[GOVERNANCE.md "Every Finding Ends in an
Action"](https://github.com/ptr727/ProjectTemplate/blob/develop/GOVERNANCE.md#every-finding-ends-in-an-action)
is right that the thread is where it belongs while the pull request is
open. Afterwards the thread is the wrong place. The pull request merges,
the next round starts with no memory of the last, and the second
occurrence of the same claim reaches a maintainer with no way to tell it
from a first.

`Disproved Claims` is a new `###` under the runbook, so the three
declared `##` sections in `spec/files.json` are unchanged. An entry
names the claim, what was run or read to disprove it, the revision it
was proved against, and what ends it. **It is deliberately not a list to
append to**: an entry outliving the code it was proved against becomes a
reason not to check, which is strictly worse than proving the claim
again, so an entry whose subject moves is deleted by the change that
moves it rather than edited to look current. Two guards ride with it.
The record answers a repeated claim and never dismisses a new one, so a
finding is judged on its merits first and matched second, and a reply
carries the proof re-read rather than a pointer to a file the reviewer
cannot open. And the entries are this repository's own, so a repository
holding a copy carries the shape and deletes an entry whose subject it
does not carry.

## The three worked examples

**`keys_unsorted` requires jq 1.6.** A suppressed finding on #555
against the normalizer in `repo-config/configure.sh`, reasoning by
analogy from the `walk/1` failure #553 fixed. Re-run in this session
rather than quoted from the thread, on `jq-1.5-1-a5b5cbe` in
`ubuntu:18.04`, the build that reproduces `walk/1`:

```console
jq-1.5-1-a5b5cbe
$ echo '{"b":1,"a":2}' | jq -c 'keys_unsorted'
["b","a"]
$ echo '{"b":1}' | jq -c 'walk(.)'
jq: error: walk/1 is not defined at <top-level>, line 1:
jq: 1 compile error
```

**The write-guard's fallback parse, from #601.** Declined on the ground
that the arm cannot execute, since `punctuation_chars` arrived in Python
3.6, the module uses f-strings throughout, and `install.py` refuses
below 3.7. That is exactly the kind of disproof that expires, which is
why the entry names the floor as what ends it. It also records that the
finding earned a test case rather than a change, since only `ValueError`
from unbalanced quoting reaches that path in practice and nothing
covered it.

**The bare-SHA design, from #602.** This one came from this repository's
own backlog rather than from a reviewer, and it is here because a
rejected method costs the same to re-propose as a declined finding costs
to re-derive, while a backlog has a place for a claim the tree
contradicts and none for a method a measurement rejects. Over the 25
most recent merged pull requests the bare-SHA arm raised four references
and all four were correct prose, and a path arm flagged 54 of 215
backticked candidates.

Both #601 and #602 are folded under disposition **Amends "Record what
was tested and against which revision, and delete an entry whose subject
changes."** They land as the second and third worked examples in the
shipped section rather than as `Settled` bullets, because the entry
ships in the same change.

## Verification

- `python3 scripts/prose_lint.py . --diff develop` clean,
`markdownlint-cli2` clean on both files.
- `python3 -m unittest discover -s scripts` reports 372 tests OK and
`python3 spec/audit.py --selftest` passes, neither touched by this
change but both run because the file is carried.
- Every factual claim in the new section was read against the tree at
`756a53e`: the `keys_unsorted` call in `repo-config/configure.sh`,
`_git_subcommand_arglists` and `_PUNCTUATION_CHARS` in
`gh-write-guard.py`, the `(3, 7)` floor in `install.py`, and the verb
alternation in `pr_review.py`.

## TODO.md

The cluster is deleted per the file's own step 9. What this change does
not carry becomes a new `ready` cluster: `GOVERNANCE.md` outcome 2 ends
at the thread and nothing agent-agnostic points at the record, so an
agent that never opens the provider runbook posts a decline and has
nowhere to put the proof. That is a byte-locked carried section and a
fleet re-vendor, which is why it is a separate change rather than a
second surface in this one.

## Downstream

`.github/copilot-instructions.md` is carried `whole` at `intent` with
`appliesTo: "*"`, so this joins the re-vendor the ledger already lists.
A repository taking it carries the section and its rules, and starts its
own entries empty.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Takes the `The Prose Content Backlog` cluster from `TODO.md`. Its
`Checked`
anchor was re-verified against `develop` at `c64e3e0` before anything
was
written: `prose_lint.py --summary` reported the same 373 violations
across 26
files, `catalog/snippets` the same 0, and the three largest files the
same
99, 52 and 41.

## The exemption came first, and it was wrong

`RUN_ON` read the closing dot of an ellipsis as a sentence terminator,
so
`scripts/repo_gate.py:204` was reported as two sentences:

```text
# .editorconfig: [glob] ... end_of_line = lf
```

That is one schematic line, and the split the rule asked for would have
broken
the fragment the comment exists to show. Fixing the prose first would
have
damaged a correct comment on the checker's say-so, which is the failure
this
repository has now recorded six times. The guard is that a dot preceded
by a
dot never terminates, it carries two tests, and the verdict diff was run
in
both directions: exactly one finding disappeared and none appeared.

## The batch is the comment surface, all of it

The backlog splits by surface rather than by file. `comment-wrap` and
`comment-case` lived entirely in non-Markdown comments, `dash` and
`semicolon`
live entirely in Markdown prose, so the comment half is one reviewable
change
that owes no re-vendor: of the eleven files, four are workflows carried
at
`interface` fidelity, where the contract is job keys and tokens rather
than
comments, and the rest are hub-only.

241 findings are cleared by restructuring, never by widening an
exemption. A
sentence per comment line, and a comment that opened on a lowercase
identifier
reworded to open on a word, which is what the rule asks for and what the
snippets sweep established.

The tree goes from 373 across 26 files to 131 across 15, all of them
`dash`
and `semicolon` in Markdown. Six of those files are carried, so the next
batch
splits again at that line: the carried half rewrites byte-locked
sections and
owes a re-vendor, the hub-only half owes nothing.

## Both rules now gate

A rule swept clean but left warn-only regresses on the next edit with
nothing
reporting it, so `comment-wrap` and `comment-case` move to the gating CI
step
and out of the warn-only one. Every claim about which rules gate moved
with
them, in `OPERATIONS.md`, `scripts/README.md` and the two workflow
comments,
including the one in `scripts/README.md` saying gating still needs
`--diff`
because the tree carries several hundred of them.

## One gap found while counting, not fixed here

`home-path` is in `DEFAULT_RULES` and is named by neither CI list, so
the
pattern-detectable half of the representative-data rule runs on every
bare
local run and gates nothing in CI. It is clean tree-wide, so it is a
hole
rather than a backlog. It is recorded in `OPERATIONS.md` beside the two
gaps
already named there and in `TODO.md`, rather than closed here, because
gating
a third rule is its own decision.

The write guard and both installer wrappers are among the eleven files,
so
every installed copy is now behind the hub by that much. The divergence
is
comment-only, the self-test passes unchanged, and `TODO.md` carries it
under
the host rollout as a re-run at the next visit.

Verified with the full `OPERATIONS.md` local list: five self-tests, the
repo
gate, both prose invocations, `jq`, `spec/validate.py`,
editorconfig-checker,
markdownlint and actionlint.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
)

Takes the next batch of the `The Prose Content Backlog` cluster from
[`TODO.md`](./TODO.md), following
[#600](#600) (snippets)
and [#604](#604)
(comments). Part of
[#519](#519).

## The anchor was re-verified first

`develop` at `c9c92dd`, where `python3 scripts/prose_lint.py --summary`
reports **131 violations across 15 files**. That is the figure the
comment batch left, and merging that batch made it the anchor's own
rather than a branch's, exactly as the cluster entry predicted.

## Scope, and a correction to it

The batch is the **hub-only Markdown files**: `docs/`, `spec/*.md` and
`catalog/README.md`. That is **90 findings across 9 files** (79 `dash`,
11 `semicolon`), not the ~104 the batch was scoped at. The carried six
hold 41, and 90 + 41 = 131.

The 6 carried files are left for a later batch, since they rewrite
byte-locked sections and owe a fleet re-vendor while these owe nothing.
Every `dash` finding in the tree turned out to be hub-only, so what the
carried half now holds is **41 semicolons and no dash at all**.

| File | Findings |
| --- | ---: |
| `docs/devcontainer.md` | 22 |
| `docs/ssh-signing.md` | 17 |
| `spec/type-model.md` | 17 |
| `spec/fidelity-model.md` | 8 |
| `docs/repo-config-carry.md` | 7 |
| `spec/scope-model.md` | 7 |
| `catalog/README.md` | 6 |
| `docs/token-cost.md` | 5 |
| `spec/readme-structure.md` | 1 |

## The exemption came first, and this time it was right

`LABEL_DASH` exempts `- **Label** - text` as a definition separator that
is structurally a colon. It reaches neither a code-span label nor a
paragraph-leading one, and both shapes flagged here. Widening it was the
obvious move, and the corpus rejected it twice.

**A code-span definition list** is already spelled with a colon in five
files, against four dash-spelled lines in one (`catalog/README.md`):

```text
STANDUP.md:91             - `ARCHITECTURE.md`: how a code repo is built
spec/section-model.md:68  - `ARCHITECTURE.md`: how a code repo is built
scripts/README.md:81      - `sha-pin`: every workflow `uses:` naming
repo-config/README.md:6   - `operational/develop.json`: the `develop`
CODESTYLE.md:79           - `.NET Build`: Build with diagnostic
```

**A paragraph-leading label** is already spelled `**Label.** Sentence`,
against nine dash-spelled lines confined to two troubleshooting
sections.

So both were a file that had not adopted the tree's own convention
rather than a construct the rule fails to describe. **The checker is
unchanged by this batch.** That is the same verdict the comment batch
reached on a comment opening on a lowercase identifier.

A bulleted list terminating its items with semicolons appeared in
exactly one place tree-wide, `spec/type-model.md:24-25`, and its own
last item already ended in a period. It was punctuation to make
consistent rather than the list construction the semicolon exemption
protects, which a line-based checker could not see across bullets in any
case.

## One adjacent fix, flagged because it widens scope

Rewriting `docs/devcontainer.md` and `docs/ssh-signing.md` with a script
rather than an editor took both from CRLF to LF wholesale. Every prose
and Markdown gate stayed green, and `editorconfig-checker` was the only
one that reported it.

`OPERATIONS.md` scoped that instruction to a *new* file, so it is
widened here to a scripted rewrite of an existing one. Hub-only,
`presence` fidelity, no re-vendor owed. Happy to split it out if it does
not belong in this batch.

## Verification

Run from the repository root:

```text
prose_lint.py gating list              exit 0
prose_lint.py backlog list             131/15 -> 41/6, the carried six exactly
test_prose_lint.py                     198 tests OK
test_repo_gate.py, test_pr_review.py   OK
audit.py --selftest, gh-write-guard    SELFTEST PASS
repo_gate.py, validate.py, jq          clean
markdownlint-cli2 (44 files)           0 issues
editorconfig-checker                   0 errors
```

Every prose file changed line for line, with no line-count change in any
of the 9, and the full word-diff was read for meaning drift.

`TODO.md` carries the updated `Checked` anchor and four new `Settled`
lines: the batch figures, the two rejected exemptions, the one-off
bullet list, and the CRLF gap.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…606)

Takes the last batch of the `The Prose Content Backlog` cluster from
[`TODO.md`](./TODO.md), following
[#600](#600) (snippets),
[#604](#604) (comments)
and [#605](#605)
(hub-only Markdown). Closes
[#519](#519) on the
prose, subject to the sweep below.

## The anchor was re-verified first

`develop` at `d791930`, where `python3 scripts/prose_lint.py --summary`
reports **41 violations across 6 files**, all `semicolon`. That is the
figure #605 predicted, and merging it made the figure develop's own
rather than a branch's.

| File | Findings | Fidelity |
| --- | ---: | --- |
| `GOVERNANCE.md` | 14 | `intent`, 5 sections `verbatim` |
| `WORKFLOW.md` | 14 | `intent`, whole |
| `CODESTYLE.md` | 6 | `intent`, whole |
| `.github/copilot-instructions.md` | 4 | `intent`, whole |
| `repo-config/README.md` | 2 | `intent`, whole |
| `HISTORY.md` | 1 | `presence` |

**The tree is now at 0 across 0**, from 557 across 45 when the backlog
opened.

## The exemption came first, and left the checker alone again

Two candidates, both rejected by the corpus. That is the third batch
running where the pass ends in "do not touch the checker".

**The `*Prevents: a; b.*` tail.** Flagged on 4 lines because the list
exemption requires the items to carry commas. The tree already spells
the same construct with a comma on **5** lines against **6**
semicolon-spelled, so it is a convention half-adopted rather than a
construct the rule fails to describe. Two of the six (`D1.6`, `D5.6`)
keep their semicolons untouched, because their items *do* carry commas —
the rule working, not an exception to it.

**A two-example parenthetical**, `(A does X; B does Y)`. The tree
already spells this with a comma where the items carry none,
`GOVERNANCE.md` "No-op republish guarantee" being the case in point.

## Only 14 of the 41 are actually byte-locked

The cluster entry said the carried half "rewrites byte-locked sections".
That is true of `GOVERNANCE.md` and of nothing else, which changes what
the sweep owes:

- **`verbatim`, hash-detected.** All 14 `GOVERNANCE.md` findings sit in
five `verbatim` sections: Branching Model, Release Model, Documentation
Style Conventions, PR Review Etiquette, Workflow YAML Conventions. Every
downstream copy is now byte-mismatched and `spec/audit.py` classifies it
**stale** (matches a past hub revision), which is the correct
disposition and reaches the fleet without anyone filing anything.
- **`intent`, detected by nothing.** `WORKFLOW.md`, `CODESTYLE.md`,
`.github/copilot-instructions.md` and `repo-config/README.md` are judged
by meaning, so a punctuation-only edit produces no hash and therefore no
audit finding at all. These are the half that would be silently missed,
which is why they are written down rather than left to the run.
- **`presence`.** `HISTORY.md` is each repo's own changelog. Its one fix
owes nothing downstream.

No rule changed meaning anywhere, so the re-vendor is a **hash refresh
rather than a propagation**. A repo still holding the old copy is
correct on the rule and wrong on the bytes.

The Fleet Sweeps entry "Re-vendor the changed `verbatim` content" grows
from **five files to seven** and carries both halves as `Detail` lines.
It does not trigger a dedicated sweep — Blog remains the pilot.

## What #519 got wrong, and why it is worth recording

The issue states that the governance files were clean and that this was
"not a carry problem". That was true of the checker of the day and false
of the tree:

| Content | Checker | Result |
| --- | --- | ---: |
| the six files at `69688ec` | that commit's own `prose_lint.py` | **0**
|
| the six files at `69688ec` | today's `prose_lint.py` | **38** |

`semicolon` was in `DEFAULT_RULES` the whole time. What changed is the
**unit** the list exemption is judged over: scoping it to a sentence
rather than a whole bullet accounts for **37 of the 38**, because a
colon anywhere ahead of the first semicolon had been exempting every
semicolon after it, however plainly one joined two clauses.

So the carry problem was real from the start and invisible — the
stale-exemption hazard running in the loose direction, on the
most-carried files in the fleet.

## One fix beyond the 41

`GOVERNANCE.md` "Orchestration vs. build" carried a splice the exemption
excused only because the **bold label** on that sentence happened to
contain a comma (`...globs it; do not switch a single-target repo...`).
Same line as a flagged finding, so it is fixed inline rather than
stepped around. That is 42 semicolons removed, not 41.

## Verification

Run from the repository root:

```text
prose_lint.py --summary            41/6 -> 0/0
scripts/ unittest (375 tests)      OK
spec/audit.py --selftest           SELFTEST PASS
spec/validate.py                   22 cataloged, 0 backlog
scripts/repo_gate.py               eol 0, sha-pin 0
markdownlint-cli2 (44 files)       0 issues
cspell (README.md, HISTORY.md)     0 issues
editorconfig-checker               0 on tracked files
```

Every edit is a **within-line** replacement, so all seven CRLF files
stayed CRLF — verified by an `\n` versus `\r\n` count per file, not by
`file`. The full word-diff was read for meaning drift; the change is 31
lines, 31 insertions and 31 deletions.

`editorconfig-checker` reports 221 errors, every one of them under
`.artifacts/` or `Tests/obj/` — untracked pre-repurpose build leftovers
in the working tree, not tracked files. Pre-existing and untouched here.

## `TODO.md`

- The `The Prose Content Backlog` cluster is deleted, per selection rule
9.
- [#519](#519) moves to
"Verified Complete, Awaiting Close" with both of its questions answered
and the checker-versus-tree finding as closing evidence.
- The Fleet Sweeps re-vendor entry grows to seven files and gains the
two `Detail` lines above.
- Four link reference definitions orphaned by the cluster deletion are
dropped.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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:

- Shape inventory: **0** items raised across all 332 bodies, and **0**
across every review and comment author on those 120 pull requests
(`copilot-pull-request-reviewer` and `ptr727`).
- Coverage: `full 299 / unstated 28 / partial 5`, the five being exactly
#476 (x2), #479 and #592 (x2).
- Live: #592 reports `coverage=PARTIAL shapes=ok` and exits `42`. #595,
#604 and #606 report `shapes=ok` and exit `0`.

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](https://claude.com/claude-code)

---------

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

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

Promotes the current develop branch state to main, carrying forward the completed prose/backlog cleanups and the tightened “fail-closed” review-loop and gating mechanics so main becomes the new baseline for downstream repos and the fleet audit tooling.

Changes:

  • Updates governance/spec/docs to incorporate “Local Verification” as the pre-merge operations entry point and to reflect the latest review-loop/gating contract.
  • Enhances scripts/repo_gate.py SHA-pin checking by optionally resolving pins against GitHub (scoped to the scanned repo’s owner), with accompanying unit tests.
  • Refreshes catalog snippets and workflow/doc comments to align with current prose/style rules and clarified operational guidance.

Reviewed changes

Copilot reviewed 61 out of 62 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
STANDUP.md Updates baseline repo standup guidance; adds/clarifies OPERATIONS.md heading set and rationale.
spec/type-model.md Prose clarification and consistency updates in the type model doc.
spec/section-model.md Updates destinations/requirements text, including OPERATIONS.md headings and “Local Verification” rationale.
spec/scope-model.md Prose clarifications for rule scoping and selector semantics.
spec/readme-structure.md Adjusts TODO.md minimum-shape guidance to match grouped-form reality.
spec/fidelity-model.md Prose edits clarifying fidelity/normalization semantics.
spec/fidelity_honesty.py Comment/documentation clarifications; no functional change indicated in the diff.
scripts/test_repo_gate.py Adds comprehensive tests for SHA-pin resolution behavior and gh error classification.
scripts/repo_gate.py Adds owner detection and GitHub-backed pin resolvability checks plus note-reporting plumbing.
reports/divergences.md Regenerates/updates divergences report content to reflect current dispositions and gaps.
repo-config/README.md Prose clarification about secrets/publishing mechanisms.
README.md Adds “term definitions” section and missing link references for hub/fleet concepts.
OPERATIONS.md Adds “Local Verification” section and updates local gate invocations and known gh limitations.
host-setup/agent-safety/README.md Updates host safety block descriptions and clarifies what is hook-enforced vs prose-only.
host-setup/agent-safety/install.sh Refines wrapper comments; no logic changes.
host-setup/agent-safety/install.ps1 Refines wrapper comments; no logic changes.
host-setup/agent-safety/claude-md-safety.md Renames/expands safety heading and adds checkout-boundary guidance.
host-setup/agent-safety/claude-md-fleet.md Clarifies “hub” definition and adds explicit “own checkout” reach rule.
HISTORY.md Minor prose edit for clarity in release history.
docs/token-cost.md Prose edits for clarity/structure.
docs/ssh-signing.md Prose edits for clarity/structure.
docs/repo-config-carry.md Prose edits and clarifications about carry/apply/regen procedure.
docs/devcontainer.md Prose edits for clarity/structure.
CODESTYLE.md Prose edits for clarity/structure in style guidance.
catalog/snippets/workflows/run-periodic-codegen-pull-request.yml Comment restructuring for prose rule compliance.
catalog/snippets/workflows/README.md Prose edits in snippet overview/mapping table text.
catalog/snippets/workflows/publish-plan-task.yml Comment restructuring for prose rule compliance.
catalog/snippets/workflows/publish-docker-readme-task.yml Comment restructuring for prose rule compliance.
catalog/snippets/workflows/get-version-task.yml Comment restructuring for prose rule compliance.
catalog/snippets/workflows/check-upstream-version-task.yml Comment restructuring for prose rule compliance.
catalog/snippets/workflows/build-release-task.yml Comment restructuring for prose rule compliance.
catalog/snippets/workflows/build-pypilibrary-task.yml Comment restructuring for prose rule compliance.
catalog/snippets/workflows/build-nugetlibrary-task.yml Comment restructuring for prose rule compliance.
catalog/snippets/workflows/build-executable-task.yml Comment restructuring for prose rule compliance.
catalog/snippets/workflows/build-docker-task.yml Comment restructuring for prose rule compliance.
catalog/snippets/workflows/build-datebadge-task.yml Comment restructuring for prose rule compliance.
catalog/snippets/vscode/README.md Prose edits; emphasizes extension list formatting and punctuation.
catalog/snippets/husky/README.md Prose edits for clarity/structure.
catalog/snippets/husky/pre-commit Comment restructuring for prose rule compliance.
catalog/snippets/devcontainer/python/post-create.sh Comment restructuring for prose rule compliance.
catalog/snippets/configs/docker-hub-readme.md Adjusts definition-list formatting for prose rule compliance.
catalog/snippets/configs/dependabot.yml Comment restructuring for prose rule compliance.
catalog/snippets/configs/codecov.yml Comment restructuring for prose rule compliance.
catalog/README.md Prose edits for clarity/structure and consistent list punctuation.
AGENTS.md Clarifies “hub” definition and adds explicit “own checkout” reach rule.
.github/workflows/validate-task.yml Adds gh-write-guard selftest; updates which prose rules gate vs warn-only.
.github/workflows/test-pull-request.yml Comment restructuring for prose rule compliance.
.github/workflows/publish-release.yml Comment restructuring for prose rule compliance.
.github/workflows/merge-bot-pull-request.yml Comment restructuring for prose rule compliance.
.github/dependabot.yml Comment restructuring for prose rule compliance.

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

Comment thread scripts/repo_gate.py Outdated
…610)

Raised by Copilot on the promotion pull request #609, against code that
promotion carries.

## The defect

`check_sha_pin`'s docstring states the reason the note exists:

> The counts below print on every run so that narrowness is visible
rather than inferred from a clean line.

The code guarded it on a non-zero counter, so it printed on every run
**except** the one it was written for. A repository carrying no workflow
file at all resolves nothing, every counter is zero, and the note
vanished, leaving precisely the clean line the sentence says nobody
should have to infer the check's narrowness from.

The comment inside the guard made the contradiction plain, reading "One
fixed shape every run, so a zero in any position is as visible as a
count" while sitting behind the condition that made it conditional.

Reproduced before the fix, against a directory with no workflows:

```
bad: []
NOTES: []
```

## The fix

The note is unconditional. After it, the same call reports what it did
rather than nothing:

```
resolved 0 pin(s) against GitHub. Read for shape only: 0 under another owner,
0 whose owner could not be compared because this checkout's origin is unreadable,
0 GitHub did not answer for.
```

The existing case,
`test_the_note_carries_every_count_including_the_zeroes`, asserted the
zeroes only where a count sat beside them, which is why the all-zero run
went uncovered. A case now drives that run directly.

`repo_gate.py` on this repository is unchanged in substance and still
reports `45 under another owner`, which is the number that made the
hub's own zero worth surfacing in the first place.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 8, 2026 02:16

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 61 out of 62 changed files in this pull request and generated no new comments.

@ptr727
ptr727 merged commit d4092dd into main Aug 8, 2026
7 checks passed
ptr727 added a commit that referenced this pull request Aug 8, 2026
Follows the #609 promotion, which went partial at 61 of 62 files and
stayed there through two re-requests. That prompted measuring whether
the remedy the gate recommends has ever worked.

It has not.

## The measurement

Over the **332 Copilot review bodies** on the newest 120 pull requests,
read beside `gh pr list --json number,changedFiles,additions,deletions`:

| Question | Answer |
| --- | --- |
| Pull requests that went partial | **4** - #476, #479, #592, and the
#609 promotion |
| Rounds those four produced | **7** |
| Rounds that recovered | **0** - every later round repeated the
identical ratio |
| Files skipped, each time | exactly **1** |

```
PR 592: round 1 = 2/3    round 2 = 2/3
PR 476: round 1 = 10/11  round 2 = 10/11
PR 479: round 1 = 16/17
PR 609: round 1 = 61/62  round 2 = 61/62   (after a re-request on a new head)
```

So the digest was telling a reader to spend a round on a retry that
nothing has ever retried out of.

## Two hypotheses tested and rejected, so they are not re-derived

**Size does not predict it.** The partials changed 502, 629 and 961
lines. Fully covered pull requests on this repository reach **33 files
and 2,219 changed lines**, well past all three.

**The denominator is not itself lossy.** I suspected the reviewer might
drop a file before counting, which would make `N out of N` a lie. It
does not: the stated denominator equals the API's own `changedFiles` on
**103 of 104** pull requests, and the single exception is one whose
branch shrank between rounds. The reviewer counts the file and does not
read it.

## What changed

The three places that stated the remedy now state what the record
supports, swept by grepping the phrasing rather than by fixing the one
the finding pointed at:

- `scripts/pr_review.py` - the `status=COVERAGE_IS_PARTIAL` line, the
digest's `COVERAGE IS PARTIAL` block, and the `status` exit-code
docstring.
- `.github/copilot-instructions.md` - the "Counts unequal" bucket.
- `scripts/README.md` - the coverage paragraph, carrying the
measurements.

The new wording says a re-request has never cleared one, that splitting
is real for a feature branch and unavailable for a promotion whose head
is `develop`, and that the merge decision is the maintainer's, taken
knowing one file has no review. **No exit code or verdict changes**,
since the gate was reporting the state correctly. Only the advice
attached to it was wrong.

## What is left open

`TODO.md` gains an entry under the programmatic-reading cluster, with
the measurements attached: **which** file is skipped, and why. The
reviewer names no file list in these rounds, so it cannot be recovered
from the API, and the pull request page may be the only place it
appears. Whether it is worth escalating to GitHub needs the file first.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Aug 8, 2026
)

Closes #623.

**Disposition** `Amends "Find out which file a partial round skips, and
why re-requesting never clears it."` in the "A Programmatic Reading of a
Copilot Review" cluster. #623 was filed after this repository's last
`TODO.md` edit and appeared nowhere in the file, so selection step 1 was
false until this change.

## What was wrong

The `COVERAGE_IS_PARTIAL` caveat, `scripts/README.md` and the runbook
all said the reviewer names no file list in these rounds, so which file
went unread cannot be read from the API. Measured over 348 Copilot
review bodies on the newest 120 pull requests here and 121 on Blog, that
is false. 91 bodies carry a `| File | Description |` table, and every
table row in the corpus belongs to one of those tables.

## What the measurement says the table is worth

The issue proposed reading a table that names every changed file as
evidence that the count is a reporting artifact. It cannot carry that,
and the corpus is what says so.

| round | states | names | reading |
| --- | --- | --- | --- |
| #476 | 10 of 11 | all 11 | table contradicts the count |
| #592 | 2 of 3 | all 3 | table contradicts the count |
| #479 | 16 of 17 | 16, omitting `GOVERNANCE.md` | table corroborates
the count and names the file |
| #609 | 61 of 62 | 50 of 62 | short by 12 where the count is short by 1
|
| #574 | 33 of 33, full | 32, omitting `TODO.md` | a short table on a
fully covered round |
| #606 | 7 of 7 | names `GOVENANCE.md` | a path no diff carries |

On Blog every table names exactly the changed set, on all seven partial
rounds and on every full round alike. A reading identical under both
outcomes discriminates neither, so a full table is reported as
corroborating nothing rather than as a miscount. #574 kills the
converse, since a short table sits on a round that read everything.

## What ships

- `Q_FULL` selects the pull request's own `files`, windowed at 100 with
`hasNextPage` carried, since a path outside a short window reads exactly
like a path the reviewer left out and the record holds a pull request of
301 changed files.
- `file_table` reads the table, quotations dropped for the reason the
coverage line's are.
- `head_table` takes it from any round covering the current head and
never from a round before a push. Three of the four partials here carry
their table on the pre-push round, describing a diff that push replaced,
and comparing that against the current changed files would name a file
unreviewed on a stale list. Thirteen commits here carry more than one
round, and on one of them a round with a table sits beside a round
without, so which of the two the verdict reads must not decide whether a
table is found.
- `table_against_diff` prints one sentence and decides nothing. It names
an omitted file only where the table is short by exactly what the counts
leave unread **and** names nothing outside the diff, that second guard
existing because #606's typo would otherwise push the real
`GOVERNANCE.md` into the omissions and report it as the file nobody
reviewed.
- Exit `42` is unchanged in every arm, and the caveat keeps its
maintainer-decision framing, which is the issue's third suggestion.

## Verified against live data

#479 the reviewer's own file table omits exactly the 1 file the counts
leave unread, naming GOVERNANCE.md
Blog #60 the reviewer's own file table names all 6 changed files, ... so
it corroborates nothing
    #476/#592/#609   no round covering this head carries a file table

## Gates

228 `test_pr_review` cases, plus `test_prose_lint`, `test_repo_gate`,
`spec/audit.py --selftest`, `spec/validate.py`, both `prose_lint.py`
invocations, `markdownlint-cli2` and `editorconfig-checker`, all green
locally.

Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

pr_review.py does not read a review's file-coverage line, so a partial review reports as a clean pass

2 participants