Skip to content

fix(pm): red the board-snapshot run when the archive has gone stale - #18266

Merged
claude[bot] merged 2 commits into
mainfrom
claude/issue-18137-board-snapshot-stale-archive-red-row
Sep 15, 2026
Merged

claude[bot] merged 2 commits into
mainfrom
claude/issue-18137-board-snapshot-stale-archive-red-row

Conversation

@claude

@claude claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

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: pending is the tool's own "could not check" — but the exit code is built from the count check's ok, and pending leaves that null. So a run that archived days-old state exited 0, and the workflow step that reacts to steps.snapshot.outputs.exit_code != '0' never fired.

Read off this repository's own archive — every manifest board-archive has ever carried (21 commits touching board/manifest.json, 2731e47a6 back to 16259758b) against this workflow's run history:

manifest generated_at count_check.verdict walk.delta.cursor run conclusion
2026-09-10T15:40Z ok (580/580) — (the run that finished the open set) success
2026-09-10T20:23Z … 2026-09-13T15:30Z (13 runs) pending — "the board's own count was not read this run" (no delta block) success ×13
2026-09-13T20:23Z pending — "this run's delta did not catch up" 2026-09-11T02:47Z success
2026-09-14T02:37Z pending (expected 512 / archived 485) 2026-09-12T00:40Z, two days behind success
2026-09-14T08:37Z / 14:28Z / 20:24Z pending 2026-09-12T14:32Z / 2026-09-13T11:19Z / 2026-09-14T04:46Z success ×3
2026-09-15T02:34Z surplus (expected 519 / archived 536) 2026-09-15T02:30Z failure (exit 2)

Nineteen consecutive pending runs, 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 of cb648cb1):

========== BEFORE — exit 0 ==========
  count check  PENDING — archived 485 open issue(s); the board reports 512.
               Why not a verdict: the open set is complete but was enumerated in an earlier run and this run's delta did not catch up with the live board, and the closed history is still walking — cards closed or opened since then are archived in their old state
  paused       the per-run budget of 800 requests is spent — this is a planned stop, not a failure.
               The next run resumes the history walk at 2026-08-06T13:26:08Z.

========== AFTER — exit 5 ==========
  (every line above, unchanged, then:)
  freshness    STALE — no run has reached a count-check verdict since 2026-09-10T20:23:11.768Z (78 h ago); the window is 24 h.
               This archive is behind the live board by an unknown amount, so the run is RED rather than green: a backup that is
               behind while reporting success is the one state this row exists to end. Whatever this run DID read is committed
               before the step that reads this exit code — a stale archive is better than none.
               It clears on the first run whose delta catches up with the live board and whose count check reports a verdict again.

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 code EXIT_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.cursor is the updated_at of the newest row the delta archived, not the instant the board was last read: an empty page is a short page, nextWalkStep ends 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-test rather than argued (a five-day-quiet board: delta complete, count_check: ok, cursor five days old, verdict fresh, exit 0) and ablated below.

It is a stamp, ⛔ not a counter of consecutive runs. The file's own materialManifest exists 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.

building is why a PR run stays green. The workflow's pull_request leg 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 snapshotCommit and push the archiveUpload the archive to R2Publish 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 to stale + exit 5, the correction's caught-up manifest replayed to fresh + 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 -c over a multi-line block, which is what a first attempt got wrong and the guard caught), restored with git checkout HEAD -- path and verified by blob hash against HEAD:

ablation on disk self-test
delete the EXIT_STALE leg from renderRun's exit selection removed-block 1 → 0, marker 1 RED — 2 of 145 fail: the card replay and the driven stale run
age the delta cursor instead of the streak (the literal reading of the card's condition (a)) removed-block 1 → 0, marker 1 RED — 2 of 145 fail, one of them the quiet-board case: the false alarm is real, and the case that names it is an instrument and not a decoration

Restore verified both times: blob 1ead53c1… equals HEAD:scripts/pm/board-snapshot.mjs and git diff HEAD is empty; the tree re-runs 145/145 green.

Gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack with no paths, on the merged head d53749c2 — 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-gates was 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_TOKEN with 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's files[].
  • Noted, not filed — the workflow header calls this script's self-test "70-case"; it was 125 before this PR and is 145 after. A stale number in a comment is a doc nit, not one of the three filing classes, and fixing it is outside this card's defect class. Carrier: the next PR that edits that paragraph.
  • Noted, not filed — the latest scheduled run (2026-09-15T02:31Z, run 46) is already red at exit 2 with a surplus of 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=N prints them back, and a number deliberately left gone belongs in gone.json. Acting on it is the seat's and the maintainer's, not this PR's.

Generated by Claude Code

`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>
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: d53749c2091906bf66c28f32e5ab4a6cbcfae933

Reviewing seat: domain:skills, in-seat review at the contract-review tier — get_session read at 2026-09-15T04:30Z: configured_model = session_context.model = last_served_model, the constant's tier, no fallback notice this round. Card #18137, claim 5674774580, report 5675341806; the PR declares Clause-②: no and the card's claim declares the same.

① Derived judgments

  • Accept set / public surface: none moves — scripts/pm/board-snapshot.mjs (+350/−5) and .github/workflows/board-snapshot.yml (+23/−2) against merge-base fd124714 (the head is the branch's merge of origin/main; no main commit since touches either file); repo-root PM tooling and its workflow, nothing published; skip-changeset on the PR (read back with ci/cd, size/m). No cron, no cap, no slice moved — the diff carries none of the three.
  • Shape 3 only, and the seat's P2(a) is FALSIFIED by measurement, said in the PR body and pinned: walk.delta.cursor is the updated_at of the newest row the delta archived, so on a quiet board it ages while the archive is exactly level — ageing it reds a quiet board (the driven quiet-board case, 5 days old cursor, count_check ok, verdict fresh, exit 0). The anchor is the count check's own pending streak: freshness.pending_since written once when the streak opens, carried byte-identical (the idempotence case survives, pinned), cleared on any verdict; STALE_AFTER_MS = 24 h from the first unconfirmed run = interval + 24 h ≈ 30 h on the four-a-day cron, calibrated on all 21 manifests the archive has ever carried (19 consecutive pending 2026-09-10T20:23Z → 2026-09-14T20:24Z, every scheduled run green; the one legitimate catch-up ran 30 h 04 m). Three verdicts (building / fresh / stale); EXIT_STALE = 5, distinct in the six-code register (pinned); precedence rate-limit > mismatch > stale > ok. The workflow's exit-code step is LAST, after the commit and the R2 mirror, so a red run never costs the records it archived — the step is renamed and its error line names code 5.
  • Seat-measured on the PR head in a detached worktree at d53749c: node scripts/pm/board-snapshot.mjs --self-test → 145 cases / 11 batteries pass, exit 0 (main 125 / 10; the dev's 125 → 145 holds; the card's manifest replayed → stale, EXIT_STALE; the caught-up manifest → fresh and EXIT_COUNT_MISMATCH from its own surplus); eslint on the script exit 0; check:workflow-status-functions and check:workflow-step-name-quoting exit 0 on the head; the workflow diff read whole (step order kept). The dev's before/after on the card's manifest (exit 0 → exit 5 with the freshness line) is accepted as reported; ablation red both legs (drop the stale leg → 2 of 145; age the cursor instead → 2 of 145 including the quiet-board case) with the blob-hash proof and the restore proved by git diff HEAD empty. The workflow cannot run here — NOT MEASURED, validated by the repo's workflow gates and by reading, stated.
  • Gates 51 / 51 / 0 / 0 with exits captured by redirect; check:pm-dispatch-gates detached per its header (1723, exit 0).

② Semver level

Not applicable — nothing published moves; skip-changeset measured by the fast track (scripts/pm/** + workflow).

③ Boundary flags

open_questions: none. Seven declared deviations, each answered: (1) P2(a) falsified — accepted, the pin is the instrument; (2) a stamp rather than a run counter, for materialManifest's reason — accepted; (3) no cadence constant in the script — accepted, the cadence stays the maintainer's; (4) the building verdict so pull_request runs and first adoptions stay green — accepted; (5) origin/main merged before the union for the stale-tree refusal — accepted; (6) process hygiene: a first detached battery killed and relaunched after the merge changed files under it, and a pgrep -f that SIGTERMed the dev's own shell — reported, no other agent's process touched, the sibling run in another worktree identified and left alone; accepted as reported; (7) a first ablation reading voided by a line-count grep and redone with block counts — accepted, the void reading was refused by the leg's own guard. BOARD STATE the dev surfaced and the seat owns publicly: scheduled run 46 (2026-09-15T02:31Z) is already RED at exit 2 — count_check.verdict = surplus, board 519 / archive 536, the first non-pending verdict since 2026-09-10. The seat's correction 5673977209 on #18137 cited that surplus as the healthy caught-up state; it is EXIT_COUNT_MISMATCH, the tool's destruction signature — the archive holds 17 records the board no longer has, consistent with the 2026-09-14 suspension. Not a code defect and not this PR's; it is a maintainer decision (--restore=N prints them back; a gone.json ledger entry quiets the red) and goes to the round report and the seat post's maintainer items. Out of scope, recorded: the workflow header's 「70-case」 self-test figure is stale (125 → 145) — a doc nit for the next PR that edits that paragraph.

Implemented-by: claude/issue-18137-board-snapshot-stale-archive-red-row
Reviewed-by: session_01HZfg2AwVX191qCizp88gQr

VERDICT: PASS


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Landing provenance — skills seat, session session_01HZfg2AwVX191qCizp88gQr, 2026-09-15T05:46Z. In-seat landing (path face NOT governed: scripts/pm/board-snapshot.mjs + .github/workflows/board-snapshot.yml): review of record 5675368811 (## Contract review PASS) names head d53749c2091906bf66c28f32e5ab4a6cbcfae933; ACCEPT 5675369063 on #18137; check-clause2-carriers --pair 18266 exit 0 re-read at 2026-09-15T05:45Z with the C6-RECORD note naming that record; on that head Lint & Repo Gates and TypeScript Type Check both success and every other check is success or skipped (36 runs: 25 success, 11 skipped, 0 running, 0 failed); mergeable_state clean. Ready through POST …/ccr/ready_for_review (200, read back draft: false), auto-merge armed through PUT …/ccr/auto_merge SQUASH (200, enabled, method squash); timeline reads ready_for_review 2026-09-15T05:45Z and added_to_merge_queue 2026-09-15T05:45Z; queue ref gh-readonly-queue/main/pr-18266-* present on origin at 2026-09-15T05:45Z, behind PR #18259's entry. MERGED is confirmed later by two readings (the queue ref gone + git log origin/main carrying (#18266) with one parent), then the card's residue is stripped.


Generated by Claude Code

Merged via the queue into main with commit e909148 Sep 15, 2026
38 checks passed
@claude
claude Bot deleted the claude/issue-18137-board-snapshot-stale-archive-red-row branch September 15, 2026 06:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

1 participant