Uh oh!
There was an error while loading. Please reload this page.
feat(kanban): wire the Done column into the automation (backend#1411) - #126
Conversation
…oc/backend#1411) Done was added to the board on 2026-08-01 and no code knew it existed. Two of the five gaps were live exposure, not latent: two cards (#1262, #1371) already sit there. advance-deploy-env rank() Done and Cancelled both returned 0, which inverted the monotonic guard -- so the next push carrying an old commit DEMOTED a Done card, and staging->develop back-merges re-carry old commits routinely. fr-gate rank() both fell through to "" and then to strict equality, so a single Done card BLOCKED every prod promotion carrying it. kanban-archive archived only Prod/Cancelled, so Done cards would never leave the board. kanban-reconcile filed hand-closed completed issues as Prod, asserting a deployment that never happened. kanban-closure-router set nothing at all for a hand-closed completed issue. Both rank functions now return 10 for Done and Cancelled -- above Prod's 9, so nothing can demote them and fr-gate's "Ready for prod or later" is satisfied rather than blocked. Verified by extracting both functions and running the full column set through them rather than reading the case arms: Ready for prod 8 Prod 9 Done 10 Cancelled 10 unknown 0 / "" The router now routes a hand-closed completed issue to Done immediately instead of delegating to reconcile's cron. Its old objection -- "do not invent a deploy state here" -- is exactly why Done is the right answer: it is not a deploy state. Reconcile stays as the backstop for anything the close event misses, and its own rule changes from Prod to Done for the same reason. Semantics are RFC-BACKEND-1405 D8: Prod = something deployed, Done = completed but nothing deployed, Cancelled = not planned. Sequenced deliberately after backend#1412 (merged and promoted to main in #124): arming those fail-closed assertions AFTER wiring Done would have enlarged the mutation population on the first run and turned a warning into a red cron. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
… card is not Done Two Bugbot findings on #126, both mine and both consequences of routing Done where "leave it alone" used to be. HIGH -- the closer lookup mapped any GraphQL or transport failure to NONE, which was harmless while NONE meant "leave Status unchanged". Now NONE means Done, so a transient API error on a PR-closed issue would move it to Done -- and kanban-archive, which this same PR taught to archive Done, would hide it the next day. I converted a fail-open default into a fail-WRONG one, which is worse than either. ERROR is now distinct from NONE, and the closer type is checked against an allowlist so an empty or unexpected read also leaves the Status alone with a warning. Verified by extracting the decision and running all six inputs: PullRequest main -> Prod · PullRequest develop -> On dev · Commit -> On dev NONE -> Done · ERROR -> unchanged · empty -> unchanged MEDIUM -- reconcile's terminalization covers cards in deploy columns too, so blanket Done labelled deployed work as "completed, nothing deployed" and overwrote what the closure router had correctly set. Only a card with no deploy state qualifies for Done. That leaves a closed card sitting in a non-terminal column, which D8's third invariant forbids while D8's own routing table says "follow the PR's stage". The two rules genuinely conflict for work that shipped to dev or staging and then stopped. Asserting something false is the worse option, so this reports and moves on; the conflict is raised on backend#1411 rather than silently resolved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LukasWodka
commented
Aug 3, 2026
Both fixed, and both were mine — same root causeEach finding is a consequence of putting Closer lookup failure sets Done (High) — the sharpest of the two. The lookup mapped any GraphQL or transport failure to
Reconcile Done ignores closing PRs (Medium) — also right. Blanket A D8 conflict this surfaces, which I have not resolvedSkipping deploy-column cards leaves a closed card in a non-terminal column — which D8's third invariant explicitly forbids ("no closed item may sit in a non-terminal column"), while D8's own routing table says "closed completed, closed by a PR merged to develop/staging → follow the PR's stage". Those two rules genuinely conflict for work that shipped to dev or staging and then stopped. There is no reading of D8 that satisfies both. I have chosen the option that does not assert something false, logged the skip, and raised it on |
Uh oh!
There was an error while loading. Please reload this page.
Third Bugbot finding on #126, and it is the asymmetry the previous commit created: I added the deploy-column guard to kanban-reconcile.yml and not to the router, so the two files disagreed about the same rule. The router's hand-close path set Done with no reference to the card's current column. Real path: reopen, then hand-close an issue sitting in On dev / FR on staging / Ready for prod -> the deploy state is overwritten -> kanban-archive.yml, which this PR taught to archive Done, hides the card the next day. The item lookup now also reads the card's current Status, and Done is refused when that Status records a deployment. Only Done is guarded: a PR-derived Status is a deploy fact and may advance a card normally. Done + On dev / FR on staging / Ready for prod / Prod -> skip, notice, exit 0 Done + anything else or unknown -> apply Prod / On dev / FR on staging (from a PR) -> apply Verified by extracting the guard and running the table rather than reading the case arm. Same root cause as the two before it: Done was introduced where "leave it alone" used to be, and every path that previously ended in "change nothing" needed re-examining -- including the ones in files I was not editing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LukasWodka
commented
Aug 3, 2026
Fixed — and this is the asymmetry my previous fix createdCorrect, and the finding is sharper than it looks: your last review made me add the deploy-column guard to The path is real: reopen, then hand-close an issue sitting in The item lookup now also reads the card's current Status, and
Verified by extracting the guard and running the table, not by reading the case arm. The pattern across all three findings on this PREvery one is the same shape, and none of them is in a line this PR set out to change:
Introducing a new terminal value into a state machine means every path that previously ended in "leave it alone" has to be re-examined — including the ones in files the diff doesn't touch. I checked the five sites the ticket named and not the paths that reach them. That is the actual lesson from #1411, and it is worth more than the change itself. |
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 a19d5cb. Configure here.
Uh oh!
There was an error while loading. Please reload this page.
Fourth Bugbot finding on #126, and the only pre-existing one: the "Determine target Status" step runs `gh api graphql` to find an issue's closing PR, and the step has no GH_TOKEN. So the lookup has failed on EVERY invocation since it was written. It was invisible because a failed lookup collapsed into "no closer", and "no closer" meant "leave Status unchanged". The closed-by-PR routing in this step -- the whole PullRequest/Commit branch -- has therefore never executed. kanban-reconcile.yml's cron has been doing all of it, which is precisely what its own comment describes: "the closure-router only moves issues closed by a linked PR (and can miss even those to a close-time race), and never moves hand-closed ones" The first clause was never true. It moved nothing. backend#1411 is what made this matter: the no-closer branch now sets Done, so a lookup that always fails means the Done path never runs either. Audited every gh call in the file -- this was the only step missing a token; the update step already uses the same secret, and the lookup is cross-repo so it needs the PAT rather than GITHUB_TOKEN. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Uh oh!
There was an error while loading. Please reload this page.

Closes
tracebloc/backend#1411. Unblocked by #124 landing, which is what put#1412's fail-closed assertions onmain— the ordering the work order requires.Two of these were live exposure, not latent
Donewas added to the board on 2026-08-01 and no code knew it existed. Two cards already sit there (backend#1262,#1371), so:advance-deploy-env.ymlrank()Done/Cancelledboth returned 0, inverting the monotonic guard — so the next push carrying an old commit demoted a Done card.staging → developback-merges re-carry old commits routinely, so this was not hypothetical.fr-gate.ymlrank()""and then to strict equality, so a single Done card blocked every prod promotion carrying it.kanban-archive.ymlProd/Cancelled, so Done cards would never leave the board.kanban-reconcile.ymlProd— asserting a deployment that never happened.kanban-closure-router.ymlVerified rather than read
Both
rank()functions extracted and run over the full column set, instead of eyeballing the case arms:10 > 9is what makes them terminal: nothing can demote them, andfr-gate's "Ready for prod (8) or later" is now satisfied rather than blocked.The router change, and why its old objection no longer holds
The router previously left a hand-closed completed issue alone, with the comment "Do not invent a deploy state here." That was right — and it is precisely why
Doneis the correct answer now:Doneis not a deploy state. It means completed with nothing deployed, which is exactly what a hand-closed issue is.Doing it on the close event rather than delegating to
kanban-reconcile's cron makes the card terminal immediately. Reconcile stays as the backstop for anything the event misses, and its own rule changes fromProdtoDonefor the same reason.Semantics throughout are RFC-BACKEND-1405 D8:
Prod= something deployed ·Done= completed but nothing deployed ·Cancelled= not planned.Ordering
Deliberately after
#1412, which #124 promoted tomain. The work order sequences it that way because arming those fail-closed assertions after wiringDonewould enlarge the mutation population on the first run and turn a warning into a red cron. With#1412already live, this lands against armed guards.Note for the reviewer
This repo has no test harness, so
actionlintandcheckare the only automated verification — hence the manual rank extraction above. Worth knowing that the epic's own#1415would be the place to change that; four of the five files touched here have had a defect found by review rather than by CI.🤖 Generated with Claude Code
Note
Medium Risk
Changes board Status mutations and prod FR-gate behavior across five workflows; mis-routing could hide cards (archive) or leave closed issues in wrong columns, though deploy-column guards and unreadable-closer handling reduce that risk.
Overview
Wires the Done column into engineer-kanban automation per RFC-BACKEND-1405 D8 (completed, nothing deployed vs Prod).
rank()inadvance-deploy-env.ymlandfr-gate.ymlnow maps Done and Cancelled to 10 (above Prod). Previously they ranked as 0 or unknown, which could demote Done cards on back-merge pushes and block prod promotions that carried a Done card.kanban-archive.ymlarchives Done alongside Prod/Cancelled so terminal hand-completed work leaves the board.kanban-closure-router.ymlsets hand-closed completed issues to Done immediately (not left for reconcile). AddsPROJECTS_KANBAN_TOKENto the closer GraphQL step so PR-based routing actually runs. Distinguishes lookup errors from no closer so transient failures do not incorrectly set Done (and get archived). Skips setting Done when the card is already in a deploy column (On dev through Prod).kanban-reconcile.ymlterminalizes closed-completed issues to Done instead of Prod, with the same deploy-column skip as the router.Reviewed by Cursor Bugbot for commit 1832db6. Bugbot is set up for automated code reviews on this repo. Configure here.