Skip to content

fix(pm): every patrol anchor states the cadence that makes a stalled Swept line decidable - #17841

Merged
claude[bot] merged 1 commit into
mainfrom
claude/issue-17720-swept-line-states-its-interval
Sep 12, 2026
Merged

fix(pm): every patrol anchor states the cadence that makes a stalled Swept line decidable#17841
claude[bot] merged 1 commit into
mainfrom
claude/issue-17720-swept-line-states-its-interval

Conversation

@claude

@claude claude Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

Four patrol writers stamp a Swept TIMESTAMP line 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 its Swept stamp, 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/main e3411eda

  1. Four writers, none stating its cadence — CONFIRMED. scripts/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, inline actions/github-script); .github/workflows/test-nightly-tiers.yml :454 (stamp, inline actions/github-script).
  2. Zero interval spellings — CONFIRMED. git grep -n -iE "expected every|next expected|schedule 37" over the six files returns nothing (exit 1). Control: stops advancing hits all four sites plus two workflow comments.
  3. No open PR touches any of the six files — CONFIRMED, re-measured by this seat. All 21 open PRs' file lists pulled at 2026-09-12T10:5xZ (196 file rows); zero rows match any of the six paths.

Cadence table, measured from the files (not from the card)

anchor / writer workflow and its cron: line cron (UTC) interval
half-state patrol — scripts/pm/check-half-states.mjs half-state-patrol.yml :134 37 1,7,13,19 * * * 6 h
release-coverage patrol — scripts/render-release-coverage-anchor.mjs release-coverage-patrol.yml :105 19 4 * * * 24 h
platform-checklist watchdog — inline github-script platform-checklist-watchdog.yml :99 51 2 * * * 24 h
nightly tiers — inline github-script test-nightly-tiers.yml :81 29 5 * * * 24 h

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

site route why it is the cheapest here
check-half-states.mjs literal beside the renderer — already present The file ALREADY declared PATROL_CADENCE_HOURS = 6, exported, with this exact cron quoted in its docblock. Promoting the cron string to a sibling PATROL_CRON is zero new plumbing; an env route would have added a second home for a fact the file already asserts. The script and half-state-patrol.yml are 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 asserts cronPeriodHours([PATROL_CRON]) === PATROL_CADENCE_HOURS, so the two literals cannot drift apart from each other.
render-release-coverage-anchor.mjs env from the workflow that declares the cron (PATROL_SCHEDULE on the existing Render the report step env:) One caller, which already passes PROVENANCE / ADVISORY_CODE / STRICT_CODE the same way. The cron keeps exactly one home, in the file that schedules it. Unset, expectedCadence() states expected cadence UNSTATED and names the unwired input rather than assuming a period.
platform-checklist-watchdog.yml literal beside the caller (workflow-level env: PATROL_SCHEDULE, beside the other single-sourced card strings) The body is inline github-script no 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.yml literal beside the caller, same shape Same reason. The report job'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 .mjs sites 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 .mjs sites 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. Each cron: 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-script bodies

The repo's existing pattern for pinning workflow prose is a static check-*.mjs run from lint.yml. Exactly one exists — scripts/check-platform-checklist-watchdog.mjs, which pins that workflow's structure (triggers, the paths filter, board-write if: guards, the package-script invocation), not its prose — and test-nightly-tiers.yml has 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 .mjs sites 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 -- PATH and verified byte-identical with git hash-object (and git diff HEAD empty at the end). Both legs carried an EXIT INT TERM trap.

  • Leg 1PATROL_CRON changed from 37 1,7,13,19 * * * to 11 3 * * * (old literal count 1 to 0, new 0 to 1): check-half-states.mjs --self-test exit 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.
  • Leg 2 — the · ${expectedCadence({ schedule, sweptAt })} fragment deleted from the release-coverage stamp (call sites 2 to 1): render-release-coverage-anchor.mjs --self-test exit 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 Swept line on the two RED-only cards carries one extra reading, and says so

platform-checklist-watchdog.yml and test-nightly-tiers.yml file or refresh their card only when red, and neither closes it on green. So a stalled Swept there means the caller died or the thing went green — two readings, both ending at that card. Those two bodies render expected every 24h while this card stands and 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, measured

Root package.json is private: true, and of the 70 published packages scanned, zero carry a files[] entry reaching scripts/ or .github/. Symbol search for everything this PR introduces (renderCadenceExpectation, nextCronFire, PATROL_CRON, expectedCadence, PATROL_SCHEDULE) across packages/ and examples/: zero hits (grep exit 1). Positive control on the same command shape: defineStack in packages/spec/src612 hits, exit 0. Nothing published moves.

A FIFTH Swept writer exists — reported, ⛔ not edited

scripts/ci/scheduled-full-run.mjs :352 renders a _Swept ... _ stamp for the hourly-full-run card (scheduled-full-run-card.yml). It sits inside scripts/, 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 on main every hour"). Left untouched.

Not done, and why

  • ⛔ No cron changed, no sweep predicate touched, no exit code, band or report-job rewrite semantics changed.
  • ⛔ No new gate and no new clause in an existing one — including the cron-to-literal parity clause that scripts/check-platform-checklist-watchdog.mjs could 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.
  • ⛔ No other anchor, row or card edited.

Gates

All 66 commands derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack in this worktree were run; the per-command exit codes are in the report comment on #17720. dispatch-gates --ran reconciles: 66 derived, 66 run, 0 UNRUN. Plus the two self-tests the dispatch named, both exit 0.


Generated by Claude Code

…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
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 12, 2026
@claude
claude Bot marked this pull request as ready for review September 12, 2026 11:18
@claude
claude Bot added this pull request to the merge queue Sep 12, 2026
Merged via the queue into main with commit cd3ed83 Sep 12, 2026
44 checks passed
@claude
claude Bot deleted the claude/issue-17720-swept-line-states-its-interval branch September 12, 2026 12:07
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