Uh oh!
There was an error while loading. Please reload this page.
fix(2526): the archive asserts the board is clean, not that its writes worked - #339
Conversation
…s 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>
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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>
LukasWodka
commented
Aug 26, 2026
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 f57d269. Configure here.
saadqbal
left a comment
There was a problem hiding this comment.
The diagnosis is the valuable part: every guard in here answered "did my writes succeed?" and every one of them could answer yes while the board was untouched. 63 ok / green at 05:14 against 668 un-archived terminal cards at 06:24 on the same board with a different credential is the measurement that makes it undeniable, and pointing out that the file's own header had already named this failure mode — then guarded only against a failed archive — is the honest version of "why it shipped".
The null-Status count is the right instrument. An item returned with an unreadable field value is dropped by the filter and reported nowhere, so it's the one number that separates "the board is clean" from "this credential cannot see the board". Nothing printed it before.
Every new guard turns an absence into a refusal, which is the correct shape throughout: a failed re-read exits 1 rather than assuming clean, left != 0 is an error rather than a partial success, and nullst_after > 0 refuses because an item that can't be judged terminal means the run cannot show the board is clean. "Unverified is not verified-clean" is the whole principle in four words.
The shrink guard is the subtle one and I checked it can't false-positive: both first_total and reread_total count every item regardless of isArchived — which is exactly why the terminal filter tests isArchived == false separately — so a successful archive leaves the totals equal and only a genuine loss of visibility trips it.
aptracebloc
commented
Aug 26, 2026
Code review — The direction is right (assert board state, not just write-success) and the incident is genuinely caught via 1. [HIGH] 2. [MED] Total-blindness passes as "clean" (line 259 / 309-313). Fail-closed only triggers on 3. [LOW-MED] 4. [LOW] First-read 5. [LOW] Efficiency (step at line 209). The assert step re-paginates the whole board a second time (13 pages / 1252 items) each run, with 5 separate 6. [LOW] Duplication (line 235). The re-read GraphQL/pagination loop is a near-verbatim copy of the collect loop (line 88). Two ~40-line copies will drift; extract a shared script/step. 7. [LOW] Stale label (line 80 / header line 4). Step name "Collect un-archived terminal items (Prod / Cancelled)" and the header comment "terminal column (Prod / Cancelled)" both omit — drafted with Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
aptracebloc
left a comment
There was a problem hiding this comment.
Requesting changes — the structural fix is right, but the new "Assert the board is clean" step has a blocking logic defect. Flagging up front that green CI is not reassurance here: no PR check runs this workflow against the live board (only actionlint/shellcheck/pipefail), so the runtime logic is entirely unexercised by CI.
Blocker — the shrink check reds every healthy run (line 310).if [ "$reread_total" -lt "$first_total" ]; then ::error::; exit 1. Archived items are excluded from the ProjectV2 items connection (verified: a full read of project #2 returns ~1260 items, 0 with isArchived==true), so after a run archives K terminal items the re-read necessarily sees first_total − K < first_total → exit 1. Every run that archives ≥1 item — i.e. the normal case, and especially the first backlog-clear (~682 terminal items today) — goes red. The comment's premise ("a read that shrank mid-run for a reason that is not archiving") is inverted: archiving is exactly what shrinks the count. Compare against first_total − <archived count>, or count only still-live items rather than raw node totals.
Likely also blocking — nullst_after > 0 hard-fail (line 305). This fires on any un-archived null-Status card, and 2 genuinely-untriaged (No-Status) cards exist on the board right now, so a real run reds on them too. I get the fail-closed intent (backend#2526: can't-read-Status ≠ verified-clean) and I'm not asking you to drop it — but as written it can't distinguish "the credential couldn't read the Status" from "the card genuinely has no Status yet," and the latter is a normal board state. Worth narrowing the guard to the unreadable case.
The rest — a vacuous "clean" pass when both reads return 0 nodes, a dead select(.isArchived==false) filter, DRY between the two ~40-line pagination loops, and a "Prod / Cancelled" label that omits Done — are in the detailed comment above and are non-blocking.
To be clear on what's good: extracting the assertion into its own step and dropping the n==0 early exit 0 genuinely closes the unreachable-guard bug this PR set out to fix. It's the two new predicates that need adjusting.
— drafted with Claude Code
…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>
Summary
The daily archive has been reporting success for weeks while leaving most of the board untouched. Every guard in it answers "did my writes succeed?" — and every one of them can answer yes while the board is unchanged.
Measured today (2026-08-26). The 05:14 scheduled run:
exit 0, green. At 06:24 a human PAT read the same board and found 668 un-archived terminal cards — 475
Prod, 153Done, 40Cancelled. A manual dispatch minutes after that reportedUn-archived terminal items: 0.Same board, same query shape, two credentials, two answers.
The read logic is not the bug. It paginates correctly (
items(first:100)+hasNextPage/endCursor) and filters correctly (isArchived == false, status inProd/Cancelled/Done). The bug is that "I archived everything I saw" was allowed to stand in for "the board is clean", and nothing measured the gap.This file's header had already named the failure mode:
…and then guarded only against a failed archive, never against an archive that sees nothing.
What changed
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 the filter silently drops it and nothing reports it. That count is precisely what separates "the board is clean" from "this credential cannot see the board", and nothing has ever printed it. Non-zero 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. It also 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, and I want that explicit rather than implied.
tracebloc-release-trainApp is installed withrepository_selection: allandorganization_projects: write.So 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: whatever the cause, the next silent degradation cannot be silent.
Expect this to go red
Its next run should fail with ~668 items still terminal. That is the guard working, not a regression — the board was already in that state, just unreported. Two ways forward once the diagnostic prints:
Either way the board can then be cleared once, knowingly.
Type
fix
Test plan
actionlintclean locally on the modified workflow.yaml.safe_loadparses.run:blocks checked withbash -n.Checklist
developNote
Medium Risk
Changes only CI workflow behavior (expected red runs until permissions/root cause are fixed) but alters failure semantics for a critical board-hygiene job.
Overview
The daily kanban archive workflow no longer treats “archived everything I saw” as “the board is clean.” It adds visibility into what the App credential can actually read and a post-archive assertion that can fail the job when hygiene cannot be verified.
During the initial project scan, each item’s archived flag and Status (or
NULL_STATUSwhen unreadable) is recorded. The run logs and GitHub step summary now show items read, un-archived count, null-Status count, and terminal candidates; unreadable Status values emit a warning instead of being silently dropped from filters.The Archive step no longer exits early when there is nothing to archive, so later checks still run. Archive failures already exit non-zero; that behavior is unchanged.
A new Assert the board is clean step (skipped on dry run) re-paginates the board after archiving and fails closed if any terminal (
Prod/Done/Cancelled) items remain un-archived, if the re-read fails, if un-archived items have unreadable Status, or if the re-read sees fewer items than the first pass. This closes the case where the workflow went green while hundreds of terminal cards stayed on the board because the credential could not classify them.Reviewed by Cursor Bugbot for commit 353db78. Bugbot is set up for automated code reviews on this repo. Configure here.