Uh oh!
There was an error while loading. Please reload this page.
release-train: staging -> main - #363
Conversation
…336) Step 2 of the reusable's three-step arming order, rolled from the claude-skills pilot (claude-skills#36) to the rest of the fleet. No branch protection is touched and `bugbot / review` is not required: Bugbot does not review Dependabot PRs, so a required context would park every one of them red with no route to green. Reported, not required. repo-inventory.yml is untouched -- caller first, entry after; the flip to `required` is the follow-up. Refs tracebloc/backend#2284 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…2284) (#337) Ten bugbot-gate callers are now live (claude-skills from the pilot, plus nine merged 18:33 today). Their rows still read exempt, which is the stale-exemption finding at caller-drift.py:2309 -- the accepted cost of land-the-caller-first, and this closes it. Derived, not listed: the flip script re-reads repo-inventory.yml from disk and confirms each caller by matching the resolved `uses:` value on that repo own audit branch, so a required row cannot be written ahead of the caller it claims. Ten repos whose PRs have not merged stay exempt. The anchor is RETAINED at ten citations and its text corrected. It has now been wrong twice -- first asserting ZERO callers org-wide, then naming only the pilot -- so it now says outright that its own list is not to be trusted and points at the anchored grep instead. It also carries its own delete condition for when that grep returns zero. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
`edited` is the only pull_request event GitHub fires on a base-branch change. Without it a retargeted PR can never re-run fr-gate: synchronize needs a push, and a retarget has nothing to push. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…appened (#342) * fix(2516,2517): two reasons that describe something other than what happened Both deferred from the .github#334 staging hop under the severity policy, both verified against source before fixing. 2516 -- claude-skills fr-gate. The reason framed the caller absence as the audit DISCOVERING a missing caller, and ended "if the caller was removed by mistake, restore it and flip this back". It was not a mistake: claude-skills#38 deleted fr-gate-caller.yml on purpose (+0/-15) as the remainder of backend#2347, because this repo does not ride the train. The remedy does not run both ways and the reason no longer says it does. 2517 -- the rfcs prod cell claimed reason-citations.py "can at least check that a cited issue is still open" while naming backend#2501 only in a YAML comment, which that script never reads. The claim is now the opposite and says why moving the citation into a scanned string is NOT the fix: the only reason-bearing field there is an alias to a shared anchor. The citation guard then caught this change: the new 2516 text cites backend#2347, which is CLOSED. That is the guard working, so #2347 is EXEMPTed with what it is doing there rather than reworded out to silence it. Also folded in, because repo-inventory.yml is a GUARDED file and two PRs cannot both hold a green audit on their own SHA: the five bugbot-gate rows whose callers merged today. The anchor stops enumerating repos -- that list went stale twice and is now derived. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * ci(2284): the four callers that merged at 07:46 get their rows too Folded onto this PR rather than opened separately: repo-inventory.yml is a GUARDED file needing a green org audit on its own SHA, and callers kept landing while this sat unmerged. Two PRs racing for that green is a deadlock, not a queue. averaging-service, data-ingestors, tracebloc-engine and tracebloc-py-package all merged at 07:46. 19 of 20 rows are now `required`; design-system-v2 is the only one left, and its caller PR is held by a pre-existing Code Connect failure on that repo (backend#2523), not by anything in the rollout. This PR had already passed `audit` -- before those four merged. That green was stale the moment they landed, which is the re-run trap the release-train README documents, one layer out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix: a PR must not cite the issue it closes in a SCANNED string (Bugbot High) The rewritten claude-skills fr-gate reason cited backend#2516 inside an `exempt:` string -- and this PR closes backend#2516. reason-citations.py would then see a citation to a CLOSED issue on every subsequent PR and fail the required selftests job fleet-wide. It could not be pre-EXEMPTed either, because the issue is open until this merges, and an EXEMPT row for a live issue is itself a stale exemption. A self-inflicted trap and a good catch: the citation now names the hop (.github#334) rather than the ticket the merge closes. Also -- the folded scalar lost its indentation AGAIN, from my own edit that removed the stale enumeration. Three continuation lines sat two spaces deeper than the block base, so they stopped folding and injected literal newlines into every repo aliasing the reason. Second time today. Verified this time by loading the file and reading the rendered value rather than by reading the diff, which looked fine both times. And the splice that removed the enumeration had duplicated the claude-skills#36 parenthetical. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(inventory): the blocked-gate reason named two live PRs; neither is live client-runtime#192 was closed unmerged at 08:26 today, which the citation guard caught. Reading the sentence caught the other half: client#490 had already merged. "two live PRs go red the moment it is on" described the fleet at writing time and nothing since. Restated in the past tense, and the citation kept -- the reason now records that #192 was closed rather than leaning on it landing, so the deadness is the content. EXEMPT carries that rationale and says when to delete it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ase they are (#343) Bugbot Medium, deferred from the .github#334 staging hop. The block was created 2026-08-25 by copying ON-TRAIN design-system, so all three promotion-shaped rows arrived `required` while every other off-train repo exempts version-bump and fr-gate. The rows were UNEXPLAINED rather than wrong -- and measured 2026-08-26, the three are not the same case at all: version-bump-gate branches: [develop] -- ACTIVE on every PR here fr-gate branches: [staging, main, master] -- NONE exist, dormant fr-pass-comment issue_comment, no branch filter -- CAN fire, on a repo with no `FR on staging` card to advance All three stay `required` because all three callers EXIST, and an exempt row whose caller is present is the stale-exemption finding at caller-drift.py:2309 -- flipping them would trade an unexplained row for a red audit. Exempting means deleting the callers first, which is an onboarding decision and stays open on backend#2512. Comment-only: no cell value changes. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…suming it (#340) * fix(2441): the mutation harnesses verify their baseline instead of assuming it All seven `scripts/tests/*-mutations.py` runners overwrite a tracked file, run the suite, and restore it in a `finally`. That covers a crash. It does not cover SIGKILL, a runner timeout, or a second harness racing the first in the same worktree -- and the mutation left on disk becomes the NEXT run's `pristine`. Every mutation is then measured against a premise nobody typed, the restore writes the corruption back, and the run reports `0 uncaught`, which is byte-identical to real coverage. Fail-open in the one direction that matters, in the tier whose whole job is proving guards catch bugs. It has already destroyed a tracked file here (`scripts/pipefail-early-close.awk`). `scripts/tests/mutation_baseline.py` refuses to start unless every file the runner is about to mutate matches its committed content at HEAD, and every way of not being able to tell -- unreadable file, git that will not run, no HEAD, untracked path, any git status that is neither "matches" nor "differs" -- refuses too. It does not restore: those bytes might be somebody's work. Only the writing path is guarded. `--dry` writes nothing and is what `make check` runs on every push, where refusing on an uncommitted edit would block the pre-push tier for whoever is editing the target. Closestracebloc/backend#2441 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(2441): derive the runner count instead of writing it down (saqlainsyed007) The module docstring and the selftest comment said "seven" *-mutations.py runners in three places -- and this PR adds the eighth, so all three were wrong the moment it landed. Off-message for a change whose thesis is verify the baseline rather than assume it, and it is the same stale-hardcoded-count trap Makefile:277 already records as having "gone stale twice over". Bumping seven to eight would have been wrong for the same reason it was wrong at seven. The counts are gone instead: the guard already derives the roster by globbing *-mutations.py and fails closed on an empty glob, so there was never a reason for prose to hold a second copy of the answer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(2441): the runner must not ask the mutated guard whether it is mutated Bugbot Medium on #340, and it is this change failing in its own terms. The runner imported `mutation_baseline` from disk and then handed that same in-memory copy `mutation_baseline.py` to certify. A run killed mid-mutation leaves a fail-open `guard()` on disk, so: the import binds the broken one, it certifies its own corruption as clean, `pristine` captures the MUTATION, and the `finally` writes it back for good. Every later harness then imports a guard that no longer guards -- backend#2441 fail-open, one level up, inside the fix for it. It now loads the guard from HEAD (`git show HEAD:<rel>`). Not a second copy of the rule -- that would be the thing that drifts -- but the SAME function read from the one version a mid-run kill cannot have touched. Fails closed on no git, no HEAD, no such path, or a file that will not compile, because the fallback IS the defect. Measured: with a fail-open guard planted in the tree, the runner refuses with exit 2 and leaves the corrupted bytes on disk rather than adopting them. Before this it would have written them back as the baseline. The selftest`s per-runner check matched `mutation_baseline.guard(`, so it would have failed the fix; it now matches `.guard(` -- the question is whether a guard runs before the first write, not how it was bound. Two assertions added for the regression itself, both mutation-proven. Verified: make lint clean, selftests 28 passed 0 failed, 12 mutations 0 stale 0 uncaught. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…s worked (#339) * fix(2526): the archive asserts the board is clean, not that its writes worked Every guard in this workflow answered "did my writes succeed?" and every one of them could answer yes while the board was untouched. That is what happened. MEASURED, 2026-08-26. The 05:14 scheduled run logged: Un-archived terminal items: 63 === Archived: 63 ok, 0 failed === exit 0, green, as it had been for weeks. At 06:24 a human PAT read the same board and found 668 un-archived terminal cards -- 475 Prod, 153 Done, 40 Cancelled. A manual dispatch minutes later reported "Un-archived terminal items: 0". Same board, same query shape, two credentials, two answers. The read logic is not the bug: it paginates correctly and filters correctly. The bug is that "I archived everything I saw" was allowed to stand in for "the board is clean", and nothing measured the difference. This file's own header had already named the failure mode -- "the board could degrade for weeks behind an unbroken green history ... slowly enough to look like normal growth" -- and then guarded only against a FAILED archive, not against an archive that sees nothing. TWO CHANGES. 1. Say what the credential actually saw. Items read, un-archived, and -- the one that matters -- how many returned a NULL Status. A null means the item came back but its field value was unreadable, so it is silently dropped by the filter and reported nowhere. That count is the measurement that separates "the board is clean" from "this credential cannot see the board", and nothing has ever printed it. A non-zero count raises a warning rather than passing quietly. 2. Re-read AFTER archiving and fail on anything terminal still live. Deliberately a SECOND read rather than arithmetic over the first. The defect being caught IS that the first read is incomplete, so comparing it against itself would agree and prove nothing -- the test-a-list-against-itself trap this org's rules name directly. It fails closed: an unreadable re-read exits 1 rather than reporting a clean board, because unverified is not verified-clean. WHAT THIS DOES NOT DO. It does not fix the cause. The `tracebloc-release-train` App is installed on ALL repos with `organization_projects: write`, so narrow repo visibility is refuted; it can also read and write the project, since it resolved the project id and archived 63 items successfully. The remaining candidate is the field read, and the diagnostic above is what will name it on the next scheduled run. Arming the assertion first is deliberate: it means the next silent degradation cannot be silent. Expect this to go RED on its next run, with 668 items still terminal. That is the guard working, not a regression -- and the board it describes was already in that state, unreported. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(2526): the assertion was unreachable in the case it exists to catch Two Bugbot Highs on #339, both real. UNREACHABLE. The board-is-clean re-read sat at the bottom of "Archive them", below `if [ "$n" = "0" ]; then ... exit 0; fi`. When the first read finds nothing terminal -- which is exactly what a credential that cannot see the cards produces, and exactly the silent success this PR is about -- the step exited before asserting anything and the job went green having checked nothing. A guard reachable only when the bug is absent is not a guard. It is now its own step, so it runs whatever the archive did. SAME CREDENTIAL, SAME BLIND SPOT. The old comment claimed a second read avoids comparing the first read against itself. True of the item list; false of what made the incident invisible. On 2026-08-26 the items WERE returned -- their Status came back null for this credential, so they matched no terminal filter and `left` stayed 0. A second read with the same token reproduces that and agrees with itself. So a null Status on an un-archived item is now a refusal rather than the ::warning:: it was, and a re-read that saw fewer items than the first read is a refusal too. Skipped in dry run: a dry run archives nothing, so the board is dirty by construction and this would be permanently red. Verified: make lint, make selftests (95 passed), make shellcheck clean, actionlint clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…345) * fix(2555): the sync PR title stops naming an epic it must not close Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(2555): the body check covers every closing-keyword family, not just Closes Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…hor go (#346) `design-system-v2` took a `bugbot-gate-caller.yml` and its inventory row still said `exempt`, which is the last of the backend#2284 rollout drift. Flipping it takes the anchor's citation count to ZERO. So the anchor is deleted, which is what it asks for on its own face: DELETE THIS DEFINITION when that grep returns zero: an anchor left defined and uncited is dead prose, which this file treats as a defect rather than as harmless. grep -nE '^\s*exempt: \*bugbot_gate_rollout_pending\s*$' repo-inventory.yml -> no matches That is the rollout finishing, not a shortcut: every active repo now carries the caller, so there is no repo left for the exemption to describe. The anchor's text had already been corrected twice as the fleet moved (backend#2507, .github#335), and it says outright that writing a count there was the mistake -- leaving a third stale version behind would repeat it. WHY THIS KEPT RECURRING, recorded because the pattern cost six hops. `.github`'s `audit` is a REQUIRED check that measures the LIVE state of every repo, while `.github`'s own diff is the inventory describing that state. So its mirror can be correct at the moment it is cut and wrong by the time the audit runs, for reasons that have nothing to do with its contents. Today: a fleet-wide hop moved 16 repos, nine bugbot-gate rows went stale, .github#337 fixed them on develop, the mirror predated that fix, a scoped recut carried it -- and then a tenth repo appeared. Each fix was right and each was overtaken. Nothing here changes that shape. It is worth its own ticket; this commit is the nine plus the tenth plus the dead anchor. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ount (#347) Bugbot HIGH on .github#341, and correct. The post-archive assertion I added in #339 treats a fully paginated `nodes` list as the whole board, and it never asks the server how many items the connection actually holds. Two ways that passes while the board is dirty: * `nodes|length` renders a MISSING or NULL path as 0. A query that resolved to nothing at all counts as "no terminal items left" and the job reports clean. * Items omitted from `nodes` while `totalCount` still counts them are invisible to every check in the step. Both the first read and the re-read are this same credential's, so they agree on the same subset -- or on zero -- and agree confidently. That second one is the exact defect #339 was written to close, one layer further in. The file even said so out loud: "Both reads are this credential's, so this cannot see what neither can." I wrote that sentence, and then let the assertion rest on those two reads anyway. Comparing a read against another read by the same blind credential is the test-a-list-against-itself trap, and #339's own commit message claimed to have avoided it. `totalCount` is the fix because it is the ONLY number in this job that does not come from this credential's view of `nodes`. Three guards now: 1. the connection must resolve to an array -- an unreadable read fails rather than counting as zero; 2. `totalCount` must be present and numeric, or completeness is unestablished; 3. paginated count must EQUAL `totalCount`, or items are being omitted and the run cannot claim a clean board. All three fail closed, which is the same rule the rest of the step follows. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Managed sync from tracebloc/.github/org-standards.md. Co-authored-by: tracebloc-release-train[bot] <309815517+tracebloc-release-train[bot]@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…348) * fix(2590): the sync opens its PRs as a human, so Bugbot reviews them `standards-sync.py --create-prs` authenticated `gh pr create` with the tracebloc-release-train App token, making the PR author `tracebloc-release-train[bot]` (type: Bot). Cursor Bugbot keys its review on the author's Cursor seat and a Bot has none, so Bugbot reviewed NONE of the 14 open sync PRs -- zero `Cursor Bugbot` check runs, measured 2026-08-26 -- and `bugbot / review` failed closed on every one. An explicit `bugbot run` is attributed to the author too, so it answers "Bugbot is not enabled for your user on this team" and cannot rescue the PR. `gh` now takes an explicit identity and PR CREATION ALONE uses it, from SYNC_PR_AUTHOR_TOKEN. The fleet reads and the branch push keep the App token: `owner:`-scoped, short-lived, not tied to one account, which is backend#2036. An empty PAT is a hard per-repo refusal, never a quiet downgrade -- the downgrade IS the bug, and it looks identical to success. Reviewer and assignee become SYNC_REVIEWER rather than GITHUB_ACTOR. The dispatcher is now the author, and GitHub refuses an approving review from a PR's own author, so keeping GITHUB_ACTOR would deadlock every sync PR. The 4 open PRs already requesting LukasWodka were reassigned by hand. Mutation-proved: 5 mutations, each caught by the assertion named for it. Writing them surfaced two harness defects worth keeping out of the sibling runners -- a mutation that applies but does not COMPILE reads exactly like good coverage, and a suite that CRASHES on a mutation is red without a verdict (rule 10). The new runner reports MALFORMED and CRASHED separately, and the empty-PAT case scripts the whole happy path so the assertion is what reddens. Closestracebloc/backend#2590 * fix(2590): the invariant is asked of the token, and the guard runs before the writes @saqlainsyed007's review on #348. Both blockers, both should-fixes, and the security point as far as code can take it. F4 (BLOCKER) -- reviewer != author rested on two literals that agreed with each other and with nothing else. `SYNC_REVIEWER = "saqlainsyed007"` here, and the selftest pinning the other side as `!= "LukasWodka"`. Re-provision SYNC_PR_AUTHOR_TOKEN to saqlainsyed007's PAT and author == reviewer, every `--add-reviewer` 422s, the backend#2590 deadlock returns -- and neither literal moved, so the suite stays GREEN through it. A credential's owner is a fact about the credential, so `author_login()` asks GitHub, as the PAT rather than as the ambient App identity, and `main()` refuses when it equals SYNC_REVIEWER. An unresolvable token is its own refusal rather than a guess. F3 (should-fix, and the one with teeth) -- the PAT-empty guard lived inside `_ensure_pr`, which runs AFTER `remediate()` has created the branch and pushed CLAUDE.md. A missing token therefore produced a fleet-wide HALF ROLLOUT: branches pushed to every drifted repo, PRs opened nowhere. Validation moved into `main()`, before the first repo is touched. The in-function check stays as belt-and-braces for a caller that skipped the gate, and says so. F1 + F2 -- `_ensure_pr` returned as soon as an open PR tracked the branch, so a PR whose `--add-reviewer` had failed once was never repaired on any later run; and that failure was a `::warning::`. Together: a reviewer-less PR, which branch protection makes unmergeable, shipping on a green run for ever. The role assignment now runs on BOTH paths, and the reviewer half is FATAL. The assignee half stays cosmetic on purpose -- it blocks no merge, and failing the run over it would be the opposite error. F5 -- the workflow's TOKEN header still said the App satisfied both needs, which the step comment 90 lines below already contradicted. Reconciled, including the paragraph arguing that "a second fleet-wide credential is the thing backend#2036 exists to stop": that argument was right about the risk and is overtaken by a harder constraint, and saying so is better than deleting it. SECURITY -- the in-code rationale claimed the PAT "needs `repo` scope; contents: write alone cannot open a PR". That conflates two claims. The true one is that the APP's contents:write cannot open a PR BUGBOT WILL REVIEW -- an authorship problem, not a scope one. A fine-grained PAT with `pull_requests: write` + `contents: read` opens PRs perfectly well and is dramatically tighter. Corrected in place; the re-provisioning itself is a decision, not a code change, so it is recorded rather than asserted as done. TWO DEFECTS I FOUND IN MY OWN TEST EDITS, worth recording because both are the shape this repo keeps paying for: * The new checks were appended AFTER the summary line and `sys.exit(1)`. They ran only when everything else passed, were never counted, and a failure among them could not affect the exit code -- a test block that inspects nothing. Moved above the summary; the count went 36 -> 42, which is how it was caught. * Stubs scripted `(0, "[]", "")` for "no open PR". The real call carries `--jq '.[0].number // empty'`, so no-PR is an EMPTY string and `"[]"` is truthy. Now that the existing-PR path also makes two edit calls, a `"[]"` stub consumes the same number of entries down the OPPOSITE branch -- passing while testing the case it is named against. Three stubs corrected. Verified: selftest 42 checks / 0 failed, `py_compile` clean, ruff clean, actionlint clean on the workflow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(2590): the identity gate is extracted so it can be exercised The mutation harness reported "SYNC_REVIEWER becomes the account that authors the PRs" as UNCAUGHT once the literal-vs-literal check was retired. A guard nothing can exercise is the shape this repo keeps removing, so the gate is a function now and each refusal is pinned individually. And the two mutations are re-aimed at what can actually break. Flipping the SYNC_REVIEWER literal is SURVIVABLE now -- the runtime gate refuses the run -- so the mutation that matters is dropping the comparison, not changing one of its operands. The reviewer-reverts marker follows the reviewer call into _assign_roles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(2590): the selftest could not go red below line 486 `failed = [...]` was computed a hundred lines above the last checks, so anything appended below it PRINTED its FAIL and did not count -- the summary read `0 failed` while two checks had failed, and the script exited 0. The mutation harness is what surfaced it: a gate mutation came back UNCAUGHT because the suite it was measured against was structurally incapable of failing there. A test file that cannot go red is the same class as a guard that cannot fire, one level up. Also re-aimed the unresolvable-token mutation. Skipping the `is None` branch crashes on `login.lower()` -- red without a verdict, which tells the harness nothing. Making `author_login` return a login it could not resolve is the same defect with a readable outcome. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(2590): a PAT that resolves but cannot create stops the fleet (Bugbot on #348) `check_author_identity` proves the token EXISTS, RESOLVES and is not the reviewer. Bugbot's point is that none of that proves it can open a PR -- the wrong fine-grained permissions, or a token never SSO-authorized for the org, passes every one of those checks and then fails at `pr create`, by which point `remediate` has pushed a branch and a commit to every drifted repo. That is the half-rollout the gate was added to prevent, arriving through the one failure mode the gate cannot see. AND NOTHING READ-ONLY CAN CLOSE IT. "Can this token open a PR in this org" has exactly one proof, which is opening one; a `GET /user` or a repo-permissions probe narrows the odds and still cannot answer it. So the guarantee is BOUNDED rather than claimed: `pr create` failing now raises `AuthorUnusable`, `main()` aborts the remaining targets, and at most ONE repo is left with a branch and no PR instead of nineteen. A DISTINCT TYPE, not a string the loop matches on prose. The loop has to tell "this repo failed" from "this credential fails everywhere", and those are different facts with different correct responses -- continue versus stop. The report says it happened. A run that stopped early and did not say so reads as a complete sweep of a smaller fleet, which is the same shape as every other silent-success defect this PR has been about. Verified: selftest 49 checks / 0 failed, py_compile and ruff clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(2590): an open bot-authored sync PR is refused, not reported ensured (Bugbot on #348) The existing-PR path repaired roles and returned None, so every sync PR already open -- all of them opened as tracebloc-release-train[bot] -- would have been reported ensured while staying exactly as unreviewable as before. A PR's author cannot be reassigned, so the only honest outcome is a per-repo error naming the remedy. is_bot is read from GitHub rather than pattern-matched off the login; an unparseable row fails closed rather than reading as human. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(2590): a refused PAT disarms the writes instead of skipping the audit (Bugbot on #348) Moving the credential gate into main() stopped the half-rollout, but it was written as die() -- which also skipped the read-only fleet audit. standards-sync.yml argues against exactly that twenty lines above the secret: aborting before the audit turns "PRs could not be opened" into "fleet state unknown", strictly less information. The gate exists to stop writes, and the audit is a read. A refusal now switches remediation off: every target is still classified and reported, each drifted row reads NOT REMEDIATED, the report carries a REMEDIATION DISABLED block naming the cause, no branch is pushed, and the run exits 2 rather than the --create-prs 0 (which would claim every drifted repo has a PR open). The workflow comment is updated so it no longer describes a placement the code has moved past. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ing (#349) backend#2157, ONE OF FOUR -- this does not close it. `set-pr-status.yml`s first mint passed `owner:` with no `permission-*` inputs, so it minted the App ENTIRE permission set -- contents:write, issues:write, administration:read and the rest -- across every repo the installation covers, on every PR event in the fleet. Largest exposure of the four purely because of how often it runs. SCOPES DERIVED FROM THE CALLS, not copied from a template: organization.projectV2 { fields } -> projects, read repository.pullRequest.projectItems -> pull-requests, read updateProjectV2ItemFieldValue -> projects, WRITE No `issues` scope: this job is PR-only. The SECOND mint in this same file already carried `permission-pull-requests: read` + `permission-issues: read` -- the closing-ref job reads the linked issue. Worth naming because the exemption is per FILE, so `set-pr-status.yml` read as wholly unscoped while half of it was already done. Exemption removed. The guard now reports 3 unscoped / 3 exempted / 0 findings, down from 4 and 4. A PASSING MINT IS NOT THE PROOF, and the comment says so: release-train add-to-kanban.yml records that "a mint can succeed with a scope the board write then needs and lacks". The evidence is a card that actually moves, which is why this lands alone rather than with the other three. Verified: make lint clean, selftests 28 passed, actionlint clean. Part of tracebloc/backend#2157 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…353) backend#2157, three of four -- this does not close it. NARROWED, NOT MADE READ-ONLY. `kanban-reconcile.yml` genuinely writes: three ProjectV2 mutations, and it CREATES labels (POST repos/{o}/{r}/labels) and applies them (POST .../issues/{n}/labels) for `keep-open` and `sibling-merge`. Claiming it read-only would break the Monday run. THE WIN IS `contents`. It minted contents:WRITE across every repo the installation covers -- the capability that can push code anywhere -- for a job whose only content access is READING .github/workflows/add-to-kanban.yml and a couple of branch refs. That drops to read; administration:read, actions:read and checks:read go entirely. DERIVED FROM THE CALLS: contents/... , branches/{main,master} -> contents: read label create + apply -> issues: WRITE the same labels on PRs -> pull-requests: WRITE addProjectV2ItemById / updateProjectV2ItemFieldValue / archiveProjectV2Item -> organization-projects: WRITE `pull-requests: write` is retained because the labels land on PRs as well as issues and the endpoint is shared. Guessing it short would fail on a path that only runs weekly, so it stays until measured rather than assumed away. THIS ONE IS PROVABLE WITHOUT A HOP, which is why it goes before standards-sync: .github default branch is `develop`, crons run from the default branch, and this workflow has a `dry-run` dispatch input. So after merge, `gh workflow run kanban-reconcile.yml -f dry-run=true` exercises the real mint and every read with no mutation -- the only one of the four where "a card that actually moves" is obtainable the same day. Verified: make lint clean, selftests 28 passed, actionlint clean, mint-scope 3 unscoped / 3 exempted / 0 findings. Part of tracebloc/backend#2157 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…354) Four PRs in one day failed `closing-ref` for the same reason -- .github#349, #350, #352, #353 -- and all four were fixed by retitling. That is a rule the canon is missing, not four mistakes: a finding that recurs across PRs becomes a rule rather than a fifth hand-fix. THE COLLISION. The canon says commit subjects are `type(scope): summary`, `referencing the ticket (backend#1234)`. Read quickly, `(scope)` looks like the ticket slot, so sessions write `sec(2157): ...`. `closing-ref` then reads a bare number in the title as a PROMISE to close that ticket and fails unless the body carries a closing keyword -- which for partial work would be WRONG to add. Every one of the four was partial work correctly saying `Part of tracebloc/backend#N`, and every one went red for saying so. The canon's own example is already the qualified form (`backend#1234`), not a bare number; the gap is that it never says the number does not belong in the subject, and that a bare one has a specific machine meaning. Both are now stated. WHY NOT LOOSEN THE GATE INSTEAD. The gate is right and its failure message is excellent -- a title reference is inert, and it cannot tell which repo owns a bare number, so it would accept a link to any repo at that number. The defect is the convention pointing one way while the check reads the other. Verified: `classify` reports IN_SYNC (canon and CLAUDE.md agree), standards-sync selftest 31 checks / 0 failed. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…hing (#352) backend#2157, two of four -- this does not close it. `fr-gate.yml` performs ZERO mutations. No GraphQL mutation, no `gh pr/issue` write, no REST POST/PATCH, no git push. It reads and reports its own conclusion. It was nevertheless minting the App entire permission set -- contents:write, issues:write, organization_projects:write, administration:read -- across every repo the installation covers. A pure reader holding org-wide write is the sharpest mismatch of the four. SCOPES TAKEN FROM THE THREE CALLS THE FILE OWN COMMENT ALREADY NAMES, which is why this could be checked rather than guessed: commits/{sha}/pulls -> contents: read + pull-requests: read compare/{base}...{head} -> contents: read ProjectV2 graphql -> organization-projects: read `actions/checkout` is not on this token -- it takes the job default GITHUB_TOKEN -- so the checkout needs nothing from here either. IF THE SCOPE IS SHORT the gate fails closed and blocks promotions: loud, immediate, and recoverable with the `skip-fr-gate` label. That is the failure direction this gate already chose for itself -- "blocking wrongly is far better than passing wrongly". NO SIGNAL UNTIL THE HOP, and the body says so: callers pin `fr-gate.yml@main`, so every promotion keeps using main unscoped copy until develop -> staging -> main lands. A green run here proves nothing. Verified: make lint clean, selftests 28 passed, actionlint clean, mint-scope 3 unscoped / 3 exempted / 0 findings. Part of tracebloc/backend#2157 Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…erent reason (#350) * docs(2598): the bug-to-ready exception is two repos, and for a different reason @saqlainsyed007's blocking finding on the sync PRs (client#848). He is right, and chasing the mechanism moved the reason as well as the count. MEASURED. All three named repos now have a caller -- `.github`'s landed under backend#2396, and `release-train`/`rfcs` both carry a real `customer-priority-bump.yml` calling the reusable. So caller presence is no longer the differentiator, and `.github` is fully wired: it has the caller AND all four `work-type:*` labels. The list was wrong by one, exactly as the review said. What remains is narrower and invisible: `release-train` and `rfcs` have no `work-type:*` labels AT ALL. Neither has its own `.github/ISSUE_TEMPLATE`, so both inherit the org Bug template, which asks for `work-type:bug` -- and GitHub SILENTLY DROPS a template label the target repo lacks. Nothing is added, `bug-to-ready` never sees its trigger, and the card sits in `Backlog`. A caller without the label is wired in exactly the way that does nothing. THE ANCHOR THE OLD TEXT CITED IS GONE. `customer_priority_bump_caller_missing` no longer appears anywhere in `repo-inventory.yml` -- it was removed when the exemptions were retired -- so the reusable's header sent a reader to a key that returns nothing. That is worse than a stale count: it is a doc teaching a lookup that cannot answer. Corrected in place, with the new reason stated and backend#2598 named as where it gets closed. THREE COPIES, because the sync only reaches one of them. `org-standards.md` and its 19 synced `CLAUDE.md` blocks travel together, but a workflow COMMENT does not -- so the reusable's header and the caller's header were both hand-corrected here. The review's second point ("check the sync actually reaches every copy") is the reason I went looking; it does not, and two of the three copies were only reachable by hand. ON NOT REPLACING A DRIFTED LIST WITH A FRESH ONE, which is the review's real argument: the sentence now states the CONDITION rather than a roster -- the label has to exist for the trigger to match -- so a reader can check it in one API call instead of trusting prose. It still names two repos, and that is deliberate rather than settled: backend#2598 creates the labels, at which point the exception has no members and the sentence is DELETED rather than kept accurate by hand. Emptying the list is the fix; re-writing it is not. Verified: `classify` reports IN_SYNC (canon and CLAUDE.md agree), standards-sync selftest 31 checks / 0 failed, both workflows parse, actionlint clean, reason-citations 0 findings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(kanban): three repos, derived from the inventory rather than from the prose @aptracebloc's review on #350. He is right on every point, and the one that matters is the one I would have kept getting wrong. THE COUNT IS THREE. `design-system-v2` is in the identical broken state to `release-train` and `rfcs`: no `work-type:*` labels at all, no own `.github/ISSUE_TEMPLATE`, and `customer-priority-bump.yml: required` in repo-inventory.yml (added 2026-08-25 via .github#332, after the prose this PR was written against). WHY I MISSED IT, WHICH IS THE ACTUAL DEFECT. I checked the three repos the OLD SENTENCE NAMED and found one of them wired, so I wrote "two". I never asked the inventory which repos declare the caller. That is CLAUDE.md rule 6 exactly -- derive the input domain from the producer's declared surface and test all of it, because a vocabulary gap is invisible to any amount of checking inside the wrong domain. In a PR whose whole argument is "restating the authority is the defect", I restated the authority. Derived properly this time: `repo-inventory.yml` declares `customer-priority-bump.yml: required` for TWENTY repos and exempts none; all 20 were measured, and exactly three have an empty label set. The arithmetic follows -- 20 declared minus 3 unlabelled is 17 covered. The previous "17" was right only by coincidence, being 19 minus two. Fixed in all four places the omission appeared, as asked: `org-standards.md`, `CLAUDE.md`, and both workflow headers. The reusable's header also now records that the list is derived and why an earlier version named two, so the next reader sees the trap rather than just the corrected number. MINOR, ALSO HIS: "grepping for it returns nothing" was too broad. The `customer_priority_bump_caller_missing` key is gone from `repo-inventory.yml` (true, and the point), but the STRING still appears in `scripts/reason-citations.py` and in the comment itself -- I had grepped it up earlier in this same session and still wrote the wider claim. Scoped to the inventory. backend#2598 widened to three repos in the same pass, since it was scoped to two and would have left `design-system-v2` broken after landing. Verified: `classify` reports IN_SYNC, standards-sync selftest 31 checks / 0 failed, both workflows parse, actionlint clean, reason-citations 0 findings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
backend#2157, LAST OF FOUR. With #349, #352 and #353 this empties EXEMPT. Scoping this to the UNION would have kept fleet-wide contents:WRITE on every scheduled report -- which is the mismatch, not the fix, since `report` mode only reads. So the contents scope is conditional on the mode: permission-contents: ${{ (dispatch && create-prs == true) && write || read }} On a schedule run `inputs` is empty, the condition is false, and the same job gets READ. DERIVED FROM THE CALLS THIS TOKEN MAKES, checked one at a time: git/ref/heads/develop, repos/{r}, contents/CLAUDE.md?ref= -> contents: read PUT contents/CLAUDE.md, POST git/refs (create-prs only) -> contents: WRITE gh pr list -> pull-requests: read gh issue comment 1602 (the drift report) -> issues: WRITE NOT pull-requests:write: `pr create` and both `pr edit` calls run on SYNC_PR_AUTHOR_TOKEN, not on this token (backend#2590). The App only LISTS. TWO CORRECTIONS TO WHAT I SAID EARLIER ON THE TICKET, both from reading the file rather than the summary: * The minting job is gated on `schedule || workflow_dispatch`, so it never minted on push/pull_request/issues. The exposure was real but narrower than "nearly every run". * `== true` rather than a bare truthiness test, matching the CREATE_PRS line below it. Equivalent for a `type: boolean` input -- but if it ever became a string, `false` is TRUTHY and this would mint write on every scheduled run while still looking conditional. Verified: make lint clean, selftests 29 passed, actionlint clean, mint-scope 3 unscoped / 3 exempted / 0 findings on this branch (the other three are on their own branches; together they reach 0 and 0). Part of tracebloc/backend#2157 -- the LAST of four. Merge order is not guaranteed, so this does not carry a closing keyword: if it landed first it would close the ticket with three mints still unscoped. Close #2157 by hand once #349, #352, #353 and this one are all in. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…356) * fix(2284): the gate tolerates a review that never came, and says so Two absences were one verdict, and they mean opposite things. `PENDING` covered both "Bugbot is still running" and "Bugbot never showed up", and the timeout failed both identically. A check that STARTED and never finished is a review that BROKE -- worth blocking on. A check that never appeared is Bugbot dropping the PR, which this repo cannot fix, retry, or wait out. Measured 2026-08-26 on human-authored PRs, well past p50 164s / max 635s: six of nine never got a check at all -- .github#349 (57 min), #350 (55), #352 (40), #353 (37), #354 (32), e2e-test-agent#273 (2h+) -- while #351, opened BETWEEN two of them, was reviewed in three minutes. Not latency, not the seat limit, not the author. backend#2114 closed COMPLETED saying "no discriminator survives the data"; the drop is still live. `bugbot run` cannot recover it: Cursor refuses on a seat limit and the App will not be given one (decision, 2026-08-26). So requiring this context while failing UNCLAIMED would block roughly two thirds of PRs for the full wait and then fail them with no remedy -- the gate would look broken while behaving exactly as written. WHAT THIS IS NOT: it is not a pass. UNCLAIMED exits 0 so the context can be required, and every other word says the head is UNREVIEWED -- its own banner, its own summary line, and an explicit "read the diff yourself". The honest report is "nothing looked at this", not "this is clean". PENDING still blocks, and that asymmetry is the whole change. Also corrected: the vocabulary comment said "Only PENDING is waitable", which this makes false. `main` now READS `WAITABLE` rather than restating it. 79 selftest assertions (4 new through `main`, since the exit code is the behaviour and `evaluate` alone cannot show it). Registry: 1 stale marker fixed, 5 mutations added -- the collapse in both directions, the tolerance leaking onto PENDING, the tolerance removed, and the banner reading as a pass. Part of tracebloc/backend#2284 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(2284): pin the UNREVIEWED banner, which no assertion covered The mutation `the UNREVIEWED banner reads as a pass` came back UNCAUGHT. Every other assertion in this suite checks a verdict or an exit code, and the banner is neither -- it is the one line a human skimming the summary actually reads, and the only thing standing between "exits 0" and "looks clean". Three cases: the UNCLAIMED headline says UNREVIEWED, does not contain the word pass, and a genuine pass still does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * style: hoist the selftest imports ruff flagged E402/E401 from the banner test added in the previous commit. `make lint` is where I should have run it before pushing, not after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(2284): the threshold is applied before the head is classified Bugbot on #356, High: `evaluate` returned UNCLAIMED as soon as the head had no Bugbot check, without applying the open-finding threshold to the threads it had ALREADY loaded one block earlier. So the tolerance this PR adds for a dropped review would launder a finding that had already come -- review head A, get a High, push head B, Bugbot never claims B, and the gate reports UNREVIEWED-but-not-blocked over an open High at exit 0. The same hole was in PENDING, and fixing only the branch Bugbot named would have left its twin. So `blocking` is now computed before `bugbot_check` is consulted at all, and an absence of either kind with a blocking finding is a FAIL that names the finding and does NOT wait -- the answer would not change. Both absence verdicts now also state that the threshold was checked, so the report says which question was answered. The tolerance itself is intact and asserted: a Low on an unreviewed head is still UNCLAIMED, exit 0, and so is a resolved High -- otherwise the remedy the FAIL message names would not clear it. Not a shipped bug: `required_conversation_resolution` is true on every train branch, so the merge was blocked anyway. It was a gate naming the wrong reason, which is the failure mode this file exists to prevent. One renderer (`_finding_lines`) feeds both paths, because two would drift and one of them would quietly stop saying OPEN (rule 9). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
… (backend#2526) (#357) * fix(kanban-archive): report every completeness finding, not the first (backend#2526) The "Assert the board is clean" step had four checks, each exiting on the spot. The most diagnostic one was ordered last, so the first to fire hid it. Measured on the 2026-08-27 06:28 run: the step exited on a single null Status, and `totalCount` -- the only number in the job that does not come from this credential's own view of `nodes` -- was never compared. The run therefore could not say whether the credential sees a SMALL board or is being served an incomplete view of a LARGE one, which is the exact question backend#2526 needs answered. That run read 92 items; a human PAT reads 1362. One short-circuit cost a whole run's worth of diagnosis, and the next run would have cost another. All four now record into `fail` and the step exits once at the end, with the server's own count checked FIRST and a step-summary table of the four numbers. A clean board additionally prints the paginated-vs-totalCount agreement, so a green run says what it verified instead of staying silent. Mutation-proved on the extracted decision block: all four bad -> exit 1, 4 findings (was 1) all clean -> exit 0, 0 findings, prints agreement only totalCount wrong -> exit 1, 1 finding (previously masked) * fix(kanban-archive): repoint the ticket citations at backend#2623 The workflow cited backend#2526 (7x) and backend#2528 (2x) as the source of its own guards, including inside the ::error:: text an operator reads when one fires. Both resolve to unrelated merged PRs: backend#2526 -> fix(fr-gate): re-run the gate when a PR changes its base backend#2528 -> release-train: develop -> staging (a promotion PR) So a guard that fired sent whoever read it to an fr-gate change. No ticket for the archiver's blindness existed at all; backend#2623 is now that ticket and carries the 92-of-1361 measurement plus the two refuted causes.
`design-system-v2` joined the release train on 2026-08-26T12:12:21Z
(release-train/repos.yml commit 8330f408, onboarded under backend#2547), and
`repo-inventory.yml` still recorded `release_train: false`. caller-drift.py
cross-checks that flag against repos.yml, so the disagreement was a
repo-conformance finding -> a red `audit` -> a red REQUIRED `gate` context ->
`.github` could not promote to staging. Measured before/after on the live
fleet: 1 finding -> 0.
Not an ungated hop. `design-system-v2` does have
`.github/workflows/fr-gate-caller.yml` on develop, so the promotion gate is
present; the guard cannot tell that from a repo that joined the train without
one, and is right not to guess.
The flag is not the only cell the train join invalidated, so the reasons and
measurements around it move with it rather than being left false:
* protection.develop / staging / prod. The block was written when `develop` was
this repo's only branch. Onboarding created `staging` and `main`; measured
2026-08-27 all three return "Branch not protected" and the repo has ZERO
rulesets. The two aliases used here (`develop_unprotected_non_train`,
`no_staging_branch_not_on_train`) assert a premise that is now false, and the
prod reason ("no prod branch exists ... structural, not a gap") is simply
false. Replaced with inline, measured reasons that record the gap as
UNREMEDIATED under the open backend#2547.
* The rows stay `exempt` rather than becoming `required`: `required` over an
unarmed branch lands a red gate, and nothing in this file may land red
(arm while green). Branch first, cell after. Staleness still bites - the
moment any of the three gains protection of either kind, caller-drift flags
the exemption.
* rulesets.tag_trust_root said "no tag-triggered workflow and no v* tags cut".
Both halves are false now: `publish.yml` triggers on `push: tags: ['v*']` and
`v0.1.0-rc.1` is cut, so the trust root is MISSING, not inapplicable.
* The callers comments asserted "this repo has ONE branch, `develop`" and that
the fr-gate caller is "permanently dormant". `staging` and `main` exist, so
the caller is live and the fr-pass-comment misfire it described is gone.
Verified: caller-drift.py live findings_total=0 / "No drift" (was exit 1 with
the design-system-v2 finding against origin/develop's inventory),
caller-drift-selftest 197/197, reason-citations 0 findings, house-rules clean.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>release-train: develop -> staging
…e truthful (backend#2556) (#360) * fix(closing-ref): re-run when the title changes, and let a child PR be truthful (backend#2556, backend#2616) Two defects in one gate, fixed together because either alone leaves the other's remedy unusable. BACKEND#2556 -- THE GATE READS THE TITLE AND NEVER RE-RUNS WHEN IT CHANGES. `closing-ref` decides its verdict from the PR title and body; the callers triggered on `[opened, reopened, ready_for_review, converted_to_draft]`, and `edited` is the only event GitHub fires when either field changes. So the two inputs the gate reads were the two inputs that could change without re-running it. That is a bypass, not a gap: open a PR titled `chore: tidy up`, the gate records NOTHING_NAMED and goes green, then retitle it to `fix(1234): ...` with nothing linked -- no event fires, the green stands. It also made the remediation unusable in the other direction: 20 sync PRs had to be cleared with 20 manual `gh run rerun` calls (backend#2555). Same one-word fix, same reason, as `fr-gate-caller.yml` (backend#1945). A CONSEQUENCE THAT HAD TO BE GUARDED, not discovered later. Every trigger this workflow had was reachable only on an OPEN PR, so `set-status` writing Status unconditionally was always correct. `edited` is not: GitHub fires it when a MERGED PR's title or body is edited, and this is the one board writer in the fleet that does not compare pipeline rank. Without the new guard, fixing a typo in a shipped PR's description would drag its card from `Prod` back to `Code review`. Both jobs are now `if: ...state == 'open'`. BACKEND#2616 -- ONE SATISFYING FORM, AND A CHILD PR CANNOT TRUTHFULLY USE IT. The checker read `closingIssuesReferences` and nothing else, and that field is populated only by a closing keyword. So a PR whose title named a ticket had exactly one satisfying form: promise to close it. For this org's most common shape -- a parent ticket with several child PRs titled `type(NNNN): ...`, none of which finishes it -- the only remedies were a FALSE `Closes`, or deleting the number from the title, which passes the check by removing the traceability the check exists to enforce. Four PRs in one day took the second (.github#349, #350, #352, #353) and #354 wrote that workaround into the canon. A declared NON-CLOSING body reference now satisfies the title, reported as its own state (`MENTIONED`) so a green run still says which promise was made. The closing link stays the stronger form, and `WRONG_REPO` is decided BEFORE any body reference is consulted -- a truthful `Part of tracebloc/backend#304` must not mask a `Closes#304` that closes `.github#304` on merge. DERIVED, NOT RESTATED (rule 1). The non-closing vocabulary is PARSED out of `org-standards.md`, which already declares the org's partial-work form. Add `Refs <owner>/<repo>#N` to the canon and the gate accepts it with no code change. The derivation fails closed in both directions: an unreadable canon and a canon declaring no non-closing form are separate cannot-tells (exit 2), never a silent reversion to closing-only -- reverting is the defect itself. GitHub's closing keywords are the one list here that cannot be derived from this org, so they are named, cited, and used only to subtract. The canon and CLAUDE.md carried two statements this change makes false -- that the check fails unless the body carries `Closes`, and to keep the number out of the subject. Both are rewritten; classify() reports IN_SYNC. EVIDENCE closing-ref-gate-selftest 156 assertions, all passed (was 109) closing-ref-gate-mutations 53 mutations, 0 stale, 0 uncaught (was 36) ruff / actionlint / shellcheck / house-rules clean standards-sync selftest 60 checks, 0 failed The mutation harness now rewrites the two WORKFLOW files as well as the checker: `edited` and the open-state guards are guarantees declared in YAML, and rule 5 does not exempt a guard for living in a different language. Part of tracebloc/backend#2556 Part of tracebloc/backend#2616 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(closing-ref): an escaped Unreadable reddens a case instead of killing the suite The multi-word-keyword mutation was scored UNCAUGHT for the right reason: it made `evaluate` raise, which killed the suite mid-run, and the harness refuses to count a broken harness as coverage. `evaluate` grew a path that can raise for a NEW reason (the derivation from org-standards.md coming back empty), so the positive cases now go through a wrapper, exactly as `value` already does for every other call. Part of tracebloc/backend#2616 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(closing-ref): the derived-vocabulary cases report a refusal instead of crashing `value` returns the exception TEXT on a refusal, and a string is iterable -- so a case that walked the derived list iterated characters and raised IndexError on the space in "RAISED Unreadable: ...". Normalised once. Found by the multi-word-keyword mutation, which is the job: it was scored UNCAUGHT for breaking the harness rather than being caught by a case. Part of tracebloc/backend#2616 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…nd#2722) (#361) * fix(kanban): a completed issue is terminal, not a deploy state (backend#2722) Closed issues were landing in deploy columns because two mechanisms deliberately put them there, not because either was failing. 1. kanban-closure-router.yml mirrored the closing PR's Status onto the issue, so a PR-closed issue inherited On dev / FR on staging / Prod. Its own declared contract said so. 2. advance-deploy-env.yml then marched those cards onward (backend#1600), because issues parked at On dev never advanced when the code shipped and drifted permanently -- all 20 drifted cards on 2026-08-06 were closed issues, 0 PRs. #1600 was right that a closed issue must not be stranded and wrong about where to put it. `Done` is terminal, so nothing can drift, and kanban-archive sweeps it off the board daily -- which answers #1600 more completely than advancing did. So: the router routes a completed issue to Done regardless of how it was closed, and the closing-issue block in advance-deploy-env is removed. Both halves, because fixing only one looks fixed while the other drags the cards straight back in. The closer lookup goes too. It existed only to tell a hand-close from a PR-close so they could route differently; with one destination there is nothing to tell apart. It was also the source of two fail-wrong defects (.github#126, #127), and a call never made cannot fail. CLOSER_TYPE / CLOSING_PR_BASE were read nowhere else. `permission-issues` drops from advance-deploy-env with the only reads that needed it. Checked rather than assumed: fr-gate ranks Done 11 against On dev 6, so terminal cards satisfy both gates instead of blocking promotions. Cost of the status quo, measured: 117 closed issues cleared out of deploy columns by hand in one session (18 + 55 from FR on staging, 44 from On dev). Two of those passes were functional-review batches, so the next prod payload would have read 73 items larger than the work it contained. Guarded now, because it was not: flipping the router's destination left all 29 selftest suites green. kanban-deploy-state-selftest asserts both halves, reading the completed arm out of the workflow rather than restating it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(kanban): the weekly backstop must terminalise too (backend#2722) Bugbot, High. The router now sends every completed issue to Done, but kanban-reconcile.yml still derived a deploy stage from the closer`s base branch and wrote On dev / FR on staging / Prod for exactly those cards. Fixing one writer and not the other is worse than fixing neither: the weekly sweep would have put every card back, the slower job silently undoing the faster one -- which is the failure .github#295`s own comment was written to prevent, arriving from the other direction. A deploy column is a property of a PR. A finished ISSUE belongs in Done whether or not its fix shipped, and issue cards parked in deploy columns are what stops kanban-archive clearing them. The closer lookup stays, because having one still distinguishes the two cases in the LOG -- but both arms now reach the same column, which is the rule. Second finding, same shape one layer out: the selftest read only the router and advance-deploy-env, so a green run confirmed the two EDITED files rather than the invariant. It now extracts the reconcile`s closer arm too and asserts it writes only DONE_OPT, names no deploy column, and reaches no branch->stage mapper. Mutation-proved: restoring the On dev write reddens it. Also removes two now-dead option ids shellcheck flagged, and the comment that justified them, which my own change had made false. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(kanban): realign the mapper selftest with a terminal completed issue (backend#2722) The selftest already encoded #2722 for the router -- one call site, not two -- but still required kanban-reconcile.yml to keep one. Removing reconcile`s router-miss derivation made that false, so three assertions had to follow: * reconcile`s mapper call sites: 1 -> 0. Its only one was the closed-completed issue arm. The merged-PR lookback still places PR cards in deploy columns but always mapped its branches INLINE, so the count did not move because of it. * the DEST-arm invariant is inverted rather than deleted. It asserted every Status with an option id needed an arm, or the backstop silently skipped the repos an override exists for (.github#304) -- true while reconcile DERIVED a column, meaningless now there is no $DEST. It now asserts there are no arms, plus that the extractor can still find one in a fixture, so "no arms" cannot be confused with "the reader broke". * the stderr-preservation loop drops reconcile, which has no stderr to preserve. Its absence is asserted above rather than left implicit. All 15 selftests pass and actionlint reports 0 across every workflow. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#362) set-pr-status.yml's header said the non-draft card moves to "In review". That option was renamed "In review" -> "Code review" in #20 (7dbaabf) and no longer exists on the board -- verified against the live Status field, whose options are Backlog, North Stars, Ready, In progress, Code review, On dev, Staging (agent review), FR on staging, Ready for prod, Prod, Done, Cancelled. The code has been correct throughout: the job writes `status_name=Code review` (:108), and the same header already says "the SAME event that puts a card in `Code review`" thirty lines further down. So the file disagreed with itself, and the wrong half is the part a reader meets first. Comment-only. No workflow logic, no behaviour change. Swept the repo for the same phrasing; this was the only board reference. Two other matches are deliberately left alone: repo-inventory.yml:635 uses "while this PR was in review" to mean review as an activity, which is correct English and not a column name, and standards-sync-selftest.py:428 is a Python `in` operator, not the phrase. make check: 95 passed 0 failed, 29 passed 0 failed, EXIT=0.
LukasWodka
commented
Aug 27, 2026
bugbot run |
Uh oh!
There was an error while loading. Please reload this page.
LukasWodka
commented
Aug 27, 2026
REFUSED. This promotion was blocked by 1 HIGH severity finding(s): Create failure aborts remaining classification, per the severity policy in release-train's README (High stops the line; an unreadable severity blocks too, because unknown is not low; Medium/Low are recorded and ship, at both hops). Blocking threads stay UNRESOLVED by design, so this promotion remains blocked until they are fixed on What the train did with each:
|
LukasWodka
commented
Aug 27, 2026
REFUSED. This promotion was blocked by 1 HIGH severity finding(s): Create failure aborts remaining classification, per the severity policy in release-train's README (High stops the line; an unreadable severity blocks too, because unknown is not low; Medium/Low are recorded and ship, at both hops). Blocking threads stay UNRESOLVED by design, so this promotion remains blocked until they are fixed on What the train did with each:
|
…d#2637) (#359) * ci(conflict-gate): a conflicted PR reads red, not empty-green (backend#2637) A PR with a merge conflict against its base dispatches NONE of its `pull_request` workflows: GitHub cannot compute the merge ref those runs are keyed on. Every drift and source-of-truth guard is therefore silently inactive on exactly the PRs most likely to need it, and the rollup shows nothing red because nothing ran. Measured on model-zoo#206 while it was DIRTY against develop: 0 workflow runs on its head sha, one rollup entry (Cursor Bugbot, SUCCESS), and all seven of model-zoo/develop's required contexts absent. PRs #204 and #205 either side of it got their full matrix. Adds an org-wide sweep that writes a commit status onto each open PR's head sha - failure when conflicted, success when clear, pending when GitHub will not say - which is the only signal that can reach a PR with no merge ref. Not required anywhere yet, deliberately: a 30-minute sweep would leave a freshly-opened PR at "Expected - waiting" until the next run. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(conflict-gate): a case that CRASHES is not a case that CAUGHT (backend#2637) Two mutations -- drafts skipped in plan(), and an unreadable PR list returning no error -- emptied a list the suite then indexed, so it died with an IndexError instead of reporting a FAIL. The harness correctly scored both as 'broke the harness' rather than caught: a traceback proves nothing about coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(conflict-gate): record the green-but-conflicted shape the watcher cannot see (backend#2637) backend#2257, measured live and CONFLICTING, carries 8 workflow runs and all eleven of backend/develop's required contexts present and SUCCESS -- computed against a merge base that no longer exists. bricked-prs.py reasons from an ABSENT required context, so with nothing missing it cannot see this PR at all. That is the case that justifies asking about mergeability directly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(conflict-gate): a status is written only when it would change something (backend#2637) GitHub caps statuses at 1000 per sha AND context. A 30-minute sweep is 48 writes a day onto an unchanged head, so a PR left open three weeks would exhaust the cap and every later write would 422 -- the gate going silent on exactly the stalest PRs, which are the ones most likely to have conflicted. The current state is read out of statusCheckRollup, which the PR list already returns, so this costs no extra API call. A truncated rollup omits our context, which reads as 'no status yet' and produces a write -- the safe direction. The case fold is load-bearing: GraphQL reports SUCCESS, the Statuses API takes success, and comparing unfolded would make every status look changed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(conflict-gate): assert the workflow is ARMED, not merely present (backend#2637) Everything the suite asserted proved the SCRIPT was right; none of it proved anything RUNS it. Adds nine assertions parsing conflict-gate.yml as YAML (a "run:" line under a comment or an "if: false" greps identically to a live one) and six workflow mutations, so the harness now rewrites both files. The regression most likely to be made in good faith has its own mutation: someone asks why this does not run on PRs, adds a "pull_request:" trigger, and the gate becomes inert on exactly the conflicted PRs it exists for while looking more thorough. Rule 5 does not exempt a guarantee for being written in YAML. The baseline guard now covers both targets: a mutation left in the workflow by a killed run would become the next run's premise just as silently as one left in the script. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(conflict-gate): read the current status via REST, not the rollup (Bugbot, #359) Bugbot raised this as high on the previous head, and it was right. "gh pr list --json statusCheckRollup" resolves "commit.status" underneath, and GraphQL REFUSES that subfield on a PRIVATE repo unless the token also holds "actions: read" -- measured under backend#2157 and documented in bricked-prs.yml, which declares "permission-actions: read" for exactly this reason. This gate's mint deliberately holds only pull-requests:read and statuses:write, so the dedup added in c3b8279 would have made open_prs raise on every private repo in the org: each becomes COULD NOT EVALUATE and the sweep exits 2 having judged almost nothing. The trap was documented in a file I read while writing this one, which is the argument for measuring a narrow token rather than reasoning about it. Reads "GET /repos/{o}/{r}/commits/{sha}/status" instead. It reads commit statuses and nothing else, so the statuses permission already held covers it; it cannot be refused for a scope this job has no other use for; and it has no pagination cap to straddle. One GET per open PR, ~50 per sweep. The mint stays at two permissions. Two new mutations pin it: switching back to the rollup, and treating an unreadable current state as agreement (which would silently stop reporting whenever the status read flakes). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
release-train: develop -> staging
LukasWodka
commented
Aug 27, 2026
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 50c8f65. Configure here.
Automated promotion by the release train (RFC-0008 D14). Head is the train-managed
release-train/to-mainbranch (a mirror ofstaging), so it never collides with a human PR. Merged only when the fr-gate is green.Note
High Risk
Org-wide kanban routing and deploy-env behavior change how every closed issue and promoted PR is represented on the board; combined with fleet bugbot callers and narrowed App tokens, mis-scoping or routing bugs would surface as silent card drift or red CI across many repos.
Overview
This promotion bundles a large tracebloc/.github rollout: kanban behavior, new org guards, tighter GitHub App token scopes, and inventory/docs alignment.
Kanban (backend#2722) — Completed issues now route to
Doneinkanban-closure-routerinstead of mirroring a closing PR’s deploy column. The matching cross-repo “advance closing issues” loop is removed fromadvance-deploy-env, andkanban-reconcilestops backfilling issues into deploy states. App minting dropspermission-issueswhere issue reads are gone.Gates & callers — Adds
bugbot-gate-caller.yml(advice-only rollout; inventory flipsbugbot-gate.ymlto required fleet-wide) andconflict-gate.yml(30‑minute cron writes merge-conflict status on PR heads whenpull_requestworkflows never run).fr-gate-callerandset-pr-status-callergaineditedso base-branch and title/body changes re-run checks;set-pr-statusonly updates cards on open PRs and uses Code review for non-drafts.bugbot-gate.pysplits unclaimed vs pending heads, exits 0 on unclaimed after wait but still fails if open findings exist from an earlier review.closing-ref-gate.pyaccepts canon-derived body refs likePart of owner/repo#N, not onlyCloses.Reliability & least privilege —
kanban-archivelogs credential visibility, adds a post-archive “board is clean” assertion (fail-closed on null Status /totalCountmismatch).fr-gate,set-pr-status,kanban-reconcile, andstandards-syncnarrow App installation permissions; standards sync usesSYNC_PR_AUTHOR_TOKENfor PR creation so Bugbot can review sync PRs.Inventory & standards —
repo-inventory.ymlreflects design-system-v2 on the release train (protection still exempt until armed), removesbugbot_gate_rollout_pending, and refreshes several exemption narratives.org-standards.md/CLAUDE.mdpoint branch models at the inventory, document the three repos missingwork-type:bug, and expand PR title/link and cleanup guidance. Makefile wires selftests/mutations for conflict-gate, mutation-baseline, and standards-sync.Reviewed by Cursor Bugbot for commit 50c8f65. Bugbot is set up for automated code reviews on this repo. Configure here.