Uh oh!
There was an error while loading. Please reload this page.
release-train: develop -> staging - #365
Conversation
…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.
…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>
LukasWodka
commented
Aug 27, 2026
bugbot run |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 33944d5. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
LukasWodka
commented
Aug 27, 2026
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): Caller fires edited before callee guard 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 What the train did with each:
|
Uh oh!
There was an error while loading. Please reload this page.

Automated promotion by the release train (RFC-0008 D14). Head is the train-managed
release-train/to-stagingbranch (a mirror ofdevelop), so it never collides with a human PR. Merged only when the fr-gate is green.Note
Medium Risk
Changes org-wide GitHub Actions behavior (kanban project writes, new status API sweeps, and closing-ref policy) with broad test coverage but real fleet impact if routing or tokens misbehave.
Overview
This promotion bundles several fleet CI / kanban changes in
tracebloc/.github.Kanban model (backend#2722): Completed issues are routed to
Doneinstead of mirroring a closing PR’s deploy column. That removes the GraphQL “closer” logic inkanban-closure-router.yml, stopsadvance-deploy-envfrom advancing closing issues cross-repo, and alignskanban-reconcileso PR/commit-closed issues also terminalize toDonerather than derivingOn dev/ staging columns. App token comments drop issue-read scope where it’s unused; selftests are updated for the slimmerbranch_status_mapusage.Merge-conflict visibility (backend#2637): New scheduled
conflict-gateworkflow plusscripts/conflict-gate.pysweep open PRs and post aconflict-gate / mergeablecommit status (failure/success/pending) because conflicted PRs run nopull_requestchecks. Includes selftests and mutation coverage in theMakefile.Closing-ref gate (backend#2616, #2556):
closing-ref-gate.pynow requires title-to-reference agreement:Closesor canon-derived non-closing forms likePart ofparsed fromorg-standards.md(with fail-closed derivation). Callers listen for PRedited;set-pr-statusjobs only run while the PR is open so merged PR title/body edits don’t demote cards. Non-draft board status label becomes Code review. Docs (CLAUDE.md,org-standards.md) match the new rules.Reviewed by Cursor Bugbot for commit 33944d5. Bugbot is set up for automated code reviews on this repo. Configure here.