ci(devx): a daily watchdog workflow gives check:platform-checklist a reporting channel - #16092
Conversation
A GitHub Actions workflow on a daily schedule (plus workflow_dispatch) runs `pnpm check:platform-checklist` on `main`. On red it files, or refreshes, one issue carrying the gate's own output; on green it does nothing. The gate stays OUT of per-PR CI, exactly as the standing maintainer decision in lint.yml says — only the reporting channel changes. `scripts/check-platform-checklist-watchdog.mjs` is the static pin, wired into lint.yml on every PR: the workflow exists, fires on schedule + workflow_dispatch, carries NO pull_request / pull_request_target / merge_group trigger, and invokes the gate's package script rather than an inlined copy of its command. Every clause, positive and negative, has a --self-test fixture that makes it fire. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PU9zBGbH2s2ZtxSyu963M3
… alias `check:self-test-wired` reds the moment CI runs a script that ships a `--self-test` no workflow executes — and the watchdog makes CI run this one. Wiring the flag into the root alias is the remedy that gate names; a SELF_TEST_RUN_OTHERWISE row would be the finding written down somewhere quieter. The gate's header said a `--self-test` leg here "would execute NOWHERE". That premise is falsified by the watchdog, so the header is corrected rather than left to read as verified. The inline battery stays inline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PU9zBGbH2s2ZtxSyu963M3
…the pin narrows to match
Maintainer ruling of 2026-09-06, answering the A/B/C fork this PR opened: A.
The workflow gains a `pull_request:` trigger filtered to its own file and
nothing else — the posture every patrol in this repo keeps, and the one
`scripts/pm/dispatch-gates.mjs` pins tree-wide. It fires on no unrelated pull
request, so the standing decision's purpose ("an unrelated PR is never blocked
by checklist drift") is untouched; only its letter changed.
The pin narrows rather than drops its negative half: `merge_group` and
`pull_request_target` stay refused outright, and a `pull_request` trigger must
carry a `paths:` filter naming this workflow and nothing else. Unfiltered, an
empty list, a `paths-ignore:` complement, an extra path and a different path
each fire with their own message. A second clause follows from the first: no
board write may be reachable from a pull_request run, judged on the step's own
`if:`. 42 self-test assertions, up from 28.
Measured before the edit: `check:pm-dispatch-gates` went from
`3 of 1511 case(s) failed` to `1511 cases pass`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PU9zBGbH2s2ZtxSyu963M3
|
Review of head Generated by Claude Code |
…mment its own trigger falsified Review found two things and both are correct. 1. The workflow carried a comment claiming NO pull_request and NO merge_group trigger, four lines under the pull_request trigger this branch added, and false about the pin besides. Replaced with the true statement. 2. The new gate's self-test decided success by `failures.length === 0` and printed an UNCOMPARED case count, and the dispatch discarded its completion — so an early return would have printed `0 assertions` and exited 0. Landing a new scripts/** gate in that shape re-opens two closed sets by one. Brought to the landed shape: a frozen SELF_TEST_BATTERIES roster of eight battery names with per-battery floors, the roster's own size pinned, registerCase() attribution to the most recently opened battery, the floor evaluated at the verdict site before the green line, and the reached-verdict handshake read at the --self-test dispatch. Measured, not asserted: `measure-self-test-floor.mjs --json` now reads ROSTER for this file (was COUNT), and `--probe --only` reads HELD (0 DEFEATED, 1 HELD, 0 ACCIDENT). Every floor equals its measured count; 42 assertions across 8 batteries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PU9zBGbH2s2ZtxSyu963M3
Fixes #11730
Ruled C′ on 2026-09-02 (verbatim 「其他同意」, director record
5511022650). One clause of that ruling was then re-decided: implementing "nopull_requesttrigger" literally reddened a tree-wide pin, the fork was put to the maintainer, and on 2026-09-06 the answer was 「A(荐)」 — filter the trigger rather than forbid it. Recorded on #11730 as5556265772. The[Decision]title prefix is stale; this is an implementation card.What lands
.github/workflows/platform-checklist-watchdog.yml— a dailyschedule:plusworkflow_dispatch:workflow that runspnpm check:platform-checklistonmain. On red it files, or refreshes, exactly one issue carrying the gate's own output verbatim; on green it files nothing, edits nothing and closes nothing.It also carries a
pull_request:triggerpaths-filtered to that one workflow file and nothing else — the posturehalf-state-patrol.yml,release-coverage-patrol.ymlandrequired-set-patrol.ymlall keep, so a change to the patrol is exercised on a real runner before it merges. The filter is the whole of what makes that compatible with the standing decision: in the decision's own words in.github/workflows/lint.yml, "keeping it out of the per-PR path means an unrelated PR is never blocked by checklist drift". A trigger naming only this workflow fires on no unrelated pull request, so the decision's purpose is untouched and only its letter changed. ⛔ Apull_requestrun never writes to the board — it runs the gate, publishes to the run summary, and stops. The trigger block's own comment states that, and states which triggers are refused outright — an earlier revision of it claimed the opposite of the code four lines above and has been replaced.scripts/check-platform-checklist-watchdog.mjs— the static pin, wired intolint.ymlon every PR. Six clauses: the workflow exists and parses; it carriesschedule:(with a realcron:) andworkflow_dispatch:; it carries nomerge_group:and nopull_request_target:trigger, refused outright because no filter makes them safe here; anypull_request:trigger ispaths-filtered to this workflow alone; no board write is reachable from apull_requestrun, judged on the step's ownif:; and the gate is invoked aspnpm check:platform-checklist, never as an inlined copy of that alias's command.Its
--self-testcarries 42 assertions across 8 floored batteries: a frozenSELF_TEST_BATTERIESroster of battery names with per-battery floors, the roster's own size pinned (deleting an entry silences a floor as effectively as zeroing it),registerCase()attribution to the most recently opened battery, the floor evaluated at the verdict site before the green line, and the reached-verdict handshake read at the--self-testdispatch. Without those,failures.length === 0alone cannot tell "every case held" from "the cases never ran", and an earlyreturnwould have printed0 assertionsand exited 0 — the one shape a PR about making an invisible red visible must not land. Measured on the branch, not asserted:node scripts/measure-self-test-floor.mjs --jsonreads ROSTER for this file (it read COUNT before), and--probe --onlyreads0 DEFEATED, 1 HELD, 0 ACCIDENT. Every floor equals its measured count today.docs/qa/platform-checklist/RUNNER.md— the ruled line naming the workflow as the channel that sees red.README.mdgets the same correction where it said the gap was still open.package.json— thecheck:platform-checklistalias now runs the gate's own--self-testleg. Not cosmetic: see "Second-order finding".⛔ The standing maintainer decision —
check:platform-checklistis not on the per-PR critical path — is preserved, by the paths filter rather than by absence.The clause that was re-decided, and the measurement that forced it
scripts/pm/dispatch-gates.mjspins tree-wide that zero discovered gate families are SCHEDULED-ONLY, because every patrol here carries a paths-filteredpull_request:trigger. A watchdog without that trigger madecheck:platform-checklistthe first member. Measured, both legs, tree restored clean:pnpm check:pm-dispatch-gates, the required gate that reads it:eafd626(trigger refused outright)✗ dispatch-gates self-test: 3 of 1511 case(s) failed.✓ dispatch-gates self-test: 1511 cases pass.⛔
scripts/pm/dispatch-gates.mjsis not touched by this PR, and neither is its pin expectation.The negative clauses are demonstrated, not asserted
Six ablations, each committed first, each mutation proven on disk by a count of the exact text before any reading was taken, each restored and the restoration proven by an empty
git diff HEADplus agit hash-objectcomparison against theHEADblob.On the narrowed clause, against the real workflow:
paths:filter from thepull_request:triggerdocs/qa/platform-checklist/**toogithub.event_name != 'pull_request'from the issue step'sif:MUTATION DID NOT LAND — reading VOIDand refused to report a verdict. Re-run with a corrected anchor, it fires as above.On the clauses carried over from the first round: adding a
pull_request:trigger reddened the pin before it was narrowed; deleting the refused-trigger loop reddened the self-test (7 of 28 assertions) while leaving the live gate exit 0 — the reading that proves the self-test is the only instrument on that clause; deleting the workflow file reddened the pin.The
--self-testalso drives each narrowed case both ways: unfiltered,types:-only, an emptypathslist, apaths-ignore:complement, an extra path, a different path, and the bareon: [pull_request]sequence all fire; a comment mentioningpull_requestdoes not, and a step that writes nothing needs no guard.De-duplication — read off the precedent, and its lesson is negative
The C′ ruling names the Required-Set Patrol as the precedent for reporting by issue. Measured: it does not report by issue — it reports to the run summary plus an annotation and refuses a tracker outright in its own header.
half-state-patrol.ymlandrelease-coverage-patrol.ymldo report by issue, but via one pre-minted pinned anchor from a repository variable, rewritten every run including on green. Neither is "opens an issue, or updates the existing open one".The in-repo precedent that actually files-or-refreshes is
merge-queue-triage.yml, and its anchor lookup is copied here with its measured lesson intact:platform-checklist-watchdog) that is never graded off — ⛔ notpm:queueortooling, which are triage-state labels; keying on those makes the anchor invisible the moment a human triages it and files a fresh duplicate every day;A closed anchor is evidence and is not reopened: red again after the card was answered is a regression, so a new issue is filed naming the one it regressed from.
Verification
node scripts/pm/dispatch-gates.mjs --changed --commands --repo objectstack-ai/objectstack— identical 59-command family to the previous round — then reconciled: 59 derived, 59 run, 0 NOT-MEASURED, 0 UNRUN. Every exit code captured before any pipe.pnpm check:pm-dispatch-gates. Two exit 3 = PREREQUISITE NOT MET, i.e. NOT MEASURED and declared to CI:pnpm check:type-check-debtandpnpm check:dual-build-cjs-loadsboth demand a whole-treepnpm build; this diff changes no package source, so neither verdict can move on it.check:doc-formula-expressionsandcheck:dts-closurealso returned 3 on a cold tree and both returned 0 after building their two-package closure underscripts/pm/os-verify-lock.sh.pnpm check:platform-checklistitself: exit 0 on this tree — the watchdog would file nothing today, which is also the positive control that the pinned invocation actually runs.turbo ls --affectedagainst the merge base: 0 packages.pnpm lintiseslint . --no-inline-config, andeslint.config.mjsstates in its own prose that this repo "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file" — so this diff cannot move the verdict on any untouched file. Run over the changed JS files with--format json: 0 errors, 0 warnings.grep -naPover every changed file finds none.Second-order finding, fixed here
Making CI run the gate at all reddened
check:self-test-wired:scripts/check-platform-checklist.mjsships a--self-testthat no workflow executed. The remedy that gate names is wiring the flag, and aSELF_TEST_RUN_OTHERWISErow is explicitly "this finding written down somewhere quieter", so the alias carries the leg now. The gate's header argued a--self-testthere "would execute NOWHERE" — the watchdog falsifies that, so the header is corrected rather than left reading as verified. The inline battery stays inline.What only a real scheduled run exercises
Verified statically: the YAML parses, the triggers are present, absent and filtered as pinned, no board write is reachable from a
pull_requestrun, the invoked package script exists, and the gate it names runs green here. ⛔ NOT verified and not claimable from this container: the anchor lookup against the live issues endpoint, label minting on first file, theretries: 3delivery, andactions/github-script@v9behaviour on a runner. The exit criterion the earlier ruling wrote down — one manualworkflow_dispatchfire as a smoke test, verdict taken from GitHub output — is still owed and can only be taken after this merges. The newpull_requesttrigger means this PR's own successors get one leg of that for free: any later PR editing the watchdog will run it for real.Generated by Claude Code