Skip to content

release-train: develop -> staging - #341

Merged
tracebloc-release-train[bot] merged 21 commits into
stagingfrom
release-train/to-staging
Aug 27, 2026
Merged

release-train: develop -> staging#341
tracebloc-release-train[bot] merged 21 commits into
stagingfrom
release-train/to-staging

Conversation

@LukasWodka

@LukasWodkaLukasWodka commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Automated promotion by the release train (RFC-0008 D14). Head is the train-managed release-train/to-staging branch (a mirror of develop), so it never collides with a human PR. Merged only when the fr-gate is green.


Note

High Risk
Changes gate logic, fleet inventory required rows, GitHub App permissions, and scheduled kanban archive failure modes—any mistake can block promotions org-wide or leave false greens on board hygiene.

Overview
This promotion bundles fleet-wide CI and guard changes from tracebloc/.github: Bugbot advice wiring, tighter GitHub App mint scopes, harder kanban archive verification, and org-standard prose updates.

Bugbot review gate (backend#2284) adds a thin bugbot-gate-caller in .github and flips bugbot-gate.yml from rollout exempt to required across the inventory. bugbot-gate.py now treats never-claimed heads (UNCLAIMED, exit 0, “UNREVIEWED”) separately from claimed-but-stuck reviews (PENDING, exit 1), applies the severity threshold before classifying the head so an open High cannot be laundered by a dropped review, and expands selftests/mutations accordingly. Branch protection still does not require bugbot / review in this step (Dependabot gap documented in the caller).

GitHub App tokens are narrowed on fr-gate, set-pr-status, kanban-reconcile, and standards-sync (permission-* on mint); mint-scope.py’s full-grant exemption list is empty so new over-broad mints fail the guard. Org-standards sync opens PRs with SYNC_PR_AUTHOR_TOKEN (human Cursor seat) while the App still reads/pushes; standards-sync.py validates the PAT before any branch push and aborts fleet remediation on failed pr create.

Kanban archive records read completeness (totalCount, null Status), warns on unreadable Status, and adds a separate “Assert the board is clean” post-archive re-read that fails on leftover terminal cards, pagination vs totalCount mismatch, or null Status—not only on archive API errors.

Smaller fixes: fr-gate-caller adds edited so retargeted PRs re-run the gate; customer-priority-bump docs note three repos still lack work-type:bug labels; design-system-v2 inventory reflects train onboarding and unprotected branches; mutation baseline guard refuses mutation runs when targets differ from HEAD.

Reviewed by Cursor Bugbot for commit b5abac6. Bugbot is set up for automated code reviews on this repo. Configure here.

LukasWodkaand others added 2 commits August 25, 2026 20:33
…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>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@tracebloc-release-traintracebloc-release-trainBot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 26, 2026

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 e709b44. Configure here.

@tracebloc-release-traintracebloc-release-trainBot removed the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 26, 2026
`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>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 fd31ef4. Configure here.

…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>
@tracebloc-release-traintracebloc-release-trainBot added gate-nudge Toggled by the release train to (re-)fire the fr-gate and removed gate-nudge Toggled by the release train to (re-)fire the fr-gate labels Aug 26, 2026
LukasWodkaand others added 2 commits August 26, 2026 11:34
…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>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@tracebloc-release-traintracebloc-release-trainBot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 26, 2026

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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 108a9f4. Configure here.

…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>
@tracebloc-release-traintracebloc-release-trainBot removed the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 26, 2026
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

…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>
@tracebloc-release-traintracebloc-release-trainBot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 26, 2026
Comment thread.github/workflows/kanban-archive.yml
…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>
@tracebloc-release-traintracebloc-release-trainBot removed the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 26, 2026
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

REFUSED. This promotion was blocked by 1 HIGH severity finding(s): Clean-board assert skips totalCount, 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 develop. This is a second look at once-reviewed code -- it passed per-feature review on the source branch, and has NOT had functional review yet (that happens on staging).

What the train did with each:

…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>
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@tracebloc-release-traintracebloc-release-trainBot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 26, 2026
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>
@tracebloc-release-traintracebloc-release-trainBot removed the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 26, 2026
LukasWodkaand others added 8 commits August 26, 2026 17:02
…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>
@tracebloc-release-traintracebloc-release-trainBot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 27, 2026
… (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.
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

Comment threadscripts/standards-sync.py
`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>
@tracebloc-release-traintracebloc-release-trainBot added gate-nudge Toggled by the release train to (re-)fire the fr-gate and removed gate-nudge Toggled by the release train to (re-)fire the fr-gate labels Aug 27, 2026
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

bugbot run

@tracebloc-release-traintracebloc-release-trainBot removed the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 27, 2026

@cursorcursorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

1 issue from previous review remains unresolved.

Fix All in Cursor

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit b5abac6. Configure here.

@tracebloc-release-traintracebloc-release-trainBot added the gate-nudge Toggled by the release train to (re-)fire the fr-gate label Aug 27, 2026
@LukasWodka

Copy link
Copy Markdown
ContributorAuthor

Promoted with 1 open Medium/Low Bugbot finding(s), per the severity policy in release-train's README (High stops the line; Medium/Low are recorded and ship, at both hops):

Create failure aborts remaining audit

This is a second look at once-reviewed code -- it passed per-feature review on the source branch, and has NOT had functional review yet (that happens on staging). Fix forward on develop if any is real.

What the train did with each:

@tracebloc-release-train
tracebloc-release-trainBot merged commit 0d01f47 into stagingAug 27, 2026
29 of 33 checks passed
@tracebloc-release-train
tracebloc-release-trainBot deleted the release-train/to-staging branch August 27, 2026 11:06
@tracebloc-release-train
tracebloc-release-trainBot restored the release-train/to-staging branch August 27, 2026 12:26
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gate-nudgeToggled by the release train to (re-)fire the fr-gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@LukasWodka