Commit cd3ed83
fix(pm): every patrol anchor states the cadence that makes a stalled Swept line decidable (#17841)
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-test`s,
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 1** — `PATROL_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/src` — **612 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](https://claude.ai/code/session_01MCLBsUgfykL74aU716rzVK)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent 6059b29 commit cd3ed83
5 files changed
Lines changed: 339 additions & 16 deletions
File tree
- .github/workflows
- scripts
- pm
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
51 | | - | |
52 | | - | |
53 | | - | |
54 | | - | |
55 | | - | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
56 | 63 | | |
57 | 64 | | |
58 | 65 | | |
| |||
96 | 103 | | |
97 | 104 | | |
98 | 105 | | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
99 | 112 | | |
100 | 113 | | |
101 | 114 | | |
| |||
154 | 167 | | |
155 | 168 | | |
156 | 169 | | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
157 | 182 | | |
158 | 183 | | |
159 | 184 | | |
| |||
313 | 338 | | |
314 | 339 | | |
315 | 340 | | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
| 348 | + | |
| 349 | + | |
| 350 | + | |
| 351 | + | |
| 352 | + | |
| 353 | + | |
| 354 | + | |
| 355 | + | |
| 356 | + | |
| 357 | + | |
316 | 358 | | |
317 | 359 | | |
318 | 360 | | |
319 | 361 | | |
320 | 362 | | |
321 | | - | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
322 | 371 | | |
323 | 372 | | |
324 | 373 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
102 | 102 | | |
103 | 103 | | |
104 | 104 | | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
105 | 110 | | |
106 | 111 | | |
107 | 112 | | |
| |||
238 | 243 | | |
239 | 244 | | |
240 | 245 | | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
241 | 254 | | |
242 | 255 | | |
243 | 256 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
78 | 78 | | |
79 | 79 | | |
80 | 80 | | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
81 | 87 | | |
82 | 88 | | |
83 | 89 | | |
| |||
121 | 127 | | |
122 | 128 | | |
123 | 129 | | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
124 | 142 | | |
125 | 143 | | |
126 | 144 | | |
| |||
440 | 458 | | |
441 | 459 | | |
442 | 460 | | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
443 | 478 | | |
444 | 479 | | |
445 | 480 | | |
| |||
451 | 486 | | |
452 | 487 | | |
453 | 488 | | |
454 | | - | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
455 | 497 | | |
456 | 498 | | |
457 | 499 | | |
| |||
0 commit comments