Repository navigation
ci: two-speed CI — PR and queue runs carry the fast set, the full battery runs on main and gates the publish lane, required set 7 → 5 - #23020
Merged
Conversation
…ttery only `Temporal Conformance (live PG + MySQL)`, the three `Dogfood Regression Gate` shards, `Dogfood Verify CLI` and `Console Pin Gate` gain one event clause in their `if:` (`github.event_name != 'pull_request' && github.event_name != 'merge_group'`), so a PR or queue build carries Build Core, Test Core and Build Docs, while main's push, hourly schedule and dispatch runs carry the full battery. The `on:` block is unchanged; `dogfood-gate` keeps `if: always()` and counts zero expected attestations when both legs skip. check-expected-skips' roster reasons for the four rows now say why they skip on every PR head (the event), instead of promising a queue-build run. Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB Co-authored-by: Claude <noreply@anthropic.com>
…val prompt exists A new `full-battery` job, needed by `publish` (whose `if:` opens with `success()`), runs `scripts/ci/scheduled-full-run.mjs --release-gate`: per workflow (`CI`, `Lint & Type Check`), the newest completed `schedule` or `push` run on main whose head is the version commit or a first-parent ancestor of it must have concluded `success`. A `cancelled` run carries no verdict and is passed over; the nearest commit with a verdict decides, and on it the newest run. A refusal skips `publish`, so no deployment is queued at the `release` environment for a version whose battery is red. The rule lives in scheduled-full-run.mjs as its third half, so the --self-test lint.yml already runs pins it: three new batteries over a recorded 2026-10-12 run listing (scripts/fixtures/release-full-battery/), synthetic runs, and the release.yml wiring (the needs edge, the invocation, publish's `success()`, no continue-on-error), each wiring case driven red. Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB Co-authored-by: Claude <noreply@anthropic.com>
…, timing gates and test budgets are rules AGENTS.md: the merge-queue paragraph names the five required contexts (`Lint & Repo Gates`, `TypeScript Type Check`, `Test Core`, `Build Core`, `Governed Surface Queue Guard`); the two-speed doctrine and the main-red rule sit beside it; the timing-gate and test-budget rules join the clocked-windows rule in Build & Test; Post-Task item 4 says the full battery's Console Pin Gate makes a sibling-breaking removal a main-red, no longer a red on every PR. Provenance: ruling record 6120820653. Net +7 lines, exactly the file's headroom under its 1116 ceiling, paid by the two dropped names and item 4's rationale clause. check-required-contexts: the `Dogfood Regression Gate` and `Temporal Conformance (live PG + MySQL)` rows leave REQUIRED_CONTEXTS with a prose tombstone (neither name is dead), the two set-stating surfaces' mustName arrays follow, the self-test fixtures that mutated those jobs re-point to still-registered ones, the 2026-08-18 snapshot's direction-B reading is derived instead of asserted empty, and the direction-B report names the tombstone case so nobody re-adds a row the two-step dropped on purpose. platform-readings.md's required-set line follows (seven to five). Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB Co-authored-by: Claude <noreply@anthropic.com>
…eption landing-operations.md gains main-red convention 3 (a full-battery red is the owning lane's first work; while it stands, no PR of that lane arms the queue), paid by deleting the third pointer to contract-review.md that line 12 already carries. triage-duties.md gains exception 2 to the human floor (any seat flips a gate that reds untouched packages twice in an hour to report-only, the ruling following; the sole-exception wording becomes exception 1), paid by deleting the Bug-type restatement of line 33's pm:queue admission rule. Both files stay at their ceilings. Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB Co-authored-by: Claude <noreply@anthropic.com>
ci.yml's moved jobs now spell `&& github.event_name != 'pull_request' && github.event_name != 'merge_group'` after their filter output, and the strict `if:` whitelist refused the whole expression: a `.objectui-sha` card derived no console gates and a dogfood card no dogfood suite, exactly when a PR no longer runs those jobs and this derivation is the only place the dev hears of them before a main-red. The clause is stripped like `!cancelled()` — exactly as spelled, only as the last conjunct; reordered, partial, positive or leading spellings still refuse (five new cases). The marker census follows check-expected-skips' header growing two lines. Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB Co-authored-by: Claude <noreply@anthropic.com>
This was referenced Oct 12, 2026
os-zhuang
approved these changes
Oct 12, 2026
os-zhuang
marked this pull request as ready for review
October 12, 2026 11:10
os-zhuang
enabled auto-merge
October 12, 2026 11:10
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #22998
Clause-②: no
Two-speed CI: a PR or queue build carries the fast set,
main's push, hourly and dispatch runs carry the full battery, and the publish lane reads the full battery before the release approval exists. This PR implements items 1, 2, 3 and 5 of the card. Item 4, the ruleset change, is the maintainer's act; its exact before and after are below. The maintainer's words are in the card body and in ruling record 6120820653 on #22991.Every governed path here is Tier H (
AGENTS.md), so this PR stays a draft until aGOVERNED_APPROVERSmember approves it. No seat flips it ready, queues it or arms auto-merge.Item 4: the maintainer's settings act (no seat can do this)
Where: Settings, then Rules, then Rulesets, then the
mainruleset (id 12119582), then "Require status checks to pass", then the list of required status checks. The merge queue enforces the same list.Lint & Repo Gates·TypeScript Type Check·Test Core·Dogfood Regression Gate·Build Core·Temporal Conformance (live PG + MySQL)·Governed Surface Queue GuardLint & Repo Gates·TypeScript Type Check·Test Core·Build Core·Governed Surface Queue GuardRemove exactly two checks:
Dogfood Regression GateandTemporal Conformance (live PG + MySQL). Do it in the same sitting as the merge. This follows the two-step thatscripts/check-required-contexts.mjsdocuments: this PR drops the twoREQUIRED_CONTEXTSrows, and the ruleset follows. Until the ruleset changes,check-required-contexts --verify-required-setlists both checks in direction B (required, but pinned by no row). That report is report-only. This PR also changes the direction-B text so that a dropped row recorded in the TOMBSTONE is answered by the settings change, never by adding the row back.Why the order does not risk a stuck queue (this refutes part of PM hypothesis H1, which said "a skipped required job counts as a pass only for a context that is no longer required"):
Dogfood Regression Gate: the aggregator job keepsif: always(). On a PR or queue run, both of its legs skip, and the aggregator counts zero expected attestations and concludes success. Measured locally:check-shard-attestation.mjs --verifywith--leg dogfood/3:skipped --leg dogfood-verify/1:skippedand a successful filter prints "satisfied — no shard was expected to run, and none claimed to", exit 0.Temporal Conformance (live PG + MySQL): on a PR or queue run, its job-levelif:is false, so the check run concludesskipped. Branch protection countsskippedas passing. The repo's own record says so (the "Whyif:is deliberately NOT asserted" section ofcheck-required-contexts.mjs), and so does GitHub's documentation for a job skipped by its own condition.What changed (five commits plus a merge of
main; read them one at a time)ab6fc3e5ci.yml. Four jobs gain one event clause in theirif::temporal-conformance,dogfood(three shards),dogfood-verifyandconsole-pin. The clause isgithub.event_name != 'pull_request' && github.event_name != 'merge_group'. Theon:block is unchanged; it already hadpush: main, an hourlyscheduleandworkflow_dispatch. A PR or queue run now carriesBuild Core,Test Core(affected-only, as before) andBuild Docs. A new comment block abovejobs:, "THE TWO SPEEDS", explains the split. Comments that called these contexts "required", or said they run "before it enqueues", now say what is true.check-expected-skips' roster reasons for the four rows now say why they skip on every PR (the event), instead of promising a run in the queue build.eba3a868release.yml and its rule. A newfull-batteryjob runsscripts/ci/scheduled-full-run.mjs --release-gate --release-commit VERSION_COMMIT.publishnow needs it (needs: [release-integrity, full-battery]), andpublish'sif:opens withsuccess(), so a refusal skipspublishand no deployment is queued at thereleaseenvironment. The gate job runs under exactly the same condition aspublish. It hasactions: readand checks out withfetch-depth: 0. The rule, pinned in the script's new HALF 3, applies toCIand toLint & Type Check:scheduleorpushruns onmainthat arecompletedand whose head is the version commit or one of its first-parent ancestors.cancelledrun carries no verdict and is skipped.run_started_at, which a re-run updates).success. Any other conclusion, or no candidate at all, refuses. An unreadable API or a shallow clone refuses with exit 3.430d04b0AGENTS.md and the pins that state the required set:Console Pin Gate(no longer a red on every PR).check-required-contexts.mjsdrops the two rows and records them in a prose TOMBSTONE; neither name is dead. The two set-statingmustNamearrays follow. Self-test fixtures that changed those jobs now change jobs that are still registered. The direction-B reading of the 2026-08-18 snapshot is derived from the registry instead of asserted empty.platform-readings.md's required-set line goes from seven names to five.5354b523The two pm-dispatch references. Each gains one line, and each pays for it by deleting one line:landing-operations.mdgains main-red convention 3: a full-battery red is the owning lane's first work, and while it stands that lane's PRs do not arm the queue. It pays by deleting line 19, the third pointer tocontract-review.md; line 12 already points there for the same subject.triage-duties.mdgains exception 2 to the human floor: any seat flips a gate that reds untouched packages twice in an hour to report-only, and the ruling follows. The old "sole exception" wording becomes exception 1. It pays by deleting theBug-type line, which only restates line 33'spm:queueadmission rule for one type.476361e4dispatch-gates. The tool's strictif:whitelist refused the new clause. As a result, a.objectui-shacard derived no console gates and a dogfood card derived no dogfood suite, at the moment a PR stopped running those jobs. The clause is now stripped like!cancelled(), but only when spelled exactly and only as the last condition. Five new cases show that a reordered, partial, positive or leading spelling still refuses. The marker census followscheck-expected-skips' header, which grew by two lines.Line ceilings:
AGENTS.mdgrows 1109 to 1116, exactly its ceiling of 1116 (+7; the space comes from the two dropped names and from item 4's rationale clause).landing-operations.mdstays 101/101,triage-duties.md120/120 andplatform-readings.md460/460. No ceiling was raised.PM mechanism hypotheses, as measured
on:block already hadschedule(hourly,'0 * * * *') andpush: main; it is unchanged. Theneeds:chains and thefilterjob are unchanged (check-aggregator-rostergreen).if: always()instead of taking the event clause.check:shard-attestationpinsif: always()on every attestation-counting gate, and keeping it makes the transition above safe under any reading of how a skipped job counts.workflow_dispatchalso runs the full battery. ci.yml calls dispatch "the same full battery, on demand". The card's acceptance dry run "on a branch" also needs it:pushis filtered tomainandscheduleruns only on the default branch.scripts/ci/scheduled-full-run.mjs(HALF 3), not in a new small file.check-self-test-wiredrequires that any script a workflow names that ships a--self-testhas that self-test run by CI. Every self-test runs inlint.yml, which this PR must not touch (ci(lint): measure whereLint & Repo Gates' 198 serial steps spend 14–37 minutes, and bring the maintainer a split plan (maintainer-directed) #22991). The--self-testofscheduled-full-run.mjsalready runs there. The new part of the rule is skippingcancelledruns. In the 2026-10-12 recording, 62 of 100CIpush runs and 81 of 100Lint & Type Checkpush runs were cancelled, so counting them would refuse almost every release.check-required-contexts(registry, surfaces, fixtures),check-expected-skips(reasons) anddispatch-gates(recognizer and census).check-ci-filter-parity,scheduled-full-run --self-test,check-harness-current --self-testandrelease-verify-npm --self-teststayed green with no edit. No gate was loosened.The release gate: proof without a release
A seat never runs the release workflow (Prime Directive #15), so the live path in
release.ymlis first verified by the maintainer's next release. Up to that point:scripts/fixtures/release-full-battery/runs-2026-10-12.jsonholds real runs read from the Actions jobs API and trimmed, plus 25 first-parent shas. With the release commit at0524c1619b, the gate passes: CI on run 38222448090 (schedule, at712361b7a8, newer than the push success on the same commit), Lint on run 38223935346. A commit two back holds only cancelled runs, and it is skipped. With the release commit at19805d8956, the gate refuses CI alone, naming run 38217670691 (the 07:25 hourly run failed and its own push run was cancelled). Lint's nearest verdict there is green. Runs on commits after the release commit are never chosen.merge_group,pull_requestandworkflow_dispatchruns are ignored. Runs not onmain, unfinished runs, and another workflow's runs are ignored.startup_failurerefuses. An empty history refuses. Either workflow red refuses, and only that workflow is named.--release-gate,publishneeds it,publish'sif:opens withsuccess(), and the gate job has nocontinue-on-error. Each of these was changed in a copy and observed red.scripts/ablation-replace.mjswith the anchor confirmed hit (1 to 0) and restored to the HEAD blobbc87f666f6d2(git diff HEADempty):cancelledfilter turned the self-test red: "0524c1619b should pass, refused: CI: ... concluded cancelled -- run 38224326416".48cca4ae00. Result: PASS. CI used push run 38224556104 at6a40a09ed9. Lint used schedule run 38223935346 at712361b7a8, 6 commits back, because the nearer push runs were cancelled. The CLI itself refuses a shallow clone (exit 3) and a missing token (exit 3), as the release job's checkout requires.Gates, final union at
f680be36These ran after the last commit and the merge of
origin/main.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 70 commands from this diff (12 paths, three-dot against794177d9b).--ranreconciliation with every exit code recorded reported: "Run reconciliation — 70 derived, 70 run, 0 NOT-MEASURED, 0 UNRUN", "a DERIVED zero — all 70 recorded an exit code and none of them is 3".pnpm check:pm-dispatch-gates(fast tier): 1833 cases pass. The full self-test (dispatch-gates.mjs --self-test, fast and slow tiers) reported "✓ dispatch-gates self-test: 2093 cases pass" (fast 1833 in 258.1s, slow 260 in 768.1s).dist/(check:dts-closure,check:dual-build-cjs-loads,check:lean-entry-closure,check:sourcemap-no-sources-content) ran over a fresh build of the merged tree:turbo run build --filter=!@objectstack/docs, 73/73 tasks, underos-verify-lock, "VERDICT command-exit 0".check:console-shaandcheck:console-injection. Both print "No console dist at packages/console/dist — skipping"; building that dist needs the objectui build. Thecheck:console-injectionself-test did run (67 assertions). On this PR, CI also skipsConsole Pin Gatebecause of its new full-battery clause, so thisci.ymlfirst meets the console pin onmain's push run after the merge.476361e4, the fast tier was red: 8 of 1828 cases failed. Four were live console-job cases, three were live dogfood-job cases, and one was a marker-census line number. All eight came from this PR'sci.ymlandcheck-expected-skipsedits and are fixed in476361e4(see commit 5 above).ci.ymlandrelease.ymlparse with the repository'syamlpackage.check-governed-merges.mjs --branch claude/issue-22998-two-speed-ciat the final head: "governed-surface predicate: 4 of 12 path(s) hit the register (6 surfaces, repo-agnostic). ⛔ GOVERNED — a human merge is the review record for this PR (governance: human merge IS the review record for governed surfaces — retire the per-PR ADR approval check, add a report-only post-merge audit #9495 regime). ⚖️ landing tier: H(人合) — the maintainer's hand, or an authorized APPROVED review (GOVERNED_APPROVERS) and then the owning seat lands it." Also: "size: 1310 changed line(s) (+1207 / -103) ≤ 3000 — under the human-merge threshold".scripts/,.claude/**andAGENTS.md, and no package'sfiles[]ships any of them. So the PR carriesskip-changeset.Open question (for the lane, not decided here)
The release gate reads
scheduleorpushruns, as ruled. A push run'sTest Coreis affected-only (#16467), so the nearest verdict can be a narrow push success sitting on top of an older red hourly run that nothing fixed. Options:scheduleruns only. This is the real full battery, but the deciding run can then be up to an hour before the version commit.Recommendation: A, measured after the first release.
Acceptance notes
.objectui-shaPR no longer runsConsole Pin Gatebefore it lands; this PR's ownci.ymlchange is one such case. dispatch-gates again derivescheck:console-shaandcheck:console-injectionfor such a card, but both skip without a built console (packages/console/dist). In practice a pin bump is first measured onmain, and the main-red rule then applies.workflow_dispatchruns of CI are full battery, but the release gate does not count them, because the ruled text saysscheduleorpush.publish-smoke.ymlsays branch protection requires "TypeScript Type Check, Build Core, Test Core and Dogfood Regression Gate". That was already incomplete before this PR (no owner for the fix).check-aggregator-rosterstill callsdogfood-gatea "required aggregator". Its roster discipline still holds and is stricter than needed.维护者速读(草稿)
改了什么
PR 与合并队列只跑快速集(Build Core、Test Core 受影响包、Build Docs 与 lint 门禁);Dogfood、Temporal Conformance、Console Pin Gate 改为只在 main 的 push、每小时定时与手动触发运行(全量电池)。发版流水线在等你点"批准"之前,先读 main 上版本提交及其之前最新一次全量电池结果,任一工作流不绿就不排发布审批。AGENTS.md 与两份 PM 细则写入两速、main 红即停车道、计时门禁只报不拦、测试只设预算不删除等规则。
为什么改
你的原则是"能让整个车队提速的就是好方案,发版前测好即可"。实测一次入队约 122 个 runner 分钟、14–36 分钟;把最慢的几条腿移出每个 PR,预计 PR 反馈约 5–6 分钟、入队约 8 分钟,测试强度不降,只是挪到 main 与发版前把关。
风险与代价(含回滚)
席位意见
你要做的
main的必需检查里删除Dogfood Regression Gate与Temporal Conformance (live PG + MySQL)两项(删前 7 项,删后 5 项,见上表)。Generated by Claude Code