Repository navigation
ci: fold the four daily patrols into one fleet-patrols.yml workflow (1 of 3) - #26512
Conversation
required-set-patrol, platform-checklist-watchdog, release-coverage-patrol and board-snapshot become four jobs of .github/workflows/fleet-patrols.yml. Each job keeps the retired file's steps, permissions, concurrency group and env byte-for-byte; a job-level `if:` gates it to its own cron (github.event.schedule), workflow_dispatch takes a `patrol` input to fire one job by hand, and the pull_request trigger is the union of the four retired filters. The two gates that read those files by name follow the rename: check-required-contexts' PATROL_WORKFLOW and the platform-checklist watchdog pin, whose allowed pull_request path list is now the frozen union (its self-test drives the widened and the narrowed shapes). The anchor renderer's published preamble, the archiver's adopt list and the two platform-checklist docs name the new file. Claude-Session: https://claude.ai/code/session_01JQwxbW8nFvKeVjgKv8RTwH Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewReviewed at 2026-10-27T06:22Z · PR #26512 · card #26071, ruling A step (ii) · independent contract-review subagent, adversarial by design (not the dispatch order's, the seat's or the dev's conclusions). Served-tier: ① Derived judgments(a) Transport, per job — every difference named. The PR's base in the object is 73683ee; its merge-base with main is d0a7e74, and the four retired files are byte-identical at d0a7e74 and at origin/main 14f3099, so the compare is against what main holds. For all four pairs (
(b) (c) (d) The rest of the diff. (e) Gates that read workflow files by name — lane and verdict on the head. (f) The PR body against the files. Line 1 the harness attribution comment, line 2 the Requested-by line, line 3 (g) Report 6435073406 and the claim. Check-runs on b33a799 read twice: at 2026-10-27T06:19Z 66 check-runs, none failed, two still in progress ( ② Semver levelNo published package is touched ( ③ Boundary flags
Implemented-by: VERDICT: PASS |
维护者速读改了什么:按你对 #26071 的裁决 A 第 (ii) 步,四个日巡逻 workflow( 席位核对ACCEPT ·
Generated by Claude Code |
Requested by Warren · project thread
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) thelint.ymlschedule skip. The card closes with the last one. Head branchclaude/project-thread-31axoxis the card thread's harness push branch (the measured platform exception toclaude/issue-N-slug; the next two PRs use-2and-3).维护者速读
.github/workflows/fleet-patrols.yml,四个 job 各守自己原来的 cron;每个 job 的 steps、permissions、concurrency、env 与原文件解析后逐项相同(脚本比对,见 Tests)。读这些文件名的两个门禁(check-required-contexts的PATROL_WORKFLOW、watchdog 静态 pin 的文件名与pull_request路径白名单)、锚点渲染器发布的文案、归档器的 adopt 清单、两份 checklist 文档改指新文件。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:(各 jobname:下方三行)与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。.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— onepatrolchoice input (allby 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 apull_requestevent (every board write keeps itsgithub.event_name != 'pull_request'guard, which the watchdog pin judges across every job of the file).permissions:,concurrency:orenv:. Each job carries the retired file's blocks at job level, unchanged:required-set-patrol(display nameLive required-set sweep, was jobsweep):permissions: {contents: read}, concurrency grouprequired-set-patrol-${{ github.event.pull_request.number || github.ref }},timeout-minutes: 10, 5 steps.platform-checklist-watchdog(Platform checklist gate sweep, waswatchdog):{contents: read, issues: write}, groupplatform-checklist-watchdog,envANCHOR_IDENTITY_LABEL / ANCHOR_TITLE / ANCHOR_MARKER / ANCHOR_ROUTING_LABELS / PATROL_SCHEDULE,timeout-minutes: 15, 5 steps.release-coverage-patrol(Release section-coverage sweep, waspatrol):{contents: read, issues: write}, grouprelease-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, wasarchive):{contents: write, issues: read, pull-requests: read}, groupboard-snapshot,timeout-minutes: 15, 9 steps.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.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" → "theboard-snapshotjob 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.GET /actions/workflows/FILE.ymlanswers withstate: 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.mjsPATROL_WORKFLOW:'required-set-patrol.yml'→'fleet-patrols.yml'; the header sentence that names the standing caller names the job. The wiring battery of--self-testnow reads the new file: no other workflow wires--verify-required-set, the file declares nomerge_group, noREQUIRED_CONTEXTSrow names it, and the drift annotation keys on the✅clean mark — all green.REQUIRED_CONTEXTSitself is untouched (ci.yml,lint.yml,governed-surface-guard.ymlonly).scripts/check-platform-checklist-watchdog.mjsWORKFLOW_REL→.github/workflows/fleet-patrols.yml.PR_TRIGGER_ALLOWED_PATHSis 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. TheGOODfixture andPR_BLOCKcarry 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).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.github/workflows/release-coverage-patrol.ymlas the writer; it names therelease-coverage-patroljob offleet-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-testpins no file name (17 assertions, green).scripts/pm/board-snapshot.mjs.github/workflows/board-snapshot.yml→fleet-patrols.yml(itsboard-snapshotjob). Declared on the same ground: a comment-only line, but it is the adopt instruction whose own text forbids shortening it from memory.--self-testgreen (169 cases).docs/qa/platform-checklist/README.md,docs/qa/platform-checklist/RUNNER.mdNot touched, by the order's surface:
half-state-patrol.ymlandgoverned-surface-guard.yml(not named by the ruling);REQUIRED_CONTEXTS;lint.yml;scripts/pm/check-expected-skips.mjs— its roster names onlypr-automation.yml,pack-smoke-optin.yml,upgrade-harness-optin.ymlandci.ymlrows, 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.ymland each retired file atgit show d0a7e748:.github/workflows/FILEwith theyamlpackage and deep-compared them: every job'ssteps,permissions,concurrency,env,name,runs-onandtimeout-minutesequal the retired file's;on.pull_request.pathsequals the union of the four retired filters with each file's own path renamed; four crons, 4 of 4; workflow-levelpermissionsabsent; triggersschedule, workflow_dispatch, pull_request; jobsrequired-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.git checkout HEAD -- FILE, proven bygit hash-objectequality and an emptygit diff HEAD): withdocs/qa/platform-checklist/**injected afterscripts/invoked-as.mjsinon.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 readsfleet-patrols.yml: the only wirer of--verify-required-set, nomerge_group, noREQUIRED_CONTEXTSrow 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).test/typecheck(the diff touches no package source, so ① and ② are empty);test:repo,pnpm lintand 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
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 inPR_TRIGGER_ALLOWED_PATHS. The alternative — a fifth routing job (dorny/paths-filter) withneeds:+always()on every patrol job — was not taken: the ruling says four jobs, and theif:subtlety it adds is the transport risk the card names..github/workflows/lint.yml:2505(the checklist NOTE namesplatform-checklist-watchdog.ymlas 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:patrolwas reused acrosshalf-state-patrol.ymlandrelease-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-1842anddispatch-gates.self-test.mjs:672,1044(measurement narratives naming the files as measured).docs/audits/gate-census-2026-09.mdnames 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.mdandui/scripts/dependabot-merge-gate.mjsdescribe objectui's ownboard-snapshot.yml, not this repo's.actionlintis not wired in this repository (no reference under.github/,package.jsonorscripts/); the workflow was parsed with theyamlpackage by the transport check and by the two pins.scripts/render-release-coverage-anchor.mjsandscripts/pm/board-snapshot.mjs(one line each), both declared above.Verification
Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackon 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-expressionsfirst refused (exit 3:@objectstack/formulaand@objectstack/lintnot built); afterpnpm exec turbo run build --filter=@objectstack/formula --filter=@objectstack/lintunderscripts/pm/os-verify-lock.sh(slotissue-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_CONTEXTSuntouched:scripts/check-required-contexts.mjsstill names onlyci.yml,lint.ymlandgoverned-surface-guard.yml.All 62 derived families, exit 0 each (wall seconds shown where ≥ 10 s)
node scripts/check-adr-links.mjs→ exit 0node scripts/check-adr-links.mjs --self-test→ exit 0node scripts/check-aggregator-roster.mjs→ exit 0node scripts/check-aggregator-roster.mjs --self-test→ exit 0node scripts/check-ci-filter-parity.mjs→ exit 0node scripts/check-closing-keyword-parity.mjs→ exit 0node scripts/check-closing-keyword-parity.mjs --self-test→ exit 0node scripts/check-comment-mask-corpus.mjs→ exit 0 (154 s)node scripts/check-declaration-mirrors.mjs→ exit 0node scripts/check-declaration-mirrors.mjs --self-test→ exit 0node scripts/check-platform-checklist-watchdog.mjs→ exit 0node scripts/check-platform-checklist-watchdog.mjs --self-test→ exit 0node scripts/check-release-section-coverage.mjs→ exit 0node scripts/check-release-section-coverage.mjs --strict→ exit 0node scripts/check-scripts-symbol-anchors.mjs→ exit 0node scripts/check-scripts-symbol-anchors.mjs --self-test→ exit 0node scripts/check-self-test-wired.mjs→ exit 0node scripts/check-self-test-wired.mjs --self-test→ exit 0node scripts/check-self-test-workflow-commands.mjs→ exit 0 (106 s)node scripts/check-self-test-workflow-commands.mjs --self-test→ exit 0node scripts/check-step-collectors.mjs→ exit 0node scripts/check-step-collectors.mjs --self-test→ exit 0node scripts/check-whole-set-label-write.mjs→ exit 0node scripts/check-whole-set-label-write.mjs --self-test→ exit 0node scripts/ci/plan-test-core-shards.mjs --self-test→ exit 0node scripts/docs-audit/check-drift-comment.mjs→ exit 0node scripts/pm/bare-root-worklist.mjs --self-test→ exit 0 (28 s)node scripts/pm/board-snapshot.mjs --self-test→ exit 0node scripts/pm/ci-failure.mjs --self-test→ exit 0node scripts/render-release-coverage-anchor.mjs --self-test→ exit 0pnpm --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 0pnpm check:bash32-floor→ exit 0pnpm 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 0pnpm check:driver-memory-census→ exit 0pnpm check:entry-guard→ exit 0 (39 s)pnpm check:gitlink-declared→ exit 0pnpm check:node-version→ exit 0pnpm check:nul-bytes→ exit 0pnpm check:parse-guard→ exit 0pnpm check:platform-checklist→ exit 0 (28 s)pnpm check:pm-board-snapshot→ exit 0pnpm check:pm-dispatch-gates→ exit 0 (354 s)pnpm check:pm-expected-skips→ exit 0pnpm check:pm-governed-merges→ exit 0pnpm check:pm-post-stamped→ exit 0pnpm check:pm-record-recognisers→ exit 0pnpm check:pnpm-acquisition→ exit 0pnpm check:pnpm-filter-targets→ exit 0pnpm check:pnpm-flag-spellings→ exit 0pnpm check:ratchet-remedy-authority→ exit 0pnpm check:refd-timer-probe→ exit 0 (21 s)pnpm check:required-contexts→ exit 0pnpm check:shard-attestation→ exit 0pnpm check:stall-guard-budget→ exit 0pnpm check:stall-guard-headroom→ exit 0pnpm check:watch-hint-literal→ exit 0pnpm check:workflow-status-functions→ exit 0pnpm check:workflow-step-name-quoting→ exit 0Generated by Claude Code