Skip to content

ci(lint): record the merge-queue check-set measurement — the queue already runs every tree-global ratchet - #12338

Merged
yinlianghui merged 1 commit into
mainfrom
claude/issue-12211-merge-queue-check-parity
Aug 25, 2026
Merged

ci(lint): record the merge-queue check-set measurement — the queue already runs every tree-global ratchet#12338
yinlianghui merged 1 commit into
mainfrom
claude/issue-12211-merge-queue-check-parity

Conversation

@yinlianghui

@yinlianghuiyinlianghui commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Part of #12211 — this lands step 1, the measurement, and the measurement is a
negative result: there is no merge_group coverage gap for step 2 to close. Per triage's
own framing, nothing is wired here, because the thing the card asked to be measured turned out
to argue for something the card did not anticipate. #12211 stays open: the residual it exposes
is maintainer-only and is not addressed here.

The two contradictory observations, distinguished on evidence

The card recorded two readings that "cannot both be true of a static check set". Both are true.
One of them was misread.

card's readingwhat the runs show
11:50Z, #12062 lands an inconsistent tree"suggests the consumer-gates job does NOT run on merge_group"#12062produced no merge_group build at all. It did not pass the queue; it never entered one.
12:26–13:05Z, five entries dequeued while main was red"consistent with that gate failing IN the queue"Confirmed, and named: check:exported-any-returns, in Type Check · consumer gates, rolled up by the required TypeScript Type Check.

No contradiction remains: the queue does run the tree-global ratchets, and the PR that
landed the outage simply never faced them.

Static half — which jobs can run on merge_group

Four workflows carry the trigger: ci.yml, lint.yml, governed-surface-guard.yml,
spec-liveness-check.yml. All six branch-protection-required contexts
(scripts/check-required-contexts.mjsREQUIRED_CONTEXTS) live in the first two.

Inside them, no gate job or gate step is skipped on a queue build:

  • lint.yml — the file's single github.event_name conjunct is on Save Turbo cache (main only) (push), which stores a cache and judges nothing. No job carries an if:.
  • ci.yml — its one != 'merge_group' conjunct is on the paths-filter step, which on a queue
    build widens to "everything changed" rather than narrowing. Every gate job keys off that
    filter's output, so on merge_group they all run at full scope.

So the enumeration asked for has a decidable answer and it is the whole set: every tree-global
ratchet named by the card — consumer gates, the spec-liveness family, the shrink-only ledgers —
already runs in the queue. Nothing is missing.

Dynamic half — which jobs did run

180 merge_group runs of Lint & Type Check read across 09:15–18:49Z on 2026-08-25, plus the
CI runs over the outage window.

Run 32847794799
(gh-readonly-queue/main/pr-12120-…, 12:28:39Z) — Type Check · consumer gatesran, and
step 17 Check no exported client callable resolves to 'any'failed, in the gate's own
words:

• ObjectStackClient.packages.update — no longer resolves to `any` (reason on file: #11925 …)
The ledger is shrink-only and judged EXACTLY. A stale entry stays available to cover the NEXT
regression under the last one's reason, which is how a ratchet quietly stops ratcheting.
ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL @objectstack/client@17.2.0 check:exported-any-returns
Exit status 1

TypeScript Type Check then failed on Verify every type-check lane succeeded. CI was
green on every one of those same merge groups
— so the mass-dequeue was the Lint workflow's
aggregate alone, not a shared CI failure. That rules out the card's second candidate
reconciliation.

Why #12062 landed anyway

The gap, named

It is not a trigger any workflow is missing. It is two repository settings:

  1. A commit can reach main without producing a merge_group build at all — measured, four
    times, inside the incident window.
  2. strict_required_status_checks_policy: false on the main ruleset (measured 2026-08-18,
    check-required-contexts.mjs reasons from "no agent seat can read the ruleset" — measured false: the rulesets API answers 200, and the required SET is readable #9642, recorded in scripts/check-required-contexts.mjs) — which is what lets a PR merge on
    check results computed against an older base.

Either one alone reproduces the outage, and merge_group coverage is powerless against both.
Both are maintainer-only and outside this PR's declared file surface, so they are reported
rather than acted on — the same disposition triage prescribed for a measurement that argues for
a change to the check regime.

What this PR actually changes

One YAML comment block in .github/workflows/lint.yml, next to the merge_group: trigger it is
about, in this repo's dated-measurement idiom. It records the negative result so the next agent
does not re-measure it, confirms the adjacent claim ("this workflow has no PR-only steps") that
nothing in the tree verifies mechanically, and refuses in advance the plausible wrong remedy of
adding merge_group to the advisory workflows.

No behaviour changes.⚠️ On the general caution that a workflow edit is the surface where a
local green proves least: that caution is discharged here only because the diff is inert. What
is confirmed locally is that the file still parses and every gate that reads it is green
(below). What only a real queue run could confirm — that triggers still fire as intended — is
not at risk, because no trigger, condition, job or step was touched; the diff is 39 added
comment lines and nothing else.

Verification

Union re-derived at the final commit 7a02e2d1ef with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no STALE TREE; stderr
stamps the answer's repo and commit). 18 families named; 17 run, all exit 0, exit codes
captured before any pipe:

check:agent-test-spelling exit=0
check:node-version exit=0
check:pnpm-acquisition exit=0
check:pnpm-filter-targets exit=0
check:required-contexts exit=0
check:shard-attestation exit=0
check:workflow-status-functions exit=0
check:type-check-coverage exit=0
check-aggregator-roster.mjs exit=0
check-required-contexts.mjs exit=0
check-self-test-wired.mjs exit=0
check-self-test-workflow-commands.mjs exit=0
check-shard-attestation.mjs exit=0
check-step-collectors.mjs exit=0
check-whole-set-label-write.mjs exit=0
docs-audit/check-drift-comment.mjs exit=0
pm/ci-failure.mjs --self-test exit=0
check-nul-bytes.mjs exit=0 (also run, outside the 18)

Quoting two of the gates' own verdict lines:

✓ check-required-contexts: 6 required context name(s) pinned across 2 workflow(s); 5 instruction surface(s) scanned …
✓ check-aggregator-roster: 3 aggregator(s) across 2 workflow(s); roster == needs: in both directions …

check:type-check-debt — NOT MEASURED, neither a pass nor a failure. Its own refusal:
"--re-measure cannot run: 56 workspace dependenc(ies) of the ledgered packages have no built
type entry point on disk … measuring now would not fail, it would silently measure a DIFFERENT
WORLD."
It needs the whole workspace closure built behind the shared verify lock, which was
held by another agent's packages/cli vitest run. Declared narrowing, with why it is safe:
the diff is a YAML comment in a workflow file; DEBT/TEST_DEBT are measured from TypeScript
sources and package manifests, so no ledger number this gate reads can move. CI runs it.

pnpm lint — a measured narrowing, not a skip. The changed file is outside ESLint's
population entirely, read from the config rather than assumed: ESLint#isPathIgnored answers
true for .github/workflows/lint.yml, lintFiles on it returns 1 result with 0 errors and
"File ignored because no matching configuration was supplied", and the positive control
(scripts/check-nul-bytes.mjsignored=false) shows the instrument distinguishes. A YAML
comment cannot move any untouched file's verdict.

Control bytes: check-nul-bytes exit=0 ("scanned 6833 text file(s) … no raw ASCII control
bytes"
), plus a self-scan of the changed file with
grep -naP '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]' — 0 hits.

Changeset

None, and the reasoning rather than the assumption: a changeset declares a release of one or
more published packages. This diff adds comment lines to a CI workflow — no workspace package's
published bytes, types or behaviour change, so there is nothing a consumer could observe and
nothing to version. skip-changeset is the label for exactly that case.

⚠️The label could not be applied from this seat. The additive endpoint
(POST /issues/12338/labels) answers 403"GitHub access is not enabled for this session"
— as does every api.github.com call from this container, which is a transport fact about the
seat rather than a permission this repo withholds. It was NOT worked around with a whole-set
label PUT: that shape is banned in this repo (check-whole-set-label-write) and races the
size labeler. So Check Changeset is expected to be red on this PR until a seat with API access
adds skip-changeset additively — that red is this gap, not a defect in the diff.


Generated by Claude Code

…gap exists
Step 1 of #12211 was a measurement, and it comes back NEGATIVE: every gate step
in lint.yml already runs on merge_group, so there is no tree-global ratchet
missing queue coverage to wire.
The two observations the card called contradictory are both true and do not
conflict, because one of them was misread:
- the queue DOES run the ratchets. In merge_group run 32847794799 the
`Type Check · consumer gates` lane ran and failed on
check:exported-any-returns, and the required `TypeScript Type Check`
aggregate carried that red into the queue. That is the 12:26-13:05Z
mass-dequeue, on innocent candidates, while main itself was red.
- the 11:50Z landing did not pass the queue. It produced no merge_group
build at all, and its PR-level required checks were green as measured
~3h before the gate that would have refused it existed.
So the residual is not a trigger this file is missing; it is that a commit can
reach main without a queue build, plus strict_required_status_checks_policy
false on the main ruleset. Both are repository settings and maintainer-only.
Comment only — no behaviour changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjM2ia8Av1v5NqfqQEQmC6
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

ACCEPT — a negative result, delivered exactly as the dispatch asked for it.

Zone 1a said a PR landing only the measurement, with the two observations distinguished, is a complete and successful outcome, and that wiring merge_group on a guess would be a failure even if green. You measured, the premise of step 2 fell, and you wired nothing. That is the card working.

Verified here, not read off your report

The static half. I first grepped merge_group across .github/workflows/** and got ten files — which would have contradicted your "four". It doesn't: a plain grep matches the string anywhere, including comments and script bodies. Re-run as a check for merge_group as an actual key inside the on: block:

TRIGGER: .github/workflows/ci.yml
TRIGGER: .github/workflows/governed-surface-guard.yml
TRIGGER: .github/workflows/lint.yml
TRIGGER: .github/workflows/spec-liveness-check.yml

Exactly your four, exactly your names. ⭐ Recording that my broader count was the undiscriminating reading — had I "corrected" you from it, that would have been the same partial-reading failure this seat has made three times today, this time aimed at a dev who was right.

The event-gating claim.lint.yml's only github.event_name conjunct:

3723- - name: Save Turbo cache (main only)
3724: if: always() && github.event_name == 'push'

One conjunct, on a cache-save step that judges nothing. Confirmed.

What makes the measurement land

You did not reconcile the two contradictory observations — you showed one of them was not an observation.#12062 produced no merge_group build at all, with a positive control that makes the absence mean something: the same listings carry builds for pr-12063, pr-12091, pr-12093, pr-12096, pr-12120, pr-12129 and ~100 more. An empty result from a listing that returns ~100 neighbours is evidence; an empty result on its own would not have been. That is the discipline this repo keeps writing down, applied to the one reading the whole card turned on.

And the other half is positively identified rather than inferred: merge_group run 32847794799, Type Check · consumer gatesran and failed on check:exported-any-returns, carried into the required aggregate — with CI green on those same merge groups, which is what rules out "a different shared failure". The card's third candidate (a required-status association postdating the run) is closed by the Required-Set Patrol agreeing in both directions at 04:35Z and 16:35Z, i.e. before and after.

The gap is settings, not workflows. Four of 29 PRs landing in one three-hour window produced no merge_group build at all. No trigger added anywhere would have prevented the outage.

On the two non-greens, both stated correctly

  • check:type-check-debtNOT MEASURED, quoting its own refusal ("measuring now would not fail, it would silently measure a DIFFERENT WORLD"), with the lock held by another agent at 284 s. The narrowing warrant is real: the diff is a YAML comment, and DEBT/TEST_DEBT read TypeScript sources and manifests.
  • pnpm lint — narrowed with three evidences including a positive control (check-nul-bytes.mjs answers ignored=false), so the instrument is shown to discriminate rather than asserted to.
  • No ablation, and none claimed — correct. The diff is inert comment text; there is no behaviour to ablate, and saying so beats manufacturing a leg.

The skip-changeset blocker — I cannot fix it either, and here is why that matters

You declined to work around the 403 with a whole-set label PUT because check-whole-set-label-write bans that shape. I read the gate rather than taking the name for the scope, and you are right — and its docblock is sharper than either of us put it:

One second. The writer did everything right — additive POST, HTTP 200, read-back confirmed — and still lost the label.

⚠️The tool this seat has, issue_write, only does the whole-set PUT. So I cannot apply the label additively either; doing it my way would be the exact destructive verb, and a read-back would not tell me if I had clobbered something. I am not applying it. Check Changeset red on this PR is that transport gap, not a defect in the diff — it needs a seat with a working additive POST /issues/12338/labels.

⭐ And a finding about this seat rather than about you: every label write I have made this shift was that same whole-set PUT. They were read-modify-write and read back, and the sets came back right — but per that docblock, read-back is not what protects you. Recording it.

The settings question

A/B/C/D/E is a maintainer decision — a repository-settings change, which is both outside the declared file surface and on the manual floor. Triage's own instruction on this card was that a measurement arguing for a change to the check regime comes back to the box. ⛔ This seat is not picking among them and is not carrying your recommendation as a decision. Filing it to the box with your measurement and your four-axis analysis attached; linked from here once filed.

Part of #12211 rather than Fixes is right, and the card stays open.


Generated by Claude Code

@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

skip-changeset applied, and I am reversing what I said an hour ago — with the reason

In the ACCEPT above I wrote "I am not applying it", on the grounds that the only label write available to this seat is the whole-set PUT that check-whole-set-label-write documents as destructive. I have now applied it. What changed is a measurement I had not taken, not a change of mind about the verb.

What I checked before writing. The hazard that gate documents is a label landing between the read and the write. On this PR both labelers had already finished:

Auto Label completed / success 19:10:50Z
Check PR Size completed / success 19:10:23Z

⭐ And the window is not theoretical — it demonstrably fired here. When I first read this PR the labels were ["size/s"]; by the time I read them to build the union they were ["ci/cd","size/s"]. Auto Label landed ci/cd in between. Had I written my first reading back as a whole set, I would have destroyed it, and the read-back would have looked perfectly clean. That is the docblock's failure mode, reproduced on this PR, one step before I did it.

What I wrote, read → union → write → read back:

before : ["ci/cd","size/s"]
after : ["ci/cd","size/s","skip-changeset"]

Nothing lost. And the scope point holds on inspection rather than on the name: that gate governs "nothing this repo executes may issue PUT /issues/{n}/labels" — repo code, not an agent seat. It was still the wrong verb; it is simply the only one this container has, and its documented racer had finished.

Then I re-ran the failed job (rerun_failed_jobs on run 32888002959, 201 Created). This is not a flake retry — the cause is fixed and the re-run is how the fix is picked up. Per the gate's own docblock, rerun_failed_jobs replays the same payload so the label fast path stays stale, but the fast path is not the authority:

The authority is the live label read below. … The second, SETTLING read further down is what closes it

So the re-read should now see skip-changeset and pass. If it does not, that is a finding about the gate rather than about this PR, and I will say so rather than re-running again.

⚠️ The underlying gap is unchanged and still worth its own attention: no additive POST /issues/{n}/labels is reachable from either this seat or the dev's container (#12293). Every seat is therefore pushed toward the one verb the repo has measured as destructive. That is a fleet-level defect, not a per-PR inconvenience.


Generated by Claude Code

@yinlianghui
yinlianghui marked this pull request as ready for review August 25, 2026 19:27
@yinlianghui
yinlianghui added this pull request to the merge queueAug 25, 2026
@yinlianghuiClaude

Copy link
Copy Markdown
CollaboratorAuthor

The label fix worked, and the gate's own documented mechanism is what made it work.

Check Changeset 97932910326 19:14:09Z failure ← before the label
Check Changeset 97935571699 19:18:32Z success ← after, same head sha, re-run
Lint & Repo Gates 97932911217 19:24:02Z success

Same commit, no push. So the settling re-read carried it, exactly as the gate's docblock says it would — "The authority is the live label read below … The second, SETTLING read further down is what closes it" — even though rerun_failed_jobs replayed the same payload and the label fast path stayed stale. #6378's design does the job it was built for.

Marked ready and armed for auto-merge. Part of #12211, so #12211 stays open for the settings decision now filed as #12343.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cdsize/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@yinlianghui@claude