fix(pm): every patrol anchor states the cadence that makes a stalled Swept line decidable - #17841
Merged
claude[bot] merged 1 commit intoSep 12, 2026
Conversation
…Swept line decidable Four patrol writers stamp a `Swept <t>` line and tell the reader that a line which stops advancing means the standing caller died. None stated the interval that makes "stalled" decidable, and the four cadences differ by 4x, so a reader who carries one over from a neighbouring anchor is wrong in both directions. Each writer now renders the schedule, the interval and the next-expected deadline beside its own `Swept` stamp, and the heartbeat sentence points at that deadline instead of asking for a judgement it withheld the input for. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK
This was referenced Sep 12, 2026
claude
Bot
deleted the
claude/issue-17720-swept-line-states-its-interval
branch
September 12, 2026 12:07
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four patrol writers stamp a
Swept TIMESTAMPline on their anchor and then tell the reader to judge the patrol's liveness from it — a line that stops advancing means the standing caller died. None of them stated the interval that makes "stalled" decidable, and the four cadences differ by 4x, so a reader who carries one over from a neighbouring anchor reads a healthy 20-hour-old daily anchor as a dead caller, or a half-state patrol that has missed three consecutive runs as healthy. Each writer now renders its own schedule, interval and next-expected deadline beside itsSweptstamp, and the heartbeat sentence points at that deadline instead of asking for a judgement whose only input the body withheld. No cron moved, no sweep predicate changed, no exit code or band changed, and every existing stamp field (run log, commit, trigger, result, gate exit) is intact.Fixes #17720
Acceptance notes
Premises — all three hold on
origin/maine3411edascripts/pm/check-half-states.mjs:15203 (stamp) and :15205-15206 (heartbeat sentence);scripts/render-release-coverage-anchor.mjs:51-52 (sentence) and :81 (stamp), self-test pin at :283;.github/workflows/platform-checklist-watchdog.yml:51 (sentence, in a file comment) and :321 (stamp, inlineactions/github-script);.github/workflows/test-nightly-tiers.yml:454 (stamp, inlineactions/github-script).git grep -n -iE "expected every|next expected|schedule 37"over the six files returns nothing (exit 1). Control:stops advancinghits all four sites plus two workflow comments.Cadence table, measured from the files (not from the card)
cron:linescripts/pm/check-half-states.mjshalf-state-patrol.yml:13437 1,7,13,19 * * *scripts/render-release-coverage-anchor.mjsrelease-coverage-patrol.yml:10519 4 * * *github-scriptplatform-checklist-watchdog.yml:9951 2 * * *github-scripttest-nightly-tiers.yml:8129 5 * * *The spread is 4x, which is what makes a cadence inferred from a neighbouring anchor wrong in both directions.
Route chosen per site, and why
check-half-states.mjsPATROL_CADENCE_HOURS = 6, exported, with this exact cron quoted in its docblock. Promoting the cron string to a siblingPATROL_CRONis zero new plumbing; an env route would have added a second home for a fact the file already asserts. The script andhalf-state-patrol.ymlare copied VERBATIM into sibling repos together (that workflow's header documents the three-file install), so the pair travels as a unit. A self-test case assertscronPeriodHours([PATROL_CRON]) === PATROL_CADENCE_HOURS, so the two literals cannot drift apart from each other.render-release-coverage-anchor.mjsPATROL_SCHEDULEon the existingRender the reportstepenv:)PROVENANCE/ADVISORY_CODE/STRICT_CODEthe same way. The cron keeps exactly one home, in the file that schedules it. Unset,expectedCadence()statesexpected cadence UNSTATEDand names the unwired input rather than assuming a period.platform-checklist-watchdog.ymlenv: PATROL_SCHEDULE, beside the other single-sourced card strings)github-scriptno test reaches, so the honest route is the one with the least inline logic: one env read plus four lines of daily-cron arithmetic.test-nightly-tiers.ymlreportjob's in-place anchor rewrite is untouched — only the body text it composes changed.Parsing the workflow file was rejected everywhere. For the two inline sites it would put a YAML read plus a regex inside a body nothing can test; for the two
.mjssites it would couple a caller-agnostic renderer to a caller's path and need a fallback for an unreadable file — and a silent fallback is exactly the shape this card is about. ⛔ No shared module was introduced: the two.mjssites do not share one today, and the card forbids inventing one.Drift: what the restatement costs, and what pays for it
Sites 3 and 4 now restate their own cron in the same file, ~250 lines from the
cron:that declares it. Eachcron:gained a comment naming the env that has to move with it. That is the mitigation this change can afford; see "Not done, and why" below for the gate route that was refused.Work item C — pinning the two inline
github-scriptbodiesThe repo's existing pattern for pinning workflow prose is a static
check-*.mjsrun fromlint.yml. Exactly one exists —scripts/check-platform-checklist-watchdog.mjs, which pins that workflow's structure (triggers, thepathsfilter, board-writeif:guards, the package-script invocation), not its prose — andtest-nightly-tiers.ymlhas no counterpart at all. Covering both inline sites would mean a new clause in one gate plus a new gate for the other, and the card rules out a new gate. So per C's own escape clause: none applies, and both inline sites are left at the rendered literal. The two.mjssites are pinned in their own--self-tests, including the deadline computed from a fixed clock and the refusal branch.Reverse verification — both new pin sets were proven able to go red
Run from the committed state, mutation proven on disk by literal occurrence counts, restored by
git checkout HEAD -- PATHand verified byte-identical withgit hash-object(andgit diff HEADempty at the end). Both legs carried anEXIT INT TERMtrap.PATROL_CRONchanged from37 1,7,13,19 * * *to11 3 * * *(old literal count 1 to 0, new 0 to 1):check-half-states.mjs --self-testexit 1, 9 cases red, including#17720 next fire: the first slot strictly after the clock (got 1787195460000, want 1787125020000)and#17720 cadence: the declared interval is rendered. Restored: hash matches baseline.· ${expectedCadence({ schedule, sweptAt })}fragment deleted from the release-coverage stamp (call sites 2 to 1):render-release-coverage-anchor.mjs --self-testexit 1, 3 cases red — one per render branch (did-not-run / clean / findings), which is the point of pinning it per branch. Restored: hash matches baseline.The
Sweptline on the two RED-only cards carries one extra reading, and says soplatform-checklist-watchdog.ymlandtest-nightly-tiers.ymlfile or refresh their card only when red, and neither closes it on green. So a stalledSweptthere means the caller died or the thing went green — two readings, both ending at that card. Those two bodies renderexpected every 24h while this card standsand state both readings, rather than borrowing the two pinned anchors' unambiguous wording. The watchdog's header comment (its "this workflow has no such tell" trade) was corrected in the same direction: no tell on green, a full heartbeat with its deadline on a card that stands.Changeset — none,
skip-changeset, measuredRoot
package.jsonisprivate: true, and of the 70 published packages scanned, zero carry afiles[]entry reachingscripts/or.github/. Symbol search for everything this PR introduces (renderCadenceExpectation,nextCronFire,PATROL_CRON,expectedCadence,PATROL_SCHEDULE) acrosspackages/andexamples/: zero hits (grep exit 1). Positive control on the same command shape:defineStackinpackages/spec/src— 612 hits, exit 0. Nothing published moves.A FIFTH
Sweptwriter exists — reported, ⛔ not editedscripts/ci/scheduled-full-run.mjs:352 renders a_Swept ... _stamp for the hourly-full-run card (scheduled-full-run-card.yml). It sits insidescripts/, so the card's stated search area covered it; it did not surface because the card's search was conjunctive and this writer carries no "stops advancing" sentence. It is not an instance of this defect: its body states its own cadence one line below the stamp ("runs the FULL battery onmainevery hour"). Left untouched.Not done, and why
report-job rewrite semantics changed.scripts/check-platform-checklist-watchdog.mjscould cheaply carry for site 3. It would cover one of the two inline sites and leave the other needing a gate from scratch; both are outside this card.Gates
All 66 commands derived by
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackin this worktree were run; the per-command exit codes are in the report comment on #17720.dispatch-gates --ranreconciles: 66 derived, 66 run, 0 UNRUN. Plus the two self-tests the dispatch named, both exit 0.Generated by Claude Code