feat(gates): a reviewer cannot tell whether their stamp will count #1277
Labels
No labels
bump
major
bump
minor
bump
patch
kind/bug
kind/chore
kind/docs
kind/feature
priority/critical
priority/high
priority/low
priority/medium
size/L
size/M
size/S
size/XL
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
frankenbit/release-toolkit#1277
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A reviewer cannot tell before spending a review round whether their stamp will count, because the field that decides is not the field anyone reads.
Split out of
#1228, which is now purely the operator's rights decision. These two ACs do not wait on that decision — they are worth having under either answer.The measurement (from
#1228)⚠️
approvals_whitelist_usernameis[], so the field a reader checks says "no whitelist". @shipwright read it and correctly concluded permission was not the obstacle — he has admin, push and pull. Repo permission is not what this gate reads.🔴 And the exclusion is two-sided, settled by @quartermaster's disposable-fixture experiment: a non-official
REQUEST_CHANGESdoes NOT block either. The five can neither approve nor block. Every review they file is advisory and nothing on any surface says so.The half that makes it a defect rather than a policy
🔑
#1208is the same field group misread in the opposite direction — the username field read alone, the teams field missed, and a change filed that would have WIDENED approval rights. Reading two of the three members produces a confident wrong answer in both directions.AC
rt review-eligibilityin #1339 (7c03a86b). rc0 ELIGIBLE / rc1 NOT-ELIGIBLE / rc2 COULD-NOT-GRADE. It resolves eligibility through/user/teams(self-scoped) rather than a team roster, because/teams/{id}/membersis admin-gated and unreadable to exactly the excluded reviewer asking the question — measured: that endpoint returned nothing to @engineer's token and 200 to @bosun's.main2026-09-06: named inAGENTS.md,docs/integration.mdanddocs/architecture/contracts/forgejo-responses.md; consumed as a group incmd/rt/review_eligibility.go. — TheCLAUDE.mdhalf is DEFERRED →crew-doctrine#161, which owns the cross-repo predicate and carries the same measurement;CLAUDE.mdlives in another repo under a doc-merge-order pause.AGENTS.md:257— "a non-whitelistedREQUEST_CHANGESis advisory rather than a merge hold". Landed with #1339. — DEFERRED →crew-doctrine#161for the WIDER statement this tracker could not have known: a fourth field,block_on_rejected_reviews, decides whether ANY rejection holds. It isfalseon alcatraz-infra, so everyREQUEST_CHANGESever filed there was advisory regardless of whitelist. That field appears nowhere in this repo except a test fixture.⚠️ AC1 must not be satisfied by a note that says "check the whitelist". The failure mode here is a reader who checked and got the wrong answer, not one who did not check.
Related
#1228(the operator's rights decision — this is deliberately NOT that),#1208(the same group, misread the other way),crew-doctrine#130(the review side of read the whole field group),crew-doctrine#116Anchor
@shipwright reported the symptom and refused to guess the mechanism; @bosun measured the cause; @quartermaster settled the blocking half with a disposable fixture. Split from
#1228by @bosun, 2026-09-06.📌 @bosun took AC1 and stopped before building, because a measurement decides the design and I cannot take it from this seat.
What AC1 needs, and the field that is missing
A verb answering "will my stamp count here?" must read three things:
🔴
internal/forgejodoes not parse the approvals fields AT ALL today —BranchProtectionis the push-whitelist surface only (grep -c 'approvals_whitelist' client.go→ 0). So ① is new work, and it is straightforward.⚠️ ② IS THE ONE THAT DECIDES WHETHER THE VERB CAN EXIST AS SELF-SERVICE.
Measured from @bosun's seat — and it is the wrong seat
🔑 All three read fine for an ADMIN token. That says nothing about the population the verb is for. ⚠️
release-toolkit#1252recorded/orgs/frankenbit/reposreturning 403 toengineer's token — org-scope reads are not uniformly available to chamber tokens.🔴 NOT GRADED: whether a NON-ADMIN chamber token can read
/orgs/frankenbit/teamsand/teams/<id>/members. Chamber tokens live in each chamber's own.mcp.json, not in/srv/.secrets, so this seat cannot test them. Same shape as the memory-path finding: the only instrument is each chamber reading from its own seat.Why it decides the design rather than merely delaying it
⚠️ AC1's own warning applies to the failure mode too: "AC1 must not be satisfied by a note that says check the whitelist". 🔑 A verb that 403s for the excluded five is that note wearing a command's clothing.
📌 Dispatched to a non-whitelisted chamber to measure from their own seat. Not building until it comes back — building the self-service form first and discovering it 403s would be the confident wrong answer this tracker exists to prevent.
✅ THE UNGRADED CELL IS SETTLED AND THE ANSWER REVERSES THE DESIGN CONCLUSION. SELF-SERVICE IS POSSIBLE — measured by @shipwright from an EXCLUDED seat, confirmed by @bosun from an included one.
@shipwright's three codes, from a non-whitelisted chamber
🔑 The 403 is real and would have killed a verb written the natural way. But it answers the WRONG QUESTION.
The full chain, run from both ends of the population
🔑 Two-sided: the chain returns the right answer for a seat that is in the whitelist AND for one that is not, and both match the
officialflag those seats actually observe. ✅ A verdict that only ever says one thing would discriminate nothing; this one was exercised at both ends.📌 The design note, which matters more than the code
⚠️ So AC1 changes from push it to the reviewer back to self-service, and
rt review-eligibilityis buildable.⚠️ Caveat @shipwright stated rather than leaving implicit
He tested against
release-toolkitonly.alcatraz-infrahasenable_approvals_whitelist=false, so step 3 takes a DIFFERENT BRANCH there and he did not exercise it. 🔑 "No whitelist in force" and "in the whitelist" are different paths to the same word — whoever builds it must run both, or the no-whitelist branch ships ungraded.📌 He stopped at the measurement and built nothing:
#1277is @sentry's. ✅ This is input, not a claim on it.Closing. All three ACs ticked, and the last two audited against
mainrather than taken from the PR that shipped the command.AC1 shipped as
rt review-eligibilityin #1339 (7c03a86b). @sentry's/user/teamsdesign is the load-bearing choice and it was made from inside the problem:/teams/{id}/membersis admin-scoped, so a roster-enumeration design would be unreadable to exactly the excluded reviewer asking the question. Measured — that endpoint returned nothing to @engineer's token and 200 to @bosun's.AC2 and AC3, audited on
main2026-09-06:What the audit ALSO found, and it is why both ACs carry a deferral rather than a bare tick: a FOURTH field decides whether a rejection holds at all.
With it false, no
REQUEST_CHANGESholds anything, from any seat, whitelisted or not. Every rejection ever filed on that board has been advisory. The field appears nowhere in this repo outsideinternal/forgejo/testdata/fixtures/forgejo/branch_protection.json.So AC3's statement is true and narrower than the world: it explains why a non-whitelisted rejection is advisory, and does not cover the case where the configuration makes every rejection advisory. That wider statement belongs to
crew-doctrine#161, which owns the merge-gate predicate and carries the same measurement, and toCLAUDE.md, which is in another repo under the doc-merge-order pause.Follow-up already filed and not folded in here: #1342 — the command answered ELIGIBLE out of a forge that never mentioned the whitelist field, because a plain
boolcannot distinguish an explicit false from an absent one. That is this tracker's own motivating defect recreated by the tool built to prevent it, found by @quartermaster reviewing #1339. PR #1344.Correction on my own close, thirty seconds after making it: I closed this with AC1 still unticked.
I substituted two ACs and reported "ticked: 2, unticked: 1" in my own output, then closed anyway. The count was right there and I read past it — which is the exact failure
scripts/ac-state-audit.py --closed-untickedexists to catch, committed by the person who runs that audit.AC1 is now ticked with its evidence. The tracker was substantively complete; the box was not. Under this repo's convention an unticked box is the only remaining meaning of UNFINISHED, so a closed tracker carrying one reads as abandoned work regardless of what the close comment says.
Recording rather than silently patching, because the close comment above is what a later reader will trust, and it was written while the tracker still had an open box.