feat(2348): a bug-labelled issue lands in Ready, not Backlog (backend#2348) - #313
Conversation
…#2348) The org rule -- "bugs get `work-type:bug` and go straight into `Ready` (defects skip refinement)" -- was written in org-standards.md and implemented nowhere: add-to-kanban puts every issue at `Backlog` and no workflow read the label. Measured 2026-08-22: seven bug-labelled tickets in one day, all seven left in `Backlog`. Implemented as a job on `customer-priority-bump.yml`, the only reusable already triggered by `issues: labeled` and already called by 16 of 19 repos -- so it reaches the fleet with no per-repo edit and touches none of conformance-gate's GUARDED contract files. MONOTONIC: promotes only from `Backlog` or from no Status, and asks the BOARD for the direction (col_index byte-identical to the closure router's) rather than holding a rank table. An inverted Backlog/Ready order refuses rather than performing a demotion. FAILS CLOSED like advance-deploy-env, not like the closure router: an unreadable board, a truncated page, an unplaceable column or a card that never reaches the project are all RED. Declining quietly here would reproduce the very defect being fixed -- and a `labeled` event fires once, where a cron gets another go next week. Tests: 46 assertions, extracted from the YAML by `# selftest:` markers and run verbatim; the Status vocabulary is derived from advance-deploy-env's rank() and cross-checked against the sibling suite, so all twelve columns are driven. 21 mutations, 0 stale, 0 uncaught. `make check` green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
…as the reads (backend#2348) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
bugbot run |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 4672df6. Configure here.
saqlainsyed007
left a comment
There was a problem hiding this comment.
Verified the routing, the token scope, and the test rigour against the code.
The gap was real and total. The org rule "bugs get work-type:bug and go straight to Ready" was in org-standards.md and every CLAUDE.md, but nothing implemented it — the string appeared in no workflow. Measured: 7/7 bug tickets on 2026-08-22 landed in Backlog and each needed a hand-run field update. A 100% miss is the tell that nothing did it.
The implementation is least-privilege and clobber-safe:
- The job mints an App token scoped to exactly
issues: read+organization-projects: write, runs withpermissions: {}(no caller GITHUB_TOKEN), and the non-narrowing ofrepositories:is justified (the org ProjectV2 write grant is unmeasured under repo-narrowing, and an unmeasured narrowing on a fleet-wide workflow would fail red on every defect filed). - It promotes only from
Backlog("but only from Backlog" — it won't move a card someone already advanced), theif:is explicitly a cost gate with the authoritative decision in alabel_gatewritten where the selftest can run it, and it guards PR-not-issue payloads. SOURCE_COLUMN="Backlog"/TARGET_COLUMN="Ready"are quoted soscripts/kanban-columns-check.py(extended here) asserts they exist on the live board — a Projects-UI rename reddens rather than silently no-ops.
Tested and mutation-tested honestly. bug-to-ready-selftest.py runs the extracted gate; bug-to-ready-mutations.py edits the code under test and treats a stale anchor (zero matches) as a failure exactly like an uncaught mutation, so inert coverage can't masquerade as good coverage (rules 5/9), and every refusal path names itself (rule 10). Armed intentionally on issues: labeled via the reusable 16/19 repos already call, rather than adding a second reusable.
CI green, the one review thread is resolved. LGTM.
|
/fr-pass |
Closes tracebloc/backend#2348.
The org rule — "bugs get
work-type:bugand go straight intoReady(defects skip refinement)" — is written inorg-standards.mdand therefore in every repo'sCLAUDE.md, and nothing implemented it.add-to-kanban.ymladds every new issue atBacklog; no workflow read the label afterwards. The stringwork-type:bugappeared in no workflow and no script in this repo.Measured 2026-08-22 (the ticket's own evidence): seven bug-labelled tickets filed in one day — backend#2324, #2327, #2329, #2340, #2341, #2344 and frontend-app#871 — all seven landed in
Backlogand each 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 — that split is the entire point — so a defect filed correctly and labelled correctly was invisible by default 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 inBacklogtoo, accurate and unread.Placement: a job on the reusable that already fires on
issues: labeledcustomer-priority-bump.ymlis the only reusable in the org already triggered byissues: labeled, and 16 of 19 repos already call it withsecrets: inheritand no inputs. A new job inside it reaches all sixteen with no per-repo edit, because a reusable is resolved from@mainat run time.No guarded file is touched.
conformance-gate.yml's GUARDED list isrepo-inventory.yml,scripts/caller-drift.py,scripts/tests/caller-drift-selftest.py,.github/workflows/caller-drift.yml,.github/workflows/conformance-gate.yml. This PR touches none of them, so the gate takes its "does not touch the repo contract" branch and is green in seconds with zero audit API calls. There is no merge ordering to respect and nothing to re-run when another PR lands.That was the deciding constraint. A new reusable would need a
repo-inventory.ymlrow for all 19 repos — andrepo-inventory.ymlis guarded, so that PR cannot merge untilcaller-drifthas passed on its exact head sha, which every other merge into this serialising repo invalidates. A rule that has been unimplemented since it was written should not queue behind that. (The alternative placement,add-to-kanban.yml, is worse still: it is the org's one per-repo copy, byte-compared bycaller-drift.py, so editing it here marks all eighteen other copiesDRIFTEDuntil the last repo's PR lands.)The cost, stated plainly rather than buried: the file name and the 16 caller names still say "customer priority bump", so a bug-label run shows up in each repo's Actions tab under that name. The reusable's own
name:is the half fixable without touching 16 repos, so it now readsLabel-driven issue triage. Renaming the file is a caller rollout (BUGBOT.md property 1 — land the callee first) and belongs with the wiring work below.Monotonic, and the direction is asked of the board
promote— atBacklog, or on the board with no Status. Those are the only two states from whichReadyis forward. An unplaced card is not "past Ready"; leaving it unplaced is what keeps the defect invisible.hold— everywhere else, green and logged:North Stars,In progress,Code review,On dev,Staging (agent review),FR on staging,Ready for prod,Prod,Done,Cancelled, alreadyReady, and archived. The label routinely arrives after triage moved the card, and demoting a shipped card would un-ship it on the board (.github#87–#92).noboard— an anchor is missing, orReadydoes not sit strictly afterBacklogon the live board. That second half is the monotonicity assertion: position is load-bearing, so one drag of the Status options in the UI turns this "promotion" into a demotion. It is checked before the no-Status shortcut, because a board that cannot be trusted to be in pipeline order cannot be trusted for any card. (backend#1994 is the same hole one file over: existence was checked and ORDER was not.)unknown— a column the board does not report. Defence in depth: the single atomic read makes it unreachable today, and it exists so thecasehas no fall-through to the write.Positions come from the board's own option order, via a
col_indexthat is byte-identical tokanban-closure-router.yml's (asserted by the selftest — same$PROJshape, one definition). No rank table is held here: a second copy of the column order agrees with itself while disagreeing with reality (backend#1729 rule 1).advance-deploy-env.yml'srank()is where the pipeline order is declared, and this suite parses it rather than restating it.Fail closed — and which sibling's direction this is
backend#2243 settled the three board consumers' failure paths as a set:
kanban-closure-router.ymlwrites a non-terminal holding state,kanban-reconcile.ymlskips,advance-deploy-env.ymlhas no fallback and aborts.This job is the
advance-deploy-envcase: no fallback, abort. Unreadable board, GraphQLerrors[]at exit 0, a truncated field page, an unresolvable Status/Readyoption, an unreadable project-items response, a card still absent after 5×5 s, an unplaceable current column, a failed write — every one is a red run, and the only green no-op is a state that was positively read and understood (hold, and a different label).Why not the router's quiet
exit 0: there, declining to write is the conservative end state — it is protecting shipped state from being overwritten. Declining here is not conservative, it reproduces the exact defect this job exists to fix: a defect parked inBacklogthat nobody reads. And reconcile may skip because it runs weekly and gets another go; alabeledevent fires once, so a green no-op is the last anyone hears of it. Quotingadvance-deploy-env.ymlon its own version of this: "a graceful degrade here means the board stops advancing while every run reports success."The other constraints
projectItems, never a scan of project chore: add auto-add to engineer kanban workflow #2's ~700 items.totalCountis read on both connections:> 20project items with the kanban card not among them, or a field page shorter than the field count, is a finding, not a "not on the board" (advance-deploy-env's.github#190lesson). A missingtotalCountis an unreadable response, not an empty one — so a non-resolving issue is named as that instead of being retried four more times and reported as "not on the board".Readyoption id keeps writing after the board moves under it, and writing the wrong column is strictly worse than writing nothing.org-standards.mdis where the rule is written, so the selftest parses the label and both columns out of that sentence and asserts the workflow'sbug-labeldefault,SOURCE_COLUMNandTARGET_COLUMNagainst them. Reword the rule and this reddens — both directions are mutation-proved.customer-priority-bump.ymljoinskanban-columns-check.py'sWRITERS(and the check'spaths:filter, which its selftest asserts againstWRITERS). That is what putsBacklogandReadyunder the daily live-board assertion — a rename in the Projects UI would otherwise turn this job into a silent no-op, since the option is resolved by name.permission-issues: read+permission-organization-projects: write, derived from the two calls the step makes, withpermissions: {}on the job.repositories:is deliberately not narrowed: that interaction is measured for an org-project read (kanban-columns.yml, backend#2181) and not for a write, and an unmeasured narrowing on a workflow that fires for every bug in the fleet fails red on every defect filed.mint-scope's existing EXEMPT row covers the oldbumpjob's full grant, not this one — 15 mint steps, 0 findings.Evidence
make check— exit 0, includingselftests-cover: all 15 selftests and 4 mutation runner(s) are wired to a target, and CI runs both tiers,actionlint: 0 findings(with shellcheck over the newrun:block),house-rules: no findings,mint-scope: 15 steps, no findings.The new suite is 46 assertions, and the decision under test is extracted from the YAML by its
# selftest:markers and run verbatim — never re-implemented (CLAUDE.md rule 9;.github#114/#115twice proved an inline copy goes on proving a regex nothing uses would have caught the bug). A dropped marker or a renamed function fails the run rather than falling back to a copy.The vocabulary is derived, not written down (CLAUDE.md rule 6 — mutation coverage cannot see a vocabulary gap): the 12 columns come from
advance-deploy-env.yml'srank()arms and are cross-checked againstkanban-deploy-state-selftest.py's independently writtenBOARD. Two derivations disagreeing is a finding, not a tie this file breaks. A monotonicity check that tried two columns would have passed withNorth Starspromoting too.Every refusal names itself (
refuse:other-label,refuse:pull-request-payload,refuse:unreadable-label,refuse:no-configured-label,refuse:not-an-issues-event) and every case pins the specific reason — CLAUDE.md rule 10: a refusal test that cannot say which refusal is a coin toss that reports success.Mutation-proved, 21 mutations, every anchor asserted applied (an inert mutation and real coverage produce an identical green log):
make checkruns--dry(anchors only, milliseconds);make mutationsruns the full pass and is executed byselftests.yml, an already-required context — so this arms with no branch-protection edit.What this does NOT cover
Named, because an unstated gap is how a suite comes to be trusted for more than it proves.
errors[]/truncation/card-absent fail-closed arms and theupdateProjectV2ItemFieldValuewrite are asserted by inspection and by copying the shape of the sibling workflows. The suite drives the three decision regions with stubbed payloads and a synthetic board. This is also why those arms carry no mutation: an UNCAUGHT verdict there would be about the suite's scope, not its rigour..github,release-trainandrfcsareexemptfor this reusable inrepo-inventory.yml(customer_priority_bump_caller_missing, UNREMEDIATED since 2026-08-03), so a bug filed in one of those three still lands inBacklog. The other 16 — includingbackendandfrontend-app, where all seven measured misses were filed — are covered. This wants its own ticket: the exemption's stated reason ("wiring more repos to it is sequenced behind backend#1408") is now weaker, because the reusable has grown a second thing worth having. The org-standards sentence names the three repos in the meantime, and that clause should be deleted when they are wired.main, not on merge. The 16 callers resolve@main, so this takes effect when the train promotes it. Nothing here proves the wiring end-to-end; the first bug labelled after that promotion is the real evidence, and it should be checked rather than assumed (add-to-kanban.yml's own comment block records what a green mint proves).opened+labeleddouble-fire is reasoned about, not measured. The Bug template applies the label at creation, so this can raceadd-to-kanban.yml; the 5×5 s retry is for that, and no test drives two concurrent runs. Both would compute the same idempotent write.Overlap with #309 — only one of these should land
#309 implements this same ticket as a new reusable (
bug-to-ready.yml) plus 19repo-inventory.ymlrows, and isBLOCKEDwithauditandgatered. Its own body argues the audit failures are the unrelated backend#1979 rollout, which is likely true — and it still needs a green audit on its head sha to clear the conformance gate, which every merge into this repo invalidates. That is the queue this PR was written to avoid.The two differ in placement and in the caller story, not much in the decision: both derive the option id from the live board, both refuse an inverted board, both extract the decision out of YAML for the selftest. Pick one. If #309 is preferred, close this; the ordering argument above is the only reason it exists.
🤖 Generated with Claude Code
Note
Medium Risk
Writes GitHub Projects Status via an org App token on every bug label in 16 repos. Fail-closed and monotonic, but a logic bug could still leave defects in Backlog or (if guards fail) move the wrong card.
Overview
When
work-type:bugis added, automation now moves the kanban card from Backlog to Ready so defects skip refinement. This lives as a newbug-to-readyjob on the existingissues: labeledreusable (customer-priority-bump.yml), so 16 already-wired repos pick it up from@mainwith no caller or inventory change.Promotion is monotonic: only Backlog or no Status; never backward, never archived. Option IDs and column order come from a live board read. Unreadable GraphQL, inverted columns, missing cards after retry, and unconfirmed writes fail red (a labeled event fires once). Token mint is scoped to issues read + org projects write.
The reusable display name is now
Label-driven issue triage.org-standards/CLAUDE.mdnote that.github,release-train, andrfcsstill need a hand move. Column names are registered withkanban-columns-check. A selftest extracts the decision from YAML plus a 21-mutation harness.Reviewed by Cursor Bugbot for commit 4672df6. Bugbot is set up for automated code reviews on this repo. Configure here.