fix(pm): red the board-snapshot run when the archive has gone stale - #18266
Conversation
`count_check: pending` is the tool's own "could not check", but the exit code was built from the count check's `ok` alone, which `pending` leaves null — so a run that archived days-old state exited 0 and the workflow's exit-code step never fired. Measured on this repository's archive: nineteen consecutive runs reported `pending` between 2026-09-10T20:23Z and 2026-09-14T20:24Z and every scheduled run in that window is green, including the one whose delta cursor was two days behind the live board on the morning a suspension destroyed cards the archive did not hold. A run now also answers whether the archive is still level with the live board: `freshness` in the manifest, one row in the summary, and EXIT_STALE (5) when no run has reached a count-check verdict for longer than a day. The anchor is the streak's start stamp, not the delta cursor — that cursor is the newest archived row's `updated_at`, so on a quiet board it sits days in the past while the archive is exactly level, and ageing it would red a board for being quiet. A stamp rather than a run counter keeps the manifest diff-stable: two writes per incident instead of one per run. The archive still lands: the commit and the R2 mirror run before the step that reads the exit code. The row prescribes no remedy — cadence, budget and slice stay the maintainer's. Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Reviewing seat: ① Derived judgments
② Semver levelNot applicable — nothing published moves; ③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Landing provenance — skills seat, session Generated by Claude Code |
Fixes #18137
Scope is the card's shape 3 only, per the skills seat's correction (comment 5673977209): shape 1 (delta-first) landed as #18054, shape 2 (more runs) and any budget raise are the maintainer's, shape 4 (an objectui port) is a separate card. This PR changes no cron, not the 800-request cap and not the 300-request delta slice.
The defect, measured rather than argued
count_check: pendingis the tool's own "could not check" — but the exit code is built from the count check'sok, andpendingleaves thatnull. So a run that archived days-old state exited 0, and the workflow step that reacts tosteps.snapshot.outputs.exit_code != '0'never fired.Read off this repository's own archive — every manifest
board-archivehas ever carried (21 commits touchingboard/manifest.json,2731e47a6back to16259758b) against this workflow's run history:generated_atcount_check.verdictwalk.delta.cursorok(580/580)pending— "the board's own count was not read this run"pending— "this run's delta did not catch up"pending(expected 512 / archived 485)pendingsurplus(expected 519 / archived 536)Nineteen consecutive
pendingruns, 2026-09-10T20:23Z through 2026-09-14T20:24Z, every one of them green — including the run on the morning an account suspension destroyed cards the archive was two days too far behind to hold. That is the card.The same manifest replayed through
renderRun, before and after this change (measured, not transcribed — the "before" leg imports the file as ofcb648cb1):What the row is, and the two spellings it deliberately does not use
A new manifest block,
freshness, with three verdicts —building,fresh,stale— and a new exit codeEXIT_STALE = 5, distinct from the count check's 2 (that one says the archive and the board disagree about a count both sides were read for; this one says no such reading has happened at all for a day).The anchor is the streak's start stamp, ⛔ not the delta cursor — and that is a falsification of the literal reading, not a preference.
walk.delta.cursoris theupdated_atof the newest row the delta archived, not the instant the board was last read: an empty page is a short page,nextWalkStepends the walk returning the cursor it was handed, so on a quiet board the delta completes every run, the archive is exactly level, and that cursor sits as far in the past as the last card anyone touched. Ageing it reds a board for being quiet. This is driven in--self-testrather than argued (a five-day-quiet board: delta complete,count_check: ok, cursor five days old, verdictfresh, exit 0) and ablated below.It is a stamp, ⛔ not a counter of consecutive runs. The file's own
materialManifestexists because a value that moves on every run commits a manifest-only diff on every scheduled run and buries the real ones. A streak start is written once when the streak opens, carried forward byte-identical while it lasts, and cleared when a verdict returns: two manifest writes per incident, and the pinned idempotence property survives untouched.The window: 24 h, measured from the first UNCONFIRMED run — which is itself one schedule interval after the last confirmed one, so a red lands a day plus one interval after the archive was last known level. On the four-a-day cron that is the 30 h the card asked for, and the script carries no cadence constant at all, so the maintainer's cadence decision touches nothing here. The calibration comes from the same 21 manifests: the one legitimate catch-up in this archive's history — a bounded delta slice closing a three-day gap after #18045 — ran 2026-09-13T20:23Z to 2026-09-15T02:34Z, 30 h 04 m, and every run inside it held an archive that did not have the newest cards. So the window never reds a single stalled run, never reds a day-long gap, reds the tail of a worst-case catch-up, and reds every run of the four-day streak this card was filed on.
buildingis why a PR run stays green. The workflow'spull_requestleg walks into a temp dir with no manifest under a--limit, so its open set never completes; without that verdict every PR touching this tool would go red on a tool working perfectly.How the red surfaces, and what it costs the archive
Nothing. The step order is measured and kept:
Run the snapshot→Commit and push the archive→Upload the archive to R2→Publish the run to the summary→ the step that reads the exit code, which is last in the job. A stale archive is better than none, so whatever a red run managed to read is committed and mirrored before the run is allowed to fail. The workflow's error line now names code 5 alongside 2/3/4, and its step is renamed to say what it now judges.⛔ The row prescribes no remedy: cadence, budget and slice are named nowhere in the verdict. What it prints is what clears it — the first run whose delta catches up and whose count check reports a verdict again. A self-test case pins that: the stale block contains none of "cadence", "budget", "slice" or "more runs".
Tests
--self-test: 125 cases / 10 batteries → 145 cases / 11 batteries, all green; the 125 existing cases keep their outcomes. The new battery ("the freshness row: a stale archive is never a green run") is 18 cases plus 2 added to the driven battery, including the card's own manifest replayed tostale+ exit 5, the correction's caught-up manifest replayed tofresh+ exit 2 (the count mismatch keeps its own code), the boundary at exactly the window and one millisecond past it, and two end-to-end driven runs.Reverse verification — the fix committed first, each leg proving its mutation reached the disk (block-occurrence counts, not a
grep -cover a multi-line block, which is what a first attempt got wrong and the guard caught), restored withgit checkout HEAD -- pathand verified by blob hash againstHEAD:EXIT_STALEleg fromrenderRun's exit selectionRestore verified both times: blob
1ead53c1…equalsHEAD:scripts/pm/board-snapshot.mjsandgit diff HEADis empty; the tree re-runs 145/145 green.Gates:
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackwith no paths, on the merged headd53749c2— 51 families derived, 51 run in the foreground with exit codes captured by redirect (cmd > log 2>&1; EXIT=$?), 51 exit 0, reconciled with--ran.check:pm-dispatch-gateswas run the way its own header prescribes (detached, log tailed) because its battery outruns a foreground command cap.⛔ Not measured here, and named rather than implied: the workflow file cannot be executed locally (no runner, no
GITHUB_TOKENwith this scope), so its change is validated by the repo's own workflow gates —check:ci-filter-parity,check:workflow-status-functions,check:workflow-step-name-quoting,check:self-test-workflow-commands,check:step-collectors— and by reading the step order. The 52 artifact-roster families, the 11 wide-population families and the one path-scheduled CI job the derivation names are outside the runnable total and are CI's.Acceptance notes
skip-changeset: nothing published moves.scripts/pm/**and.github/workflows/**ship in no package'sfiles[].surplusof 17 (board 519, archive 536). That is this tool's destruction signature doing exactly its job, and it is consistent with the 2026-09-14 suspension destroying cards the archive still holds;--restore=Nprints them back, and a number deliberately left gone belongs ingone.json. Acting on it is the seat's and the maintainer's, not this PR's.Generated by Claude Code