Repository navigation
Commit 3fcafcd
ci: fold the four daily patrols into one fleet-patrols.yml workflow (1 of 3) (#26512)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ8Bh55CNSFH3iN1HNN1Gog95)_
Part of #26071
Clause-②: no
Metadata-protocol: none
Ruling-ref: `Ruling: batch #397 item 2 · letter A · maintainer 「同意」` —
comment 6416984681 on #26071 (director seat, 2026-10-26T13:18Z). This PR
is (ii) of letter A, the first of three serial PRs from one card thread:
(ii) the four daily patrols → one workflow (this), then (iii) the four
PR-body guards → one workflow, then (i) the `lint.yml` schedule skip.
The card closes with the last one. Head branch
`claude/project-thread-31axox` is the card thread's harness push branch
(the measured platform exception to `claude/issue-N-slug`; the next two
PRs use `-2` and `-3`).
## 维护者速读
- **改了什么**:四个日巡逻
workflow(required-set-patrol、platform-checklist-watchdog、release-coverage-patrol、board-snapshot)合为一个
`.github/workflows/fleet-patrols.yml`,四个 job 各守自己原来的 cron;每个 job 的
steps、permissions、concurrency、env 与原文件解析后逐项相同(脚本比对,见
Tests)。读这些文件名的两个门禁(`check-required-contexts` 的
`PATROL_WORKFLOW`、watchdog 静态 pin 的文件名与 `pull_request`
路径白名单)、锚点渲染器发布的文案、归档器的 adopt 清单、两份 checklist 文档改指新文件。
- **为什么改**:裁决 A(批 #397 第 2 项,「同意」)的 (ii):四个文件各自 checkout + setup、各占一个
workflow;合为一文件四 job 省文件不省信号 —— 失败信号、最小权限、并发组都留在 job 级,没有一项上提到 workflow
级。
- **风险与回滚**:纯搬运,不改任何判定逻辑。风险两处:① 每个 job 的 `if:` 必须命中自己的 cron ——
`github.event.schedule` 与 `on.schedule` 里的字符串是同一事实写两遍,文件头注已标 ⛔ 同改;②
`pull_request` 触发是四个原过滤器的并集,所以改 `scripts/pm/board-snapshot.mjs` 之类的 PR
现在也跑 watchdog job(不阻塞:它不是 required context,checklist 红也不红 job)。回滚 =
revert 本 PR,四个原文件原样回来。
- **先看**:① `fleet-patrols.yml` 四个 job 的 `if:`(各 job `name:` 下方三行)与
`on.schedule` 的四条 cron;② `scripts/check-platform-checklist-watchdog.mjs`
的 `PR_TRIGGER_ALLOWED_PATHS`(冻结的并集)与 clause 4 文案;③
`scripts/check-required-contexts.mjs` 的 `PATROL_WORKFLOW`。
- **两分钟验证**:`node scripts/check-platform-checklist-watchdog.mjs
--self-test && node scripts/check-platform-checklist-watchdog.mjs &&
pnpm check:required-contexts` —— 三条 OK 行;落地后在 Actions 页对「Fleet Patrols」点
Run workflow,`patrol` 选 `required-set-patrol`,应只跑那一个 job,其余三个 skipped。
- **需要你做的**:Approve(Tier H,`.github/workflows/**`);席位随后落地。
## What changes
File by file, against the retired workflow(s).
### `.github/workflows/fleet-patrols.yml` (new) ←
`required-set-patrol.yml`, `platform-checklist-watchdog.yml`,
`release-coverage-patrol.yml`, `board-snapshot.yml` (deleted)
- **`on.schedule`** — the four crons, each under the retired file's own
cron comment, re-read on this tree and equal to the card's measurement:
`23 4,16 * * *` (required-set), `51 2 * * *` (watchdog), `19 4 * * *`
(release-coverage), `7 2,8,14,20 * * *` (board-snapshot).
- **`on.workflow_dispatch`** — one `patrol` choice input (`all` by
default, or a job id) so a manual fire or smoke test can target one
patrol, as each retired file allowed, without spending the archiver's
800-request budget to smoke-test the watchdog.
- **`on.pull_request.paths`** — the union of the four retired filters,
each retired file's own path replaced by the new file:
`.github/workflows/fleet-patrols.yml`,
`scripts/check-required-contexts.mjs`,
`scripts/check-release-section-coverage.mjs`,
`scripts/pm/board-snapshot.mjs`, `scripts/pm/check-half-states.mjs`,
`scripts/invoked-as.mjs`. A matching pull request runs all four jobs;
none writes anything on a `pull_request` event (every board write keeps
its `github.event_name != 'pull_request'` guard, which the watchdog pin
judges across every job of the file).
- **No workflow-level `permissions:`, `concurrency:` or `env:`.** Each
job carries the retired file's blocks at job level, unchanged:
- `required-set-patrol` (display name `Live required-set sweep`, was job
`sweep`): `permissions: {contents: read}`, concurrency group
`required-set-patrol-${{ github.event.pull_request.number || github.ref
}}`, `timeout-minutes: 10`, 5 steps.
- `platform-checklist-watchdog` (`Platform checklist gate sweep`, was
`watchdog`): `{contents: read, issues: write}`, group
`platform-checklist-watchdog`, `env` ANCHOR_IDENTITY_LABEL /
ANCHOR_TITLE / ANCHOR_MARKER / ANCHOR_ROUTING_LABELS / PATROL_SCHEDULE,
`timeout-minutes: 15`, 5 steps.
- `release-coverage-patrol` (`Release section-coverage sweep`, was
`patrol`): `{contents: read, issues: write}`, group
`release-coverage-patrol`, `env.ANCHOR_ISSUE` (the folded scalar, same
relative indentation), `timeout-minutes: 10`, 8 steps.
- `board-snapshot` (`Snapshot the board to the archive branch`, was
`archive`): `{contents: write, issues: read, pull-requests: read}`,
group `board-snapshot`, `timeout-minutes: 15`, 9 steps.
- **One addition per job, a job-level `if:`** — `(github.event_name !=
'schedule' || github.event.schedule == 'ITS CRON') && (github.event_name
!= 'workflow_dispatch' || inputs.patrol == 'all' || inputs.patrol ==
'ITS JOB ID')`. A scheduled run executes exactly one job (the other
three read as skipped); a dispatch runs the one the input names; a
pull_request run exercises every job. Job ids are the retired file
stems; the job display names — the check-run names — are unchanged, so
the Checks tab reads as before.
- **Comments** — each retired file's header is carried whole as the
comment block above its job (headed `formerly .github/workflows/FILE`),
and a new file header explains the four clocks, the dispatch input, the
union filter and where the retired runs' history is. Four comment edits
beyond transport, each because the sentence described the retired
file-level shape: (watchdog) "this file carries … `paths`-filtered to
THIS FILE AND NOTHING ELSE" → the union, frozen in the pin; (watchdog)
"fires only on a pull request editing this workflow" → "… or a sibling
patrol's own script", with why that blocks nothing; (board-snapshot) the
adopt list's "this file" → "the `board-snapshot` job of it; a repo that
wants only the archiver deletes the three sibling jobs and their crons";
(release-coverage) one blank comment line between the carried "Nightly…"
and "Two readers, one fact" paragraphs.
- **History of the retired files** (hypothesis 4): GitHub keeps a
deleted workflow's runs — the Actions sidebar keeps listing each retired
workflow by name while runs of it exist, and `GET
/actions/workflows/FILE.yml` answers with `state: deleted`. New history
lands under "Fleet Patrols", one job per patrol; open a run and read its
job list, or filter the run list by job name, to follow one patrol.
### `scripts/check-required-contexts.mjs`
- `PATROL_WORKFLOW`: `'required-set-patrol.yml'` →
`'fleet-patrols.yml'`; the header sentence that names the standing
caller names the job. The wiring battery of `--self-test` now reads the
new file: no other workflow wires `--verify-required-set`, the file
declares no `merge_group`, no `REQUIRED_CONTEXTS` row names it, and the
drift annotation keys on the `✅` clean mark — all green.
`REQUIRED_CONTEXTS` itself is untouched (`ci.yml`, `lint.yml`,
`governed-surface-guard.yml` only).
### `scripts/check-platform-checklist-watchdog.mjs`
- `WORKFLOW_REL` → `.github/workflows/fleet-patrols.yml`.
- Clause 4's allowed list `PR_TRIGGER_ALLOWED_PATHS` is the six-entry
frozen union above, still compared as a SET, so widening the workflow's
filter by one path without the same reviewed edit here reds exactly as
before. The refusal text, the `--list` / OK lines and the header say so,
and a new header section ("Why the allowed list is more than one entry")
records the reason and what the clause keeps. The `GOOD` fixture and
`PR_BLOCK` carry the six entries; the widened-by-one and
swapped-for-another-file cases still fire, and one case is added — the
list minus one sibling input fires on the missing half (clause 4
battery: floor 10, 11 cases; 43 assertions across 8 batteries).
- One stale header sentence ("The watchdog carries no `pull_request:`
trigger, by clause 3") corrected to clause 4's reality — it contradicted
the clause it sat under since the 2026-09-06 ruling.
### `scripts/render-release-coverage-anchor.mjs`
- The anchor body's published preamble named
`.github/workflows/release-coverage-patrol.yml` as the writer; it names
the `release-coverage-patrol` job of `fleet-patrols.yml`. Two comments
likewise. **Declared: a file the order's surface did not name.** The
string is published to the anchor issue every night, and this rename
made it false; os-dev's rule that a change must fix the published text
it falsifies applies. Its `--self-test` pins no file name (17
assertions, green).
### `scripts/pm/board-snapshot.mjs`
- The adopt list's workflow entry:
`.github/workflows/board-snapshot.yml` → `fleet-patrols.yml` (its
`board-snapshot` job). **Declared on the same ground**: a comment-only
line, but it is the adopt instruction whose own text forbids shortening
it from memory. `--self-test` green (169 cases).
### `docs/qa/platform-checklist/README.md`,
`docs/qa/platform-checklist/RUNNER.md`
- The three lines that name the retired watchdog file name the job of
the new file; nothing else.
**Not touched, by the order's surface:** `half-state-patrol.yml` and
`governed-surface-guard.yml` (not named by the ruling);
`REQUIRED_CONTEXTS`; `lint.yml`; `scripts/pm/check-expected-skips.mjs` —
its roster names only `pr-automation.yml`, `pack-smoke-optin.yml`,
`upgrade-harness-optin.yml` and `ci.yml` rows, no patrol job, so there
is nothing to re-point (hypothesis 2 measured).
## Why
The ruling (letter A, (ii)): one file per concern, four jobs on their
existing crons, each keeping its own failure signal and minimal
permissions. This PR is the transport and nothing else — no judgement
logic, no cadence, no grant changes.
## Tests
**Transport proof** — a scratchpad script parsed `fleet-patrols.yml` and
each retired file at `git show d0a7e74:.github/workflows/FILE` with the
`yaml` package and deep-compared them: every job's `steps`,
`permissions`, `concurrency`, `env`, `name`, `runs-on` and
`timeout-minutes` equal the retired file's; `on.pull_request.paths`
equals the union of the four retired filters with each file's own path
renamed; four crons, 4 of 4; workflow-level `permissions` absent;
triggers `schedule, workflow_dispatch, pull_request`; jobs
`required-set-patrol, platform-checklist-watchdog,
release-coverage-patrol, board-snapshot`. Verdict line: `TRANSPORT CHECK
OK`.
- `node scripts/check-platform-checklist-watchdog.mjs --self-test` →
exit 0 — `OK check-platform-checklist-watchdog self-test: 43 assertions
across 8 floored batteries, every clause driven by a fixture that makes
it fire.`
- `node scripts/check-platform-checklist-watchdog.mjs` → exit 0 — `OK
.github/workflows/fleet-patrols.yml: schedule + workflow_dispatch
present, pull_request_target / merge_group absent, any pull_request
trigger paths-filtered to this file and the sibling patrol jobs' own
inputs alone, no board write reachable from a pull_request run, invoked
through pnpm check:platform-checklist.`
- **Reverse verification of the pin against the live file** (fix
committed first; restore by `git checkout HEAD -- FILE`, proven by `git
hash-object` equality and an empty `git diff HEAD`): with
`docs/qa/platform-checklist/**` injected after `scripts/invoked-as.mjs`
in `on.pull_request.paths` (on disk: 1 injected line; a parse shows 7
entries) the gate exits 1 — `… whose paths: filter also names
docs/qa/platform-checklist/**.`; restored, exit 0. A first attempt
appended the line at the end of the file, which YAML read as a seventh
step of the last job rather than a path, so the gate stayed 0: that
reading was a no-op and is discarded; the anchored injection is the
measurement.
- `pnpm check:required-contexts` → exit 0 — the self-test's wiring
battery reads `fleet-patrols.yml`: the only wirer of
`--verify-required-set`, no `merge_group`, no `REQUIRED_CONTEXTS` row
names it, the drift annotation keys on `✅`.
- `node scripts/render-release-coverage-anchor.mjs --self-test` → exit 0
(`17 assertions pass`); `node scripts/pm/board-snapshot.mjs --self-test`
→ exit 0 (`169 cases pass across 13 batteries`).
- Not owed: no package `test` / `typecheck` (the diff touches no package
source, so ① and ② are empty); `test:repo`, `pnpm lint` and the
whole-tree scans are CI's. Dogfood battery: n/a — no `!` in the title
and no `(narrowing)` arm. No ablation: no tested behaviour changes; the
workflow and the two gate scripts are proven by their self-tests and the
reverse verification above.
## Acceptance notes
- **The one behaviour change of the transport, stated:** a workflow has
one `pull_request:` trigger, so the watchdog job now also runs on pull
requests that edit a sibling patrol's script
(`scripts/check-required-contexts.mjs`,
`scripts/check-release-section-coverage.mjs`,
`scripts/pm/board-snapshot.mjs`, `scripts/pm/check-half-states.mjs`,
`scripts/invoked-as.mjs`), where before its filter named its own file
alone. Those are patrol-tooling pull requests, a few a month; the job
publishes no required context and never reds on checklist drift (it reds
only when the gate recorded no exit code), so the standing decision's
purpose — no unrelated PR blocked by checklist drift — holds. The letter
is kept by the pin: the path list is frozen as a set in
`PR_TRIGGER_ALLOWED_PATHS`. The alternative — a fifth routing job
(`dorny/paths-filter`) with `needs:` + `always()` on every patrol job —
was not taken: the ruling says four jobs, and the `if:` subtlety it adds
is the transport risk the card names.
- **Stale mentions left in place** (comment-only, outside the order's
surface; carrier: none — a docs-only sweep if the seat wants one):
`.github/workflows/lint.yml:2505` (the checklist NOTE names
`platform-checklist-watchdog.yml` as the channel),
`merged-branch-reaper.yml:238`, `shard-timings-refresh.yml:217`,
`test-nightly-tiers.yml:44`,
`scripts/check-platform-checklist.mjs:88,488`,
`scripts/check-stall-guard-budget.mjs:457` (a measured job-id-reuse
reading: `patrol` was reused across `half-state-patrol.yml` and
`release-coverage-patrol.yml`; after this PR it is not),
`scripts/ci/board-snapshot-r2-upload.sh:4`,
`scripts/import-prerequisite.mjs:235`,
`scripts/sync-release-index-currency.mjs:50`,
`scripts/pm/dispatch-gates.mjs:329,1840-1842` and
`dispatch-gates.self-test.mjs:672,1044` (measurement narratives naming
the files as measured). `docs/audits/gate-census-2026-09.md` names the
retired files with line numbers as a dated census and is left as the
reading it was. `ui/content/docs/guide/ci-cd-pipeline.md` and
`ui/scripts/dependabot-merge-gate.mjs` describe objectui's own
`board-snapshot.yml`, not this repo's.
- **`actionlint`** is not wired in this repository (no reference under
`.github/`, `package.json` or `scripts/`); the workflow was parsed with
the `yaml` package by the transport check and by the two pins.
- **Seat: claim amendment** — the file surface widens by
`scripts/render-release-coverage-anchor.mjs` and
`scripts/pm/board-snapshot.mjs` (one line each), both declared above.
## Verification
Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on head b33a799 (11 paths vs merge base
d0a7e74; 2,905 changed lines, under the 3,000 human-merge threshold):
62 families. Each ran in this worktree with the exit code captured
before any pipe (`bash -c "$cmd" > log 2>&1; st=$?`), then reconciled:
- `node scripts/pm/dispatch-gates.mjs --ran ran.list --repo
objectstack-ai/objectstack` → exit 0 — `✓ dispatch-gates --ran: 62
derived famil(ies) accounted for — 62 run, 0 NOT-MEASURED (a DERIVED
zero — all 62 recorded an exit code and none of them is 3).`
- `pnpm check:pm-dispatch-gates` → exit 0 — fast tier, 1935 cases, 352 s
on this box (`check:pm-dispatch-gates: the battery (the fast tier) took
352.0s on this box.`).
- `pnpm --filter @objectstack/lint run check:doc-formula-expressions`
first refused (exit 3: `@objectstack/formula` and `@objectstack/lint`
not built); after `pnpm exec turbo run build
--filter=@objectstack/formula --filter=@objectstack/lint` under
`scripts/pm/os-verify-lock.sh` (slot `issue-26071`: `VERDICT
command-exit 0 · held the lock 97s`) it exits 0, and the ran list
records that re-run.
- `actionlint`: not wired in this repository.
- `REQUIRED_CONTEXTS` untouched: `scripts/check-required-contexts.mjs`
still names only `ci.yml`, `lint.yml` and `governed-surface-guard.yml`.
- CI on the PR is not waited for (the order forbids it); the seat reads
the check-runs. Every reading above was taken on head b33a799; no
commit followed.
<details><summary>All 62 derived families, exit 0 each (wall seconds
shown where ≥ 10 s)</summary>
- `node scripts/check-adr-links.mjs` → exit 0
- `node scripts/check-adr-links.mjs --self-test` → exit 0
- `node scripts/check-aggregator-roster.mjs` → exit 0
- `node scripts/check-aggregator-roster.mjs --self-test` → exit 0
- `node scripts/check-ci-filter-parity.mjs` → exit 0
- `node scripts/check-closing-keyword-parity.mjs` → exit 0
- `node scripts/check-closing-keyword-parity.mjs --self-test` → exit 0
- `node scripts/check-comment-mask-corpus.mjs` → exit 0 (154 s)
- `node scripts/check-declaration-mirrors.mjs` → exit 0
- `node scripts/check-declaration-mirrors.mjs --self-test` → exit 0
- `node scripts/check-platform-checklist-watchdog.mjs` → exit 0
- `node scripts/check-platform-checklist-watchdog.mjs --self-test` →
exit 0
- `node scripts/check-release-section-coverage.mjs` → exit 0
- `node scripts/check-release-section-coverage.mjs --strict` → exit 0
- `node scripts/check-scripts-symbol-anchors.mjs` → exit 0
- `node scripts/check-scripts-symbol-anchors.mjs --self-test` → exit 0
- `node scripts/check-self-test-wired.mjs` → exit 0
- `node scripts/check-self-test-wired.mjs --self-test` → exit 0
- `node scripts/check-self-test-workflow-commands.mjs` → exit 0 (106 s)
- `node scripts/check-self-test-workflow-commands.mjs --self-test` →
exit 0
- `node scripts/check-step-collectors.mjs` → exit 0
- `node scripts/check-step-collectors.mjs --self-test` → exit 0
- `node scripts/check-whole-set-label-write.mjs` → exit 0
- `node scripts/check-whole-set-label-write.mjs --self-test` → exit 0
- `node scripts/ci/plan-test-core-shards.mjs --self-test` → exit 0
- `node scripts/docs-audit/check-drift-comment.mjs` → exit 0
- `node scripts/pm/bare-root-worklist.mjs --self-test` → exit 0 (28 s)
- `node scripts/pm/board-snapshot.mjs --self-test` → exit 0
- `node scripts/pm/ci-failure.mjs --self-test` → exit 0
- `node scripts/render-release-coverage-anchor.mjs --self-test` → exit 0
- `pnpm --filter @objectstack/lint run check:doc-formula-expressions` →
exit 0 (re-run after the formula/lint build; first run exit 3)
- `pnpm check:agent-test-spelling` → exit 0
- `pnpm check:bash32-floor` → exit 0
- `pnpm check:cli-command-ids` → exit 0 (16 s)
- `pnpm check:cross-package-test-inputs` → exit 0 (35 s)
- `pnpm check:declared-population-live` → exit 0 (34 s)
- `pnpm check:doc-authoring` → exit 0
- `pnpm check:driver-memory-census` → exit 0
- `pnpm check:entry-guard` → exit 0 (39 s)
- `pnpm check:gitlink-declared` → exit 0
- `pnpm check:node-version` → exit 0
- `pnpm check:nul-bytes` → exit 0
- `pnpm check:parse-guard` → exit 0
- `pnpm check:platform-checklist` → exit 0 (28 s)
- `pnpm check:pm-board-snapshot` → exit 0
- `pnpm check:pm-dispatch-gates` → exit 0 (354 s)
- `pnpm check:pm-expected-skips` → exit 0
- `pnpm check:pm-governed-merges` → exit 0
- `pnpm check:pm-post-stamped` → exit 0
- `pnpm check:pm-record-recognisers` → exit 0
- `pnpm check:pnpm-acquisition` → exit 0
- `pnpm check:pnpm-filter-targets` → exit 0
- `pnpm check:pnpm-flag-spellings` → exit 0
- `pnpm check:ratchet-remedy-authority` → exit 0
- `pnpm check:refd-timer-probe` → exit 0 (21 s)
- `pnpm check:required-contexts` → exit 0
- `pnpm check:shard-attestation` → exit 0
- `pnpm check:stall-guard-budget` → exit 0
- `pnpm check:stall-guard-headroom` → exit 0
- `pnpm check:watch-hint-literal` → exit 0
- `pnpm check:workflow-status-functions` → exit 0
- `pnpm check:workflow-step-name-quoting` → exit 0
</details>
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbW8nFvKeVjgKv8RTwH)_
Co-authored-by: Claude <noreply@anthropic.com>1 parent c3fb185 commit 3fcafcd
11 files changed
Lines changed: 1499 additions & 1406 deletions
File tree
- .github/workflows
- docs/qa/platform-checklist
- scripts
- pm
This file was deleted.
Large diffs are not rendered by default.
This file was deleted.
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
211 | 211 | | |
212 | 212 | | |
213 | 213 | | |
214 | | - | |
| 214 | + | |
215 | 215 | | |
216 | 216 | | |
217 | 217 | | |
| |||
433 | 433 | | |
434 | 434 | | |
435 | 435 | | |
436 | | - | |
| 436 | + | |
437 | 437 | | |
438 | 438 | | |
439 | 439 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
14 | 14 | | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
| 17 | + | |
18 | 18 | | |
19 | 19 | | |
20 | 20 | | |
| |||
0 commit comments