feat(kanban): a bug-labelled issue lands in Ready, not Backlog (backend#2348) - #309
feat(kanban): a bug-labelled issue lands in Ready, not Backlog (backend#2348)#309LukasWodka wants to merge 1 commit into
Conversation
…nd#2348) The org rule "bugs get `work-type:bug` and go straight into `Ready` (defects skip refinement)" was documented in CLAUDE.md, org-standards.md and every repo's contributor guidance, and implemented nowhere. `add-to-kanban.yml` adds every new issue at `Backlog`; no workflow read the label afterwards. Measured 2026-08-22: seven bug-labelled tickets filed in one day (backend#2324, #2327, #2329, #2340, #2341, #2344, frontend-app#871) all landed in `Backlog` and needed a hand-run `updateProjectV2ItemFieldValue`. A 100% miss rate is the tell that nothing does it at all, rather than that people forget sometimes. A NEW REUSABLE, not a line in `add-to-kanban.yml`. That file is the org's only per-repo COPY, byte-compared against this repo's by `caller-drift.py`, so an edit here makes all 18 other copies read DRIFTED on the next Monday audit -- and the documented escape (merge every other repo BEFORE .github) is impossible for a change that must be authored here first. A reusable resolves from `@main` at run time, so there is no byte comparison and no ordering trap. It also keeps `add-to-kanban.yml` free for backend#1877 and backend#2157, which both have it in their path. MONOTONIC BY CONSTRUCTION. It promotes only from `Backlog` or no-Status, and it refuses unless the live board reports `Ready` strictly AFTER `Backlog` -- so a reordered board makes the "promotion" a demotion and the workflow says so rather than performing it. Positions come from the board's own option order, never from a rank table held here. A card at `In progress`, `On dev`, `Prod`, `Done` or `Cancelled` is left where it is. Ships with ZERO callers, deliberately, for exactly one PR: the decision surface is reviewed on its own and the callers follow, which is the two-step `stale_backlog_migration_in_flight` and `blocked_gate_rollout_pending` already document. Every repo's inventory row says so in writing. Evidence: `make check` green (actionlint 0, shellcheck clean, house-rules clean, action-pins clean, mint-scope clean -- the new mint is scoped, not a twelfth unscoped one). 37 new assertions, 8 mutations all caught with their anchors asserted. `caller-drift.py` against the live org: 0 findings mentioning bug-to-ready.yml. `kanban-columns-check.py` against the live board now collects `Backlog` and `Ready` from this workflow and confirms both exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
saadqbal
left a comment
There was a problem hiding this comment.
Holding on the red, which again isn't this diff's fault, and flagging one thing I like a lot.
The red is collateral. audit reports 16 findings and every one reads <repo>: stale-backlog.yml is marked \exempt` but a caller exists (stale-backlog-caller.yml). The exemption is stale— averaging-service, backend, claude-skills, cli, client and the rest of the wave. That's the transient window.github#307argues for, now at full width because the wave's callers have landed;.github#293's flip closes it. Since audit` is fleet-wide, every PR in this repo is red until then, so the sequence is #307 → #293 → re-run here. Nothing to fix on your side.
The substance is enforcing a rule that already existed and was being missed 100% of the time. The org standard is explicit — bugs get work-type:bug and go straight to Ready, because defects don't wait for refinement — and the measurement (seven bug-labelled tickets in one day, every one left in Backlog and hand-moved) is the right kind of justification: not "this would be tidier" but "the documented rule has never once been followed by the automation". Framing it as Ready being the queue engineers pull from, so a correctly-filed and correctly-labelled bug was invisible by default, is the consequence that makes it worth a workflow rather than a reminder.
The part I'd single out is registering bug-to-ready.yml in kanban-columns-check.py in the same PR, with the reason stated: Ready and Backlog are load-bearing in opposite directions, so a board rename turns this workflow into either a red run or a permanent no-op. A new workflow that reads two column names and doesn't join the cross-check is exactly how the vocabulary drifts silently — and a permanent no-op is the failure mode nobody notices, which is the same argument #304 makes about a sweep arm that skips. Adding it now rather than after the first rename is the difference between a guard and an incident report.
Also right: bug-label as an input rather than a setting, with Ready deliberately not configurable. A repo that could configure the destination away could opt out of the standard, which would defeat the point.
To be straight about scope — I've read the rule, the trigger reasoning (opened + labeled, because the template applies the label at creation) and the column-check registration, not all 347 lines line-by-line. Re-request me when it's green and I'll finish the pass over the workflow body.
Closes tracebloc/backend#2348.
The org rule — "bugs get
work-type:bugand go straight intoReady(defects skip refinement)" — is written in CLAUDE.md, inorg-standards.mdand in every repo's contributor guidance, and nothing implemented it.add-to-kanban.ymladds every new issue atBacklog, and no workflow read the label afterwards.Measured 2026-08-22: seven bug-labelled tickets filed in one day — backend#2324, #2327, #2329, #2340, #2341, #2344 and frontend-app#871 — all seven landed in
Backlogand needed a hand-runupdateProjectV2ItemFieldValue. A 100% miss rate is the tell that nothing does it at all; a rule people mostly follow produces a mixed record.Readyis the queue engineers pull from andBacklogis the refinement queue nobody pulls from, so a defect filed correctly and labelled correctly was invisible to the people meant to pick it up. The release train's deferral story rides the same path: it files a ticket for every Medium/Low it ships as "a record rather than a dismissal", and those landed unread too.Design choice: a new reusable, not a line in
add-to-kanban.ymladd-to-kanban.ymlis the obvious place — every issue already passes through it — and it is the wrong place, for two independent reasons.1. It is the org's only per-repo COPY, and copies are byte-compared.
copies:inrepo-inventory.ymlis a list of exactly one entry, andcaller-drift.pycompares each repo's copy against this repo's by git blob sha:2. Concern separation, for the tickets already queued behind it.
add-to-kanban.ymlanswers "is this on the board"; this answers "which column does the label imply". backend#1877 (cross-repo PR surfacing) and backend#2157 (token scoping) both have that file in their path, and a second concern welded into it is a second concern they have to carry.The fleet-ordering finding — stated plainly
Editing
add-to-kanban.ymlhere is not an edit to one file; it is an edit to nineteen. The moment a modified canonical copy is on.github's audit branch, all eighteen other repos' copies goDRIFTEDand the Mondaycaller-driftaudit is red until the last repo's PR lands. The workspace CLAUDE.md already records the rule this implies: "a fleet-wide bump must merge every other repo before.github, or the org audit goes red onmainuntil the last one lands."For a new feature that ordering is not merely awkward, it is impossible: the canonical text has to exist somewhere before it can be copied, and the only somewhere is this repo. So the choice was between a guaranteed red-audit window across the fleet, or a mechanism with no byte comparison at all. A reusable is resolved from
@mainat run time — the fleet picks up a change with no per-repo edit and nothing to compare. That is a real argument for the reusable and it is the deciding one.What this PR does cost, and how it is paid. A reusable still needs one thin caller per repo, and that rollout is a second PR. The order is forced by this file's own header (".github's caller state is read from its audit branch over the API, so adding a caller and flipping its entry to
requiredin one PR fails"), so the reusable lands with zero callers and every repo's row isexemptwith a written reason — the same two-stepstale_backlog_migration_in_flightandblocked_gate_rollout_pendingalready document. The anchor says out loud that if it is still there after the rollout it has become a defect, not a justification. The rollout PR flips all nineteen rows torequiredand adds the README onboarding line; adding that line now would point a new-repo onboarder at a caller file no repo has.Verified:
caller-drift.pyagainst the live org produces zero findings mentioningbug-to-ready.yml. (It reports 16 pre-existing findings, allstale-backlog.yml is marked exempt but a caller exists— that migration's callers have landed and its inventory flip is overdue. Untouched here, but somebody should close it.)The monotonicity guarantee
promote— the card is atBacklog, or is on the board with no Status. Those are the only two states from which moving toReadyis forward. A card with no Status is not "past Ready"; leaving it unplaced keeps it invisible, which is the complaint.hold— anywhere else, green and logged. The label arrives at any point in a card's life and usually arrives after triage has moved it.In progress,On dev,FR on staging,Prod,Done,Cancelled— all left alone.noboard— refuses loudly when the anchors are missing or whenReadydoes not sit strictly afterBacklogon the live board. That second half is the monotonicity assertion: drag the Status options soReadyprecedesBacklogand this workflow's "promotion" becomes a demotion, so it must refuse rather than perform it. Existence was checked and ORDER was not in the sibling classifier until backend#1994; that hole is not reproduced here, and the selftest drives the inverted board including the card sitting atBacklog— the one case that would actually perform the demotion if the anchor check ran second.Positions come from the board's own option order, via the same
col_indexshapekanban-closure-router.ymluses. No rank table is held here: a second copy of the column order agrees with itself while disagreeing with reality (backend#1729 rule 1).Also per the constraints: issues only (a
pull_requestpayload is refused with or without the label — per the board's model an issue occupies Backlog/North Stars/Ready and the flow columns are for PRs), triggers on bothopenedandlabeledat the caller (the Bug template applies the label at creation; a later label is the common case), and it fails closed everywhere — an unreadable labels payload, a card not on the board after 5×5s, a project with no readableStatusfield, and an unplaceable current column are all red runs, never a green no-op. That last one is deliberately louder than the sibling router's quietunknown, and the file says why: there, declining is the conservative end state; here it means the monotonicity proof could not be made from a read that should have been atomic.Token scoping
Not a twelfth unscoped mint (backend#2157).
permission-issues: read+permission-organization-projects: write,repositories:narrowed to the calling repo, jobpermissions: {}— derived from what the step actually runs.mint-scopecounts it among the scoped ones.Evidence
make check— exit 0.kanban-columns-check.pyagainst the live board — the ticket asked for exactly this, a test against the board's option list rather than a hardcoded name:The new suite — 37 assertions, and the decision under test is extracted from the YAML by its
# selftest:markers and run verbatim, never re-implemented (.github#114/#115twice proved an inline copy goes on proving a regex nothing uses would have caught the bug):The board's column order is parsed out of
kanban-deploy-state-selftest.pyrather than written down a second time, and the whole vocabulary is driven through the gate — mutation coverage cannot see a vocabulary gap (backend#1729 rule 6).Mutation-proved, every anchor asserted applied (an inert mutation and good coverage produce an identical green log):
And the extraction itself fails loudly rather than testing a stale copy:
What the selftest does NOT cover
Named, because an unstated gap is how a suite comes to be trusted for more than it proves:
updateProjectV2ItemFieldValuewrite are not driven — the suite runs the two decision regions with a stubbedcol_indexand stubbed payloads. The write path is asserted only by inspection and by the shape of the sibling workflows it copies.issues: [opened, labeled]reaches this reusable, because no caller exists yet. That evidence is the rollout PR's to produce, and it is a landed card — a green mint proves nothing (the lessonadd-to-kanban.yml's own comment block records).opened/labeleddouble-fire is reasoned about, not measured. Both runs compute the same write and theconcurrencygroup serialises them; no test drives two concurrent runs.unknownis defence in depth, not a live path. The workflow's single atomic query is what makes a card's Status always a name the options list contains. The arm exists so thecasehas no fall-through and is driven against a synthetic board; nobody has produced it from a real one.kanban-closure-router.ymlalready makes; this PR adds the inverted-anchor refusal for the two columns it cares about, not a general proof.CI status — the two red checks are pre-existing, not this PR
audit(caller-drift) andgate(conformance-gate) are red.gatefails becauseauditdid — it requires a passing audit for any contract change and does nothing else. So there is one failure, not two.auditreportsFINDINGS_TOTAL: 16, and all sixteen are the same finding:Zero mentions of
bug-to-ready.ymlanywhere in the audit log. This is the backend#1979 rollout mid-flight: its per-repo callers have been landing through today while the inventory flip (ci/1979-arm-the-sweep) is still open, and every landed caller adds one finding until it merges. The same job was red with 2 of these findings at 18:40 today on the unrelatedfix/2242-rfcs-exemption-reasons, and with 16 by 20:51 — it is counting the rollout, not this branch. This PR's inventory diff touchesstale-backlogon exactly one line, and that line is prose in the new anchor naming the sibling anchor.Reproduced locally on this branch before pushing: same 16, same zero. This PR adds no drift finding. It goes green the moment
ci/1979-arm-the-sweeplands; it needs nothing from here.Everything else is green, Bugbot included.
🤖 Generated with Claude Code