Repository navigation
ci(lint): Lint & Repo Gates becomes an aggregate over eight parallel lanes; PM dispatch-gates runs its fast tier on PR and queue (#22991) - #23014
Merged
Conversation
…d merge_group, both tiers on every other CI event The tier now follows GITHUB_EVENT_NAME under GITHUB_ACTIONS: the PR and merge-queue path gets --self-test --fast; schedule, push to main, workflow_dispatch and any unlisted event keep both tiers. --slow stays the manual override everywhere. The self-test drives each event and blanks the runner's own GITHUB_EVENT_NAME so it cannot leak into the cases. Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB Co-authored-by: Claude <noreply@anthropic.com>
…ng the list reds it Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB Co-authored-by: Claude <noreply@anthropic.com>
…lanes The single 193-step job is split into eight Lint · * lanes behind an aggregator that keeps the required context name byte-identical, with the TypeScript Type Check roster discipline: OS_AGGREGATOR_MEMBERS == needs, re-checked against toJSON(needs) at run time, if: always(). Every gate step moves verbatim (same run, same if, same comment block, same relative order). Each lane repeats the six setup steps, the gate-family selector where it carries a scoped step, and the unmeasured-tail reporter pair. Lane walls follow the Type Check lanes' checkout-budget-plus-body scheme. Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB Co-authored-by: Claude <noreply@anthropic.com>
check-aggregator-roster registers lint / Lint & Repo Gates as a required aggregate, adds it to both positive controls, and anchors the typecheck refusal fixtures on typecheck's own roster (an unanchored pattern hit the first declaration in lint.yml, which is now lint's). check-required-contexts holds the pin to a job the Lint & Repo Gates context carries: lint itself, or a lane it both declares and needs, with no if: at step or lane level; fixtures pin declared-only, needed-only and the lane-level if: read. Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB Co-authored-by: Claude <noreply@anthropic.com>
…he per-lane order rule
Positional references ("the step above") whose referent moved to another
lane now name the step and its lane; the GATE ORDER note, the selector and
the PM dispatch-gates step say what the split changed for them.
Claude-Session: https://claude.ai/code/session_01LqNgdPZB2Jnr5HpGbaqLeB
Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang
approved these changes
Oct 12, 2026
os-zhuang
marked this pull request as ready for review
October 12, 2026 11:06
This was referenced Oct 12, 2026
This was referenced Oct 12, 2026
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 #22991
Clause-②: no
Executes ruling 6120820653 (letter D) on the card:
Lint & Repo Gatesbecomes an aggregator over eight parallelLint · *lanes, andpnpm check:pm-dispatch-gatesruns its fast tier onpull_request/merge_groupwhilescheduleandpushkeep both tiers.What changed
.github/workflows/lint.yml: the 193-steplintjob is split. Job idlintandname: Lint & Repo Gatesare kept byte-identical (the required context is matched by check-run name). The job now runs one step: a verdict overtoJSON(needs), copied from thetypecheckaggregate. It carriesOS_AGGREGATOR_MEMBERS(equal toneeds:),if: always()andtimeout-minutes: 10. Every gate step moved verbatim into one of eight lanes: samerun:, sameif:, same comment block, same relative order. Each lane repeats the six setup steps, the gate-family selector when it carries a scoped step, and the unmeasured-tail reporter pair. Lane walls use the Type Check lanes' scheme: a checkout budget of 20 on the checkout step plus a body budget (2× the measured body maximum, 5-minute grain, floor 10). That gives 30 for seven lanes and 50 for the PM lane, whose body budget still has to cover both tiers onschedule/push.scripts/pm/check-dispatch-gates.mjs: underGITHUB_ACTIONS=true,GITHUB_EVENT_NAMEdecides the tier.pull_requestandmerge_groupget--self-test --fast. Every other event gets both tiers:schedule,push,workflow_dispatch, and any trigger nobody listed.--slowstays the manual override everywhere, and the chosen tier and its reason are printed before the spawn. Its own self-test covers each event. It also blanks the runner's ownGITHUB_EVENT_NAME, which would otherwise leak into the cases on CI.scripts/check-aggregator-roster.mjs:lint/Lint & Repo Gatesis registered as a required aggregate, so deleting its roster is a refusal. It is added to both per-aggregator positive controls. The twotypecheckrefusal fixtures are anchored on typecheck's own roster: the unanchored pattern hit the firstOS_AGGREGATOR_MEMBERSin lint.yml, which is nowlint's, and that is how the gate first spoke on this diff.scripts/check-required-contexts.mjs: the pin must now run in a job theLint & Repo Gatescontext carries. That islintitself, or a lane it both declares and lists inneeds:. The step may not carry anif:, and neither may the lane hosting it. New fixtures cover: declared and needed counts as wired; declared-only does not; needed-only does not; a lane-levelif:is seen.Lanes (predicted)
How the prediction was made: each lane's wall is rebuilt per run from the per-step timestamps of the 70 successful
Lint & Repo Gatesruns, 2026-10-12T04:40Z to 08:45Z (pull_requestandmerge_group, jobs API). Each rebuild uses that run's own setup steps plus the lane's steps as they ran there. The measured walls of this PR's own CI run go into theos-dev-reportcomment on the card. This body is written once, before that run exists.lint-eslintlint-repo-toolinglint-docs-codelint-ci-enginelint-scoped-erasurelint-scoped-slot-corpuslint-scoped-toolinglint-pm-dispatch-gatesCritical path. Before: p50 17.8, p90 35.9, max 38.5 minutes. After (predicted): p50 4.2, p90 4.7, max 5.1 minutes on
pull_request/merge_group, plus the aggregate's own seconds. Onschedule/pushthe PM lane runs both tiers, about 15–16 minutes.How the cut differs from the PM's H2:
query-options erasure(160 s) andslot-lookup(84 s) run together in 53 of 70 runs. Together they would put one lane over budget at median, so they sit in different lanes.Mechanism hypotheses, checked
steps.gate-families.outputs.*: nineif:lines and one stepid:. Norun:in the job builds, writesGITHUB_PATH, or readsRUNNER_TEMPacross steps. The oneGITHUB_ENVwrite (STORE_PATH) is a setup step every lane repeats. Of the 196 scripts the job invokes, I scanned every write call site that has no temp-dir use. None writes a repo file that a later step reads; they are ledger--writemodes or fixture strings.pull_request,merge_group) rather than "fast unlessscheduleorpush". The ruling moved the slow tier off those two events only, soworkflow_dispatchkeeps both tiers. An event nobody listed also lands on the full battery rather than the shorter one.familiesjob would put a second full-history checkout and a runner pickup on the critical path of every scoped lane, and it would change all nineif:spellings, whichcheck:select-gate-familiespins.check-aggregator-roster --self-testandcheck:required-contexts. Both are updated, and neither is loosened (see "What changed"). The other five pins named in H6 stayed green unchanged.Proof the move is pure
A one-off verifier parsed the old and the new lint.yml (the
yamlpackage; PyYAML agrees on the result). It checked:timeout-minutes: 20;name,onandconcurrency, are unchanged.Result: 0 problems. Comment edits only name the step a moved neighbour referred to ("the step above" became the step's name and its lane). They also note the per-lane order rule, the per-lane selector and the PM tier change where those notes live.
Ablations (one-time, each restored and confirmed byte-identical to HEAD by
scripts/ablation-replace.mjs)FAST_TIER_EVENTSemptied: the first run exited 0, because the self-test looped over the constant under test. The fix names the two events literally; rerun red (2 cases).lint-ci-enginedeleted from the aggregate'sneeds::check:required-contextsred, naming the unwired pin.needs:read injobsCarriedByLintdeleted: red on the declared-only fixture.lintdropped fromREQUIRED_AGGREGATORS:check-aggregator-roster --self-testred on the lint refusal fixture.Gates run locally at
79b0fd6d(this head, after mergingorigin/main)Derived with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack, which found 54 commands; I ran all of them.check-aggregator-roster(and its--self-test),check:required-contexts,check-ci-filter-parity,scripts/ci/scheduled-full-run.mjs --self-test,check-self-test-workflow-commands,check-step-collectors,check-self-test-wired,check:select-gate-families,check:workflow-step-name-quoting,check:workflow-status-functionsandcheck:nul-bytes.pnpm check:pm-dispatch-gatesran detached, exit 0: the fast tier, 1828 cases, 264.9 s on this box.pnpm check:type-check-debtis NOT MEASURED. Its--self-testpassed. Its--re-measurehalf runs tsc over the ledgered package and was cut at 500 s (exit 124). It needs the build that CI'sType Check · debt ledgerlane runs first, and this diff touches no TypeScript program.--ranreconciliation: 54 derived, 53 run, 1 NOT-MEASURED, 0 unrun.actionlintis not installed in this container; two YAML parsers agree.Acceptance notes
fetch-depth: 0. I did not re-derive which of the ~190 gates read history. A gate that loses its history degrades to "not verified" rather than red, so a shallow checkout per lane (about 20 s each) is left to a later card.check:select-gate-familiesreads lint.yml as text and does not pair each scopedif:with a selector in the same job.typecheckaggregate's inline verdict script still says, in a JS comment inside itsrun:text, that the roster gate runs "in the lint job". I left thatrun:text untouched; the YAML comment beside it is updated.scripts/pm/dispatch-gates.self-test.mjsline 6 says the both-tier run is "the battery CI runs". That is still true onschedule/push, so it is left alone.维护者速读(草稿)
改了什么:把
Lint & Repo Gates这个必过检查从"一台机器串行跑 193 步"拆成 8 条并行车道,原名字保留做汇总(合并队列按名字认,所以不受影响)。另外,PM 派发工具的自测在 PR 和合并队列上只跑快速档,完整档改在每小时定时跑和合入 main 后跑。为什么改:这个检查中位 17.8 分钟、最长 38.5 分钟,一半的合并队列都在等它。按 70 次真实运行重算,拆分后预计中位约 4.2 分钟、最长约 5.1 分钟(PR 和合并队列上)。
风险与代价(含回滚):每次运行多占约 7 台机器、多约 7 分钟的安装时间(机器分钟换等待时间)。PM 工具的慢速档用例不再在 PR 上跑,有问题会在 main 上的定时运行里第一次变红(这是裁决接受的代价)。回滚就是 revert 这个 PR,必过检查名字没变,设置里不用动。
席位意见:
你要做的:看本 PR 自己那次 CI 的各车道耗时(dev 报告评论里有),确认 8 条车道和汇总都是绿的,
Lint & Repo Gates仍出现在检查列表里;然后批准合入。Generated by Claude Code