review-verdict.yml decided whether an existing review-verdict/h10 was a verdict worth inheriting by testing .creator.login != null. That is satisfied by any account's credential. RENOVATE_TOKEN is a write:repository PAT of the renovate bot; unlike #697's registry
credential it cannot be scoped off status-write (Renovate needs repo write to push branches and
open PRs), and secrets are a per-repo store any workflow can reference — so a status posted with it
read back creator: renovate, non-null, and was inherited.
The test is now membership in H10_REVIEWERS="timothy".
A literal in the base-resolved definition, not a repo vars: entry or a secret: those are
settable without a PR, i.e. without this gate, while .gitea/ is on the PROTECTED path list so the
edit cannot ride a docs-only exemption.
Exact POSIX [ = ] over word-split entries — never [[ == ]] (RHS becomes a pattern), never grep -xF (a newline-bearing creator reads as several patterns).
A ::warning:: names the creator whenever the job re-derives over a verdict-shaped status from an
off-list account, so a reviewer left off the list is not discarded silently.
The part worth reviewing: read_existing_verdict answers TWO questions
It is called twice, and the callers want opposite widths. The first draft gated one flag and so
narrowed both — cold review caught it, and the defect was reproduced red before it was fixed.
Site
Flag
Why
Inheritance short-circuit
ex_human (allow-listed)
Narrowing is safe — an unrecognised account falls through to re-derivation
Last-moment re-read
ex_attributable (broad) + triple changed
Asks "did a reviewer post while we were classifying". Narrowing makes the job stop abstaining and post its exemption over the row — and the post-write repair does not cover that, being skipped whenever the high-water mark could not be established
Post-write raced check
broad on creator != null
See below
The two calls must compute a predicate that "changed" is measurable against. Before the split they
were identical, so anything firing at the second and not the first had arrived in between. Splitting
broke that: a pre-existing row declined for inheritance is still attributable, so the re-read abstained
on the very row the classification had decided to overwrite, left the forgery standing and posted
nothing. Seven tests went red on exactly that. The state/creator/description triple from the first read
is now snapshotted and compared.
Why the raced check stays broad — and not for the reason first written. The first draft said
"narrowing it would leave an exemption success standing over a rejection". Review showed that is not a
checked reason: a real reviewer is on the list by construction. The real reason is the misconfiguration case — a second human account added and forgotten from the literal — where this is
the last net before a green stands, and the errors are asymmetric (a repair to pending is
recoverable; a wrongly-standing success is not). The cost is stated at the site: any verdict-shaped
row landing inside the write window trips the sticky sentinel and costs that head its exemption.
Verification
Mutation-proven against the shipped file, not a hand-written mutant — each applied to the tree, run,
then reverted:
Mutation
Result
membership clause → [ -n "$ex_creator" ]
inheritance cases red
raced jq → .creator.login == "timothy"
polarity test red
mid-run abstain → ex_human
mid-run test red
drop the changed-row comparison
inheritance cases red
if false at the abstain
both mid-run tests red
H10_REVIEWERS = tim* / empty / bob
list-shape guards red (different subsets)
scripts/tests: 968 passed, 2 skipped. test_hook_fire_log.py::test_the_suite_does_not_write_to_the_PRODUCTION_log went red once during a
full run and passes in isolation on both this branch and a pristine origin/main worktree —
cross-session interference on the shared production log (#809/#822), not this change.
Live verification is post-merge, and that is stated rather than assumed.pull_request_target
resolves this definition from main (ci.gate-trigger-base-resolved), so the new behaviour cannot run
on its own PR — the same self-test gap ci.exemption-provenance records for route 1. A live
cross-account probe would additionally require minting a second Gitea account's credential; the
pre-merge evidence is the executed-behaviour tests above, which run the workflow's real classify shell.
What this does NOT close
An allow-listed collaborator's own write-scoped token can still post a verdict nobody formed;
branch protection binds the context name, not its issuer. And post-review-verdict.sh does not check
that its own account is one the gate accepts — a changed writer credential would deadlock PRs rather
than fail loudly. Filed as #845, deliberately not bundled: that script is on the merge-consent path.
Docs
docs/ci-cd.md (Review-verdict gate), ci.exemption-provenance, ci.actions-credential-scoping, release.verdict-status-check, and ci.gate-trigger-base-resolved — the last found by sweeping for the old wording rather than only the files this change touched; it stated in both its rule and its body
that a RENOVATE_TOKEN status "carries a real creator and IS inherited". Catalog regenerated.
Review history — 11 rounds, and what they changed
Recorded because several rounds changed the DESIGN, not just the diff.
Round
Family
Outcome
1
Claude ×2
The allow-list also narrowed the mid-run abstain, a mirror-image site. Reproduced red, then split ex_attributable (broad) from ex_human (allow-listed)
2
Claude ×2
My own split broke the invariant that made the second read work — both calls used to compute an identical predicate. Added the snapshot triple
3
Claude ×2
Guards were right, the suite couldn't tell: deleting the annotation, any one triple clause, or making the membership loop last-entry-only all shipped green
4
Claude
The annotation's guard was the diff's only green mutation and its "pin" could not fail → withdrew the annotation
5
Codex
The failure short-circuit's attributability was unpinned; three pre-existing post-write BLOCKERs → #849
6
Codex
"Pre-existing" was only textually true — this change alters their reachability. Fixed two
7
Codex
Those fixes were worse than the gap (generic pending is re-derived anyway; no retry trigger exists) → withdrew them
8–10
Codex
Retracted the safety claims the deferred routes disprove; three sweeps, each finding more paraphrases → recorded a scope boundary in #849
11
Claude
MERGEABLE. Nine mutations, all reddened; dropping one triple clause reddens exactly one case
Two mechanisms were withdrawn rather than patched a third time, per this repo's own rule. Both withdrawals are recorded in ci.exemption-provenance so the next reader doesn't re-attempt them the same way.
Follow-ups filed:#845 (post-review-verdict.sh doesn't check its own account is allow-listed) and #849 (post-write verification: three routes leaving an exemption success over a human failure, plus the retarget fence's post-POST gap).
fixes #742
## What shipped
`review-verdict.yml` decided whether an existing `review-verdict/h10` was a verdict worth
**inheriting** by testing `.creator.login != null`. That is satisfied by any account's credential.
`RENOVATE_TOKEN` is a `write:repository` PAT of the `renovate` bot; unlike #697's registry
credential it **cannot be scoped off status-write** (Renovate needs repo write to push branches and
open PRs), and secrets are a per-repo *store* any workflow can reference — so a status posted with it
read back `creator: renovate`, non-null, and was inherited.
The test is now membership in `H10_REVIEWERS="timothy"`.
- **A literal in the base-resolved definition**, not a repo `vars:` entry or a secret: those are
settable without a PR, i.e. without this gate, while `.gitea/` is on the PROTECTED path list so the
edit cannot ride a docs-only exemption.
- **Exact POSIX `[ = ]`** over word-split entries — never `[[ == ]]` (RHS becomes a pattern), never
`grep -xF` (a newline-bearing creator reads as several patterns).
- A `::warning::` names the creator whenever the job re-derives over a verdict-shaped status from an
off-list account, so a reviewer left off the list is not discarded silently.
## The part worth reviewing: `read_existing_verdict` answers TWO questions
It is called twice, and the callers want **opposite widths**. The first draft gated one flag and so
narrowed both — cold review caught it, and the defect was reproduced red before it was fixed.
| Site | Flag | Why |
|---|---|---|
| Inheritance short-circuit | `ex_human` (allow-listed) | Narrowing is safe — an unrecognised account falls through to re-derivation |
| Last-moment re-read | `ex_attributable` (broad) + triple changed | Asks "did a reviewer post while we were classifying". Narrowing makes the job stop abstaining and post its exemption over the row — and the post-write repair does **not** cover that, being skipped whenever the high-water mark could not be established |
| Post-write raced check | broad on `creator != null` | See below |
**The two calls must compute a predicate that "changed" is measurable against.** Before the split they
were identical, so anything firing at the second and not the first had *arrived* in between. Splitting
broke that: a pre-existing row declined for inheritance is still attributable, so the re-read abstained
on the very row the classification had decided to overwrite, left the forgery standing and posted
nothing. Seven tests went red on exactly that. The state/creator/description triple from the first read
is now snapshotted and compared.
**Why the raced check stays broad — and not for the reason first written.** The first draft said
"narrowing it would leave an exemption success standing over a rejection". Review showed that is not a
checked reason: a real reviewer is on the list by construction. The real reason is the
**misconfiguration** case — a second human account added and forgotten from the literal — where this is
the last net before a green stands, and the errors are asymmetric (a repair to `pending` is
recoverable; a wrongly-standing `success` is not). The cost is stated at the site: any verdict-shaped
row landing inside the write window trips the sticky sentinel and costs that head its exemption.
## Verification
Mutation-proven against the shipped file, not a hand-written mutant — each applied to the tree, run,
then reverted:
| Mutation | Result |
|---|---|
| membership clause → `[ -n "$ex_creator" ]` | inheritance cases red |
| raced `jq` → `.creator.login == "timothy"` | polarity test red |
| mid-run abstain → `ex_human` | mid-run test red |
| drop the changed-row comparison | inheritance cases red |
| `if false` at the abstain | both mid-run tests red |
| `H10_REVIEWERS` = `tim*` / empty / `bob` | list-shape guards red (different subsets) |
`scripts/tests`: **968 passed, 2 skipped**.
`test_hook_fire_log.py::test_the_suite_does_not_write_to_the_PRODUCTION_log` went red once during a
full run and passes in isolation on **both** this branch and a pristine `origin/main` worktree —
cross-session interference on the shared production log (#809/#822), not this change.
**Live verification is post-merge, and that is stated rather than assumed.** `pull_request_target`
resolves this definition from `main` (`ci.gate-trigger-base-resolved`), so the new behaviour cannot run
on its own PR — the same self-test gap `ci.exemption-provenance` records for route 1. A live
cross-account probe would additionally require minting a second Gitea account's credential; the
pre-merge evidence is the executed-behaviour tests above, which run the workflow's real classify shell.
## What this does NOT close
An **allow-listed** collaborator's own write-scoped token can still post a verdict nobody formed;
branch protection binds the context *name*, not its issuer. And `post-review-verdict.sh` does not check
that its own account is one the gate accepts — a changed writer credential would deadlock PRs rather
than fail loudly. Filed as **#845**, deliberately not bundled: that script is on the merge-consent path.
## Docs
`docs/ci-cd.md` (Review-verdict gate), `ci.exemption-provenance`, `ci.actions-credential-scoping`,
`release.verdict-status-check`, and `ci.gate-trigger-base-resolved` — the last found by sweeping for the
**old wording** rather than only the files this change touched; it stated in both its rule and its body
that a `RENOVATE_TOKEN` status "carries a real creator and IS inherited". Catalog regenerated.
---
## Review history — 11 rounds, and what they changed
Recorded because several rounds changed the DESIGN, not just the diff.
| Round | Family | Outcome |
|---|---|---|
| 1 | Claude ×2 | The allow-list also narrowed the **mid-run abstain**, a mirror-image site. Reproduced red, then split `ex_attributable` (broad) from `ex_human` (allow-listed) |
| 2 | Claude ×2 | My own split broke the invariant that made the second read work — both calls used to compute an identical predicate. Added the snapshot triple |
| 3 | Claude ×2 | Guards were right, the suite couldn't tell: deleting the annotation, any one triple clause, or making the membership loop last-entry-only all shipped green |
| 4 | Claude | The annotation's guard was the diff's **only** green mutation and its "pin" could not fail → **withdrew the annotation** |
| 5 | Codex | The `failure` short-circuit's attributability was unpinned; three pre-existing post-write BLOCKERs → **#849** |
| 6 | Codex | "Pre-existing" was only *textually* true — this change alters their **reachability**. Fixed two |
| 7 | Codex | Those fixes were worse than the gap (generic `pending` is re-derived anyway; no retry trigger exists) → **withdrew them** |
| 8–10 | Codex | Retracted the safety claims the deferred routes disprove; three sweeps, each finding more **paraphrases** → recorded a scope boundary in #849 |
| 11 | Claude | **MERGEABLE.** Nine mutations, all reddened; dropping one triple clause reddens exactly one case |
Two mechanisms were **withdrawn** rather than patched a third time, per this repo's own rule. Both withdrawals are recorded in `ci.exemption-provenance` so the next reader doesn't re-attempt them the same way.
**Follow-ups filed:** #845 (`post-review-verdict.sh` doesn't check its own account is allow-listed) and #849 (post-write verification: three routes leaving an exemption `success` over a human `failure`, plus the retarget fence's post-POST gap).
`review-verdict.yml` left an existing `review-verdict/h10` alone when it was "positively
identifiable as a human verdict" — a non-null `.creator.login` plus a `Review-verdict:`
description. Non-null only proves SOME user credential posted it, and this repo stores
credentials for accounts that are not reviewers. `RENOVATE_TOKEN` is a `write:repository`
PAT of the `renovate` bot; unlike #697's registry credential it cannot be scoped off
status-write (Renovate needs repo write to push branches and open PRs), and secrets are a
per-repo store any workflow can reference — so a status posted with it read back
`creator: renovate`, non-null, and was inherited as a verdict.
The test is now membership in `H10_REVIEWERS="timothy"`, a literal in the base-resolved
definition. Not a repo `vars:` entry or a secret: those are settable without a PR, i.e.
without this gate, while `.gitea/` is on the PROTECTED path list so the edit cannot ride a
docs-only exemption. Membership is an exact `[ = ]` comparison over word-split entries,
never `grep -xF` — an API-supplied creator containing a newline would be read as several
patterns, any one of which could match an allow-listed line.
The allow-list is applied to INHERITANCE ONLY. The post-write raced-verdict check reads the
same `.creator` field for the opposite purpose — "did anything human land while we were
writing" — so narrowing it would not make it stricter, it would make it count fewer rows,
and a raced verdict from an off-list account would leave the exemption `success` standing
over a rejection. That is the one path in this job whose failure direction is toward
SUCCESS, so it stays broad on `creator != null` and the repair is allowed to be over-eager.
Both directions are mutation-proven against the shipped file, not a hand-written mutant:
reverting the membership clause to `[ -n "$ex_creator" ]` reddens the seven new inheritance
cases; narrowing the raced jq to `.creator.login == "timothy"` reddens
`test_the_RACED_verdict_check_still_counts_a_NON_allowlisted_creator` alone; `H10_REVIEWERS`
set to `tim*`, empty, or `bob` reddens the two list-shape tests. `scripts/tests`: 965 passed,
2 skipped.
What this does NOT close, unchanged: an allow-listed collaborator's own write-scoped token
can still post a verdict nobody formed. Branch protection binds the context NAME, not its
issuer, and Gitea offers no way to restrict who may post a context.
fixes#742
Decisions-Edit: yes
Cold review round 1 (two independent agents) found the first draft narrowed BOTH
readers of the verdict row, not just inheritance.
* `read_existing_verdict` is called twice. The second call is the last-moment re-read,
which asks "did a reviewer post a verdict while we were classifying" — the mirror-image
question. Gating the single flag made the job stop abstaining and post its exemption
straight over a mid-run row, and the post-write repair does NOT cover that: it is skipped
whenever the high-water mark could not be established. Reproduced red first, then fixed:
`test_the_MID_RUN_abstain_still_fires_for_a_NON_allowlisted_creator`.
* Splitting the flags broke the invariant that made the re-read work at all — the two calls
used to compute an IDENTICAL predicate, so anything firing at the second and not the first
had arrived in between. A pre-existing row declined for inheritance is still attributable,
so the re-read abstained on the very row the classification had decided to overwrite and
posted nothing. Seven tests went red on exactly that. The state/creator/description triple
from the first read is now snapshotted and compared, making "changed" explicit.
* The stated reason for keeping the post-write raced check broad was asserted, not checked:
a genuine reviewer is on the list by construction, so membership there would still count
every real verdict. Replaced with the real reason (the misconfiguration case — a second
human account forgotten from the literal) and the real cost (a verdict-shaped row landing
in the write window trips the sticky sentinel and costs that head its exemption).
* A `::warning::` now names the creator whenever the job re-derives over a verdict-shaped
status from an off-list account, so a reviewer left off the list is not discarded silently.
* The list guard uses `findall` + a single-assignment assertion: `search` validates the
FIRST assignment while the shell runs the LAST executed one.
* Mutation counts dropped from the decision record in favour of test names — nothing
re-executes a number written there.
Mutation-proven: mid-run site back to `ex_human` reddens the mid-run test; dropping the
changed-row comparison reddens the inheritance cases; `if false` at the abstain reddens both
mid-run tests.
fixes#742
Decisions-Edit: yes
Sweeping for the OLD wording rather than only the sites the change touched turned up
`ci.gate-trigger-base-resolved`, which stated in both its rule and its body that a
RENOVATE_TOKEN-posted status 'carries a real creator and IS inherited'. True until this
change, and a record that reads as a checked fact is worse than none.
Also files #845 for the coupling the shell/polarity review raised and this change does not
close: post-review-verdict.sh posts with whatever account owns the credential and never
checks it is one the gate accepts, so a changed writer credential deadlocks PRs rather than
failing loudly. Recorded as a residual with the live verification behind it, not asserted away.
Decisions-Edit: yes
Round-2 cold review (two independent agents) returned BLOCKED. Both found the same class
of hole from different angles.
FAIL-OPEN, and a regression against main. The short-circuit branched on success OR failure
under one membership test, so an off-list `failure` was re-derived. Trace it: a real
reviewer left off the literal posts BLOCKED; ex_human=no so the short-circuit declines; the
PR is docs-only so the classifier exempts it; the re-read triple never changed so the
mid-run abstain declines; the row predates the high-water mark so the post-write repair
cannot see it. The job posts success over an explicit rejection — reached by the very
misconfiguration the allow-list's own diagnostic is about. `failure` now needs only
attributability, which costs nothing the allow-list bought: inheriting a rejection cannot
green anything, and the worst a forged one achieves is a stall that any human clears in one
command (post-review-verdict.sh POSTs unconditionally).
THE CHANGED-ROW COMPARISON WAS NOT PINNED. A presence-only version (`[ -z "$pre_state" ]`)
passed all 158 tests, because every mid-run test started from an empty head and exercised
only the ARRIVAL case. The REPLACEMENT case is the one that co-occurs with #742's own
scenario. Stub knob STUB_PRE_ROW plus a test; the presence-only mutant now reddens.
Also from the round: the ::warning:: fired with a self-contradictory message for an
allow-listed creator in a non-terminal state, and interpolated an API-supplied value into a
workflow command unstripped; a quoted test count that no longer matched the tree; the
function's own contract line omitted the flag this change adds; a comment naming ex_human
where it is now ex_attributable; and an unprobed claim about how Gitea renders ::warning::,
now withdrawn rather than restated.
MEASURED rather than asserted: two POSTs of context probe742/combined-newest to one sha
(pending, then success) came back as ONE row on /commits/{sha}/status — the newest — and as
BOTH on /statuses/{sha}. That claim became load-bearing with the changed-row comparison;
main depended on presence alone. The probe left the sha's combined state `success`.
Recorded, not silently gained: an attributable row in a state other than success/failure
used to stall a PR forever on main (neither short-circuit fired, the old unconditional
abstain did). The changed-row comparison removes that stall.
scripts/tests: 969 passed, 2 skipped.
fixes#742
Decisions-Edit: yes
Round-3 cold state-machine review returned MERGEABLE (no fail-open, no permanent stall, all
five clause mutations pinned) with three LOWs, all measured rather than argued.
* The re-derivation `echo` asserted 'is NOT an attributable reviewer verdict' on a path also
reached by an ALLOW-LISTED creator whose row is in a state other than success/failure —
printing "creator='timothy', allow-list='timothy'". The ::warning:: below it was already
guarded against exactly that contradiction and the echo was not, which is the same defect
one line up. Now state-neutral.
* The failure short-circuit's message said an explicit rejection is left alone 'whoever
recorded it', but the guard requires attributability: a failure with a non-verdict
description, a null creator, or a DIFFERENT recorded base is still re-derived. Message
qualified rather than the guard widened — a bare [ state = failure ] would be safe by the
clause's own argument, but it would quietly opt `failure` out of the base binding, and
none of the excluded shapes is producible by post-review-verdict.sh. Written down so the
next reader does not 'simplify' it.
* ci.exemption-provenance cited a test that the success/failure split had renamed — a
mutation proof nobody could run. Fixed, and the class is now guarded: a new test derives
the cited names from the record's own text and asserts each is defined in scripts/tests.
Mutation-proven against the pre-fix wording.
scripts/tests: 970 passed, 2 skipped.
fixes#742
Decisions-Edit: yes
Round-3 cold tests/prose review returned BLOCKED. Its findings were all of one shape: the
clauses are correct, but the suite could not tell.
Measured green with the mechanism DELETED, now each reddens on exactly one test:
* deleting the ::warning:: entirely — 166 green. It is not decoration: docs/ci-cd.md heads a
paragraph with 'If a reviewer's verdicts stop being honoured, this is why', and
ci.exemption-provenance uses its existence to soften the #845 residual. Asserted present
in the off-list case, and ABSENT for an allow-listed reviewer, which also pins the
ex_human != yes conjunct.
* dropping any ONE clause of the changed-row triple, or reducing the triple to a single
comparison. Both mid-run tests moved all three fields at once, so 'compare the whole row'
was argued, not measured. STUB_MIDRUN_ROW plus one parametrised case per field.
* a membership loop that can only ever match its LAST entry. Every behavioural test ran the
singleton list, under which that is indistinguishable from a correct loop — and the
annotation this change adds tells an operator to ADD a second entry, so the two-entry path
is the first one exercised in anger. A reviewers= override substitutes a two-entry literal
into the real step body; first-entry, last-entry control and absent-account control.
Prose, all measured by the reviewer rather than argued:
* 'was inherited as a human verdict' described an event nobody observed — renovate has never
posted a commit status in this repo (every row on all six Renovate-authored PR heads carries
creator: null). The asymmetry underneath IS measured; the ROUTE is an inference. Conditional
voice in all three places, with the distinction stated.
* the ::warning:: does not fire for a row recording a DIFFERENT base — the base binding
re-derives it before the annotation is reached. 'whenever' was false.
* a credential drift re-derives to an exemption success on an exempt PR, not only to pending —
the quieter and worse half.
* the live account-set measurement now carries its two curl paths.
* CLAUDE.md states the H10 convention and now carries the constraint: the credential you post
with must be an allow-listed account, or the verdict is written, reported as success, and
silently re-derived (#845).
* residual session chronology trimmed from the record.
STUB_PRE_ROW now emits the measured shape (total_count, a decoy context) rather than a
minimal one.
scripts/tests: 976 passed, 2 skipped.
fixes#742
Decisions-Edit: yes
Round-4 cold review found the annotation's `ex_human != yes` conjunct was the diff's only
GREEN mutation, and that the assertion captioned as pinning it could not fail: both
parametrizations of that test exit at a short-circuit two guards earlier and never reach the
block at all. A caption claiming a proof is worse than no assertion.
WITHDRAWN rather than patched a third time. That mechanism produced three defects in three
rounds — a message reading "from 'timothy', which is NOT in H10_REVIEWERS ('timothy')", an
unescaped API value interpolated into a workflow command, and now a vacuous pin — and it
duplicated all three values from the plain log line one line above it. Its only delta was a
severity whose rendering could not be probed. Deleting it removes the green mutation, ~28
comment lines, the vacuous assertion and a docs paragraph that had promoted it to a contract.
The log line keeps the whole diagnostic and gains the remediation sentence, and its three
parts are now asserted on a path the job always reaches. Both halves mutation-proven:
dropping the remediation reddens, and so does dropping the allow-list VALUE — an earlier
assertion tested for the WORD `H10_REVIEWERS`, which the remediation sentence satisfies on
its own, so that deletion had stayed green.
Also: the comment justifying POSIX `[ = ]` claimed `[[ == ]]` makes its RHS a pattern. Probed
on bash 5.3 — with a QUOTED RHS it does not; only an unquoted one globs. The invariant is
the quoting, and it is now stated as that, with the untestability claim (which rested on the
false premise) dropped.
Round-by-round narrative trimmed out of the code comments per docs.no-session-narrative; the
rules those blocks carry stay, the chronology moves to the PR.
fixes#742
Decisions-Edit: yes
Cold CROSS-FAMILY review (Codex/GPT-5.6), the first this branch has had — the earlier four
rounds were all one model family.
* The `failure` short-circuit's attributability requirement was NOT pinned. Every existing
`failure` case supplied a well-formed, base-matching, non-null-creator row, so widening the
clause to a bare [ "$ex_state" = failure ] passed the whole suite — the exact widening the
comment at the site says is deliberately not done, because it would opt `failure` out of the
base binding. Three cases now cover a null creator, a non-verdict description and a verdict
recorded against ANOTHER base; all three redden on that mutation.
* The docs described the allow-list more broadly than the code implements it: an existing
`failure` is left alone on the weaker attributability test and needs no membership. Scoped
to an inherited `success` in docs/ci-cd.md, ci.exemption-provenance (rule + body) and
CLAUDE.md, with the exception stated rather than left implicit.
Its three BLOCKERs are REAL but PRE-EXISTING, and filed as #849 rather than fixed here:
an unestablished high-water mark skips the post-write race check; an unreadable post-write
history leaves the green; and the check runs only for state=success, so a generic `pending`
write can bury a rejection a later run then greens. Verified by diff against origin/main —
those three guards are byte-identical and this change touches none of them. #742 is a
provenance change; those are the post-write verification's own residuals, and burying them in
a passing review would be the worst outcome. Recorded in the record, not just the issue.
scripts/tests: 1012 passed, 2 skipped.
fixes#742
Decisions-Edit: yes
Cross-family RE-review of the previous fix round. Its sharpest point: my claim that the
three post-write residuals were 'pre-existing, #742 touches none of them' was only TEXTUALLY
true. The guards are byte-identical to main, but this change alters their REACHABILITY —
before it, an attributable `success` from ANY account short-circuited and the job wrote
NOTHING, so the unverified-write paths were unreachable for that whole class of head.
Re-deriving an off-list `success` is the POINT of #742, and it turns 'no write' into 'a
write'. Two of the three therefore belong here, not in the follow-up:
* An UNESTABLISHED high-water mark now WITHHOLDS the exemption. The post-write race check is
the only thing between 'a reviewer posted failure during our write window' and a green over
a rejection, and it is skipped outright when the mark is missing — so the run asks for a
verdict instead of posting a green it cannot verify.
* An UNREADABLE post-write history now REPAIRS to the sticky sentinel instead of logging 'The
exemption … stands' and doing so. Same uncertainty the page-2 probe already resolves to
raced=1; which of the two reads failed is not a difference in what is KNOWN. The repair
`case` is hoisted out of the readable branch so both converge on it.
Both cost the run its exemption on a transient API failure — the price this file already pays
for the same reason in #751's page-2 rule, recoverable in one command, where a green over a
rejection is not. Mutation-proven: restoring either old behaviour reddens its test.
The withholding guard sits AFTER the mark is established, not with the classification.
$max_id_before does not exist there and referencing it early is an unbound variable under
set -u — the step dies and every PR is stranded with no status. My first placement did exactly
that: 39 tests red. Found by running it, not by reading it.
#849 keeps the third route (post-write verification runs only for state=success, so a generic
pending write can bury a rejection) and the fence's post-POST gap, with its reachability from
this change's class stated rather than dismissed.
scripts/tests: 1014 passed, 2 skipped.
fixes#742
Decisions-Edit: yes
Seventh review round. The previous commit's two post-write fixes are WITHDRAWN — re-review
showed the fix was worse than the gap on both counts, and my own rule applies: two defects
from one mechanism means withdraw, not patch a third time.
* The withheld exemption wrote a GENERIC pending, and a generic pending is exactly what a
later run re-derives into success. It did not close the burial route, it moved which run
greens it. Closing it needs a distinct STICKY 'unverified write' sentinel — a new state in
a machine that already has three.
* That pending has NO RETRY PATH. Verified: review-verdict.yml triggers only on
pull_request_target types, with no schedule and no workflow_dispatch. A transient API
failure on a PR's LAST event therefore stalls an exempt PR until a human nudges it. Trading
a race that needs BOTH a read failure AND a reviewer posting inside the write window, for a
stall that needs only the read failure, is not obviously the safe direction — and I called
it a one-run cost, which it is not.
The workflow and its tests are restored byte-for-byte to the previous commit's state; the
whole of post-write verification goes back to #849, which now also carries two further routes
the same round found (a malformed status row makes the verifier's jq error out under set -e,
skipping the repair; and an unreadable combined-status read leaves a pre-existing off-list
success standing, with the job's own red not being a required check).
Also corrects ci.verdict-write-retarget-fence, whose 'What is NOT closed' said the post-POST
gap yields only a TRANSIENT forged green because the retarget fires an `edited` event. That
assumes the
successor finishes last. It need not: the successor can start, see the base-matching failure,
short-circuit and write nothing, CONSUMING the event — after which the stale run posts its
success last and nothing remains to correct it. Permanent, not transient, and now stated so.
What #742 itself does is unbroken after seven rounds. What it changes is the REACHABILITY of
these residuals, and that is recorded in ci.exemption-provenance rather than left implicit.
scripts/tests: 1012 passed, 2 skipped.
fixes#742
Decisions-Edit: yes
Cross-family review of the withdrawal. It confirmed the revert is exact (both blobs match
the pre-attempt commit, no withdrawn stub modes or tests left behind) and found no BLOCKER
or MEDIUM — but four documentation defects, two of them contradictions this branch itself
introduced by correcting a record's body and leaving its summary alone.
Retracted in the workflow, where the claims are load-bearing for the next maintainer:
* "the POST below is still correct, only the verification is weakened" — without a mark the
race check is skipped entirely, so a verdict in the write window is neither detected nor
repaired. Now says so, and names the withdrawn attempt so it is not re-attempted the same
way.
* "pending cannot turn a rejection green, so the only write that can cause the damage is the
exemption success" — FALSE. A generic pending masks a raced rejection just as well, and
because it is not the sticky sentinel the next run re-derives it into success with the
human's row now below that run's mark. The damage arrives one event later, not never.
* "the LAST net before a green stands" — it is the last net on THAT path only; a rejection
already on the head is below the mark, and the pending route bypasses the block entirely.
Record summaries brought into line with their own bodies:
* ci.verdict-write-retarget-fence and ci.exemption-provenance both still promised the fence
RESOLVES the retarget residual. It narrows it: a retarget between the final pre-write count
and the POST leaves a PERMANENT forged green, because the successor can consume the edited
event and exit before the stale run posts last. Also "overtaken after writing" -> "after the
final pre-write count", which is where the window actually is.
* ci.exemption-provenance said a lost verdict is re-posted as pending; on an exempt PR it is
re-derived to an exemption success, the quieter and worse half.
* ci.actions-credential-scoping still described the pre-742 rule (any real creator inherited).
* The rule frontmatter said [[ == ]] makes its RHS a pattern. Only an UNQUOTED RHS does; the
workflow comment already had this right after being probed on bash 5.3.
No behaviour change. scripts/tests: 1012 passed, 2 skipped.
fixes#742
Decisions-Edit: yes
Sweeping for the RETRACTED wording rather than only the files I had edited — the rule this
repo records as "stale comments: sweep by subject, not verb" — caught two clauses that the
previous commit's own correction contradicts.
ci.exemption-provenance's rule: now says the retarget residual is "NARROWED, not resolved",
and then ended the same sentence with "leaving only the sub-round-trip window that no API
without compare-and-set can close". Both cannot be true. There are TWO windows: the
sub-round-trip one, and the post-final-count/pre-POST one, which is not sub-round-trip and
yields a PERMANENT green because the successor can consume the edited event and exit before
the stale run posts last.
The same record's #706 paragraph carried the same "the sub-round-trip window it describes
survives" framing, with no pointer to the correction. It now points at it.
A retraction that only edits the sentence you remembered writing is the failure mode here:
the previous commit corrected three claims and left two paraphrases of one of them in place,
in the same file.
No behaviour change, and this time MEASURED rather than asserted: the classify step's
executable content (comments and blank lines stripped) is 323 lines at HEAD~1 and 323
identical lines at HEAD.
scripts/tests: 1012 passed, 2 skipped.
fixes#742
Decisions-Edit: yes
Third sweep, and the third time the same mistake: I grepped for the exact wording I had
written and missed the same claim in PARAPHRASE at sites I never opened. Cross-family review
enumerated them. This closes both families by concept.
Family 1 — "the retarget fence resolved the race". Corrected at every site, not just the two
I remembered: the fence record's own rule and its "handoff, not a stall" induction (which is
a property of a run that ABSTAINS and says nothing about one that already passed its final
count and then posts), ci.exemption-provenance's "Resolved 2026-08-03" heading, and
docs/ci-cd.md's "That residual is now fenced". It NARROWS: a retarget between the final
pre-write count and the POST leaves a PERMANENT forged green, because the successor consumes
the edited event and exits before the stale run posts last.
Family 2 — "a pending write is harmless". Retracted in the workflow's untrusted-count
rationale ("for no safety gain"), in the same phrase inside the fence record's rule, and in
docs/ci-cd.md twice, including the absolute "a human BLOCKED can never be silently turned
green". A generic pending blocks the merge IMMEDIATELY, which is what makes letting it
through the right trade — but it is not inert: it masks a rejection landing in its own write
window, post-write verification does not run for it, and a later run re-derives it into an
exemption success with that rejection below the new mark. Letting it through is still right;
the trade is "immediate block, later re-derivation risk", not "free".
Every retraction names #849, which carries the behaviour work.
No behaviour change: the classify step's executable content is identical across all three
docs commits (323 lines, comments and blanks stripped, compared via the YAML-extracted step
rather than line ranges).
scripts/tests: 1012 passed, 2 skipped.
fixes#742
Decisions-Edit: yes
Fourth prose round. Three of the four sites it named are real and fixed; one is a false
positive, recorded so it is not "fixed" later.
Fixed:
* the classify comment heading "THE PROBLEM THIS SOLVES" -> NARROWS, with #849 named;
* ci.verdict-write-retarget-fence's induction, which ended "the induction terminates when
retargeting stops and the last run writes the final answer". That holds only over runs that
ABSTAIN. A run that already passed its final count is outside it — it writes whenever it
gets there — so the run that writes LAST is not necessarily the one that classified last;
* CLAUDE.md's "so a rejection is never re-derived into a green", which is MY sentence from
this branch and is overbroad: it is true of a rejection VISIBLE AT THE FIRST READ, not of
one landing inside a run's own write window;
* two test docstrings carrying the retracted "withholding pending buys no safety", and a
scope note on test_a_REAL_human_verdict_is_still_NEVER_overwritten, whose NAME asserts a
guarantee wider than what it measures.
NOT fixed, deliberately: process.check-and-use-pins-a-version's title says "the gap is stated
and fenced". That is the generic check/action version-binding rule and has nothing to do with
the retarget fence. Matching on the word rather than the concept is what put it on the list.
AND THE BOUNDARY, recorded in #849 rather than left to the next round to rediscover. Each
round corrected the sites it could see and the next found more, because these claims exist in
paraphrase across corpus prose written before #742. #742 has now corrected every site that is
its own text or directly about the allow-list, plus the fence/pending claims in the four docs
and the classify comments. Remaining corpus prose describing the #706 fence's or the
post-write verification's guarantees belongs to #849 — those sentences are wrong only because
of the residual analysis in that issue, so they should be corrected when the behaviour lands,
rather than leaving prose that promises a property no code yet has. #849 has a Done-when box
for it.
No behaviour change. scripts/tests: 1012 passed, 2 skipped.
fixes#742
Decisions-Edit: yes
Last finding from a clean cold round (VERDICT: MERGEABLE, nine mutations applied and every
one reddened). The banner said an inherited success is re-derived unless it is an
'attributable human verdict' — since this change, attributable alone is the weaker test that
governs an inherited FAILURE; a success additionally needs allow-list membership. In scope by
this branch's own boundary rule: it is prose about a property this change alters.
scripts/tests: 1012 passed, 2 skipped.
CI's 'Script lint and tests' went red on `ruff format --check`. `ruff check` was clean and
the suite was green throughout — this was purely formatting of lines this branch added, and
it is a gap in MY local gate: I ran pytest and the decisions validator every round and never
ran the formatter.
Formatted with ruff 0.12.11, the version pr-checks.yml pins, rather than the local install —
the pin exists precisely so the verdict is not a function of whoever ran it, and a different
version could have reformatted lines this branch never touched. The diff is confined to
blocks added here (a re.subn call, two assertion strings, two parametrize entries).
Verified over the whole CI population, not just the file I edited:
git ls-files '*.py' | xargs ruff check -> All checks passed
git ls-files '*.py' | xargs ruff format --check -> 43 files already formatted
scripts/tests: 1012 passed, 2 skipped.
11 cold review rounds, 6 cross-family; final verdict clean with 9 mutations all reddening. Residuals deferred to #845 and #849.
Review-verdict: MERGEABLE @ 586577c
11 cold review rounds, 6 cross-family; final verdict clean with 9 mutations all reddening. Residuals deferred to #845 and #849.
timothy
merged commit d68ce42fcb into main2026-08-27 04:52:03 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
fixes #742
What shipped
review-verdict.ymldecided whether an existingreview-verdict/h10was a verdict worthinheriting by testing
.creator.login != null. That is satisfied by any account's credential.RENOVATE_TOKENis awrite:repositoryPAT of therenovatebot; unlike #697's registrycredential it cannot be scoped off status-write (Renovate needs repo write to push branches and
open PRs), and secrets are a per-repo store any workflow can reference — so a status posted with it
read back
creator: renovate, non-null, and was inherited.The test is now membership in
H10_REVIEWERS="timothy".vars:entry or a secret: those aresettable without a PR, i.e. without this gate, while
.gitea/is on the PROTECTED path list so theedit cannot ride a docs-only exemption.
[ = ]over word-split entries — never[[ == ]](RHS becomes a pattern), nevergrep -xF(a newline-bearing creator reads as several patterns).::warning::names the creator whenever the job re-derives over a verdict-shaped status from anoff-list account, so a reviewer left off the list is not discarded silently.
The part worth reviewing:
read_existing_verdictanswers TWO questionsIt is called twice, and the callers want opposite widths. The first draft gated one flag and so
narrowed both — cold review caught it, and the defect was reproduced red before it was fixed.
ex_human(allow-listed)ex_attributable(broad) + triple changedcreator != nullThe two calls must compute a predicate that "changed" is measurable against. Before the split they
were identical, so anything firing at the second and not the first had arrived in between. Splitting
broke that: a pre-existing row declined for inheritance is still attributable, so the re-read abstained
on the very row the classification had decided to overwrite, left the forgery standing and posted
nothing. Seven tests went red on exactly that. The state/creator/description triple from the first read
is now snapshotted and compared.
Why the raced check stays broad — and not for the reason first written. The first draft said
"narrowing it would leave an exemption success standing over a rejection". Review showed that is not a
checked reason: a real reviewer is on the list by construction. The real reason is the
misconfiguration case — a second human account added and forgotten from the literal — where this is
the last net before a green stands, and the errors are asymmetric (a repair to
pendingisrecoverable; a wrongly-standing
successis not). The cost is stated at the site: any verdict-shapedrow landing inside the write window trips the sticky sentinel and costs that head its exemption.
Verification
Mutation-proven against the shipped file, not a hand-written mutant — each applied to the tree, run,
then reverted:
[ -n "$ex_creator" ]jq→.creator.login == "timothy"ex_humanif falseat the abstainH10_REVIEWERS=tim*/ empty /bobscripts/tests: 968 passed, 2 skipped.test_hook_fire_log.py::test_the_suite_does_not_write_to_the_PRODUCTION_logwent red once during afull run and passes in isolation on both this branch and a pristine
origin/mainworktree —cross-session interference on the shared production log (#809/#822), not this change.
Live verification is post-merge, and that is stated rather than assumed.
pull_request_targetresolves this definition from
main(ci.gate-trigger-base-resolved), so the new behaviour cannot runon its own PR — the same self-test gap
ci.exemption-provenancerecords for route 1. A livecross-account probe would additionally require minting a second Gitea account's credential; the
pre-merge evidence is the executed-behaviour tests above, which run the workflow's real classify shell.
What this does NOT close
An allow-listed collaborator's own write-scoped token can still post a verdict nobody formed;
branch protection binds the context name, not its issuer. And
post-review-verdict.shdoes not checkthat its own account is one the gate accepts — a changed writer credential would deadlock PRs rather
than fail loudly. Filed as #845, deliberately not bundled: that script is on the merge-consent path.
Docs
docs/ci-cd.md(Review-verdict gate),ci.exemption-provenance,ci.actions-credential-scoping,release.verdict-status-check, andci.gate-trigger-base-resolved— the last found by sweeping for theold wording rather than only the files this change touched; it stated in both its rule and its body
that a
RENOVATE_TOKENstatus "carries a real creator and IS inherited". Catalog regenerated.Review history — 11 rounds, and what they changed
Recorded because several rounds changed the DESIGN, not just the diff.
ex_attributable(broad) fromex_human(allow-listed)failureshort-circuit's attributability was unpinned; three pre-existing post-write BLOCKERs → #849pendingis re-derived anyway; no retry trigger exists) → withdrew themTwo mechanisms were withdrawn rather than patched a third time, per this repo's own rule. Both withdrawals are recorded in
ci.exemption-provenanceso the next reader doesn't re-attempt them the same way.Follow-ups filed: #845 (
post-review-verdict.shdoesn't check its own account is allow-listed) and #849 (post-write verification: three routes leaving an exemptionsuccessover a humanfailure, plus the retarget fence's post-POST gap).Round-4 cold review found the annotation's `ex_human != yes` conjunct was the diff's only GREEN mutation, and that the assertion captioned as pinning it could not fail: both parametrizations of that test exit at a short-circuit two guards earlier and never reach the block at all. A caption claiming a proof is worse than no assertion. WITHDRAWN rather than patched a third time. That mechanism produced three defects in three rounds — a message reading "from 'timothy', which is NOT in H10_REVIEWERS ('timothy')", an unescaped API value interpolated into a workflow command, and now a vacuous pin — and it duplicated all three values from the plain log line one line above it. Its only delta was a severity whose rendering could not be probed. Deleting it removes the green mutation, ~28 comment lines, the vacuous assertion and a docs paragraph that had promoted it to a contract. The log line keeps the whole diagnostic and gains the remediation sentence, and its three parts are now asserted on a path the job always reaches. Both halves mutation-proven: dropping the remediation reddens, and so does dropping the allow-list VALUE — an earlier assertion tested for the WORD `H10_REVIEWERS`, which the remediation sentence satisfies on its own, so that deletion had stayed green. Also: the comment justifying POSIX `[ = ]` claimed `[[ == ]]` makes its RHS a pattern. Probed on bash 5.3 — with a QUOTED RHS it does not; only an unquoted one globs. The invariant is the quoting, and it is now stated as that, with the untestability claim (which rested on the false premise) dropped. Round-by-round narrative trimmed out of the code comments per docs.no-session-narrative; the rules those blocks carry stay, the chronology moves to the PR. fixes #742 Decisions-Edit: yesThird sweep, and the third time the same mistake: I grepped for the exact wording I had written and missed the same claim in PARAPHRASE at sites I never opened. Cross-family review enumerated them. This closes both families by concept. Family 1 — "the retarget fence resolved the race". Corrected at every site, not just the two I remembered: the fence record's own rule and its "handoff, not a stall" induction (which is a property of a run that ABSTAINS and says nothing about one that already passed its final count and then posts), ci.exemption-provenance's "Resolved 2026-08-03" heading, and docs/ci-cd.md's "That residual is now fenced". It NARROWS: a retarget between the final pre-write count and the POST leaves a PERMANENT forged green, because the successor consumes the edited event and exits before the stale run posts last. Family 2 — "a pending write is harmless". Retracted in the workflow's untrusted-count rationale ("for no safety gain"), in the same phrase inside the fence record's rule, and in docs/ci-cd.md twice, including the absolute "a human BLOCKED can never be silently turned green". A generic pending blocks the merge IMMEDIATELY, which is what makes letting it through the right trade — but it is not inert: it masks a rejection landing in its own write window, post-write verification does not run for it, and a later run re-derives it into an exemption success with that rejection below the new mark. Letting it through is still right; the trade is "immediate block, later re-derivation risk", not "free". Every retraction names #849, which carries the behaviour work. No behaviour change: the classify step's executable content is identical across all three docs commits (323 lines, comments and blanks stripped, compared via the YAML-extracted step rather than line ranges). scripts/tests: 1012 passed, 2 skipped. fixes #742 Decisions-Edit: yesReview-verdict: MERGEABLE @
586577c11 cold review rounds, 6 cross-family; final verdict clean with 9 mutations all reddening. Residuals deferred to #845 and #849.