From 55f65aae844902fc888c531225a58cfda21d1a74 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Tue, 18 Aug 2026 00:22:53 +0000 Subject: [PATCH 1/2] ci(lint): rename the `ESLint` required context to `Lint & Repo Gates` (#9325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `lint` job in lint.yml publishes one check run whose name is its `name:`, and that name was `ESLint` while the job ran ~70 sequential `pnpm check:*` steps. Any one of them failing published a red check called `ESLint` on a PR whose diff may have nothing to do with linting, so the reader had to open the log and scroll to the failing step before they could even decide whose problem it was. That cost was paid three times on 2026-08-17 (#9258, PRs #9256/#9291), and the compounding harm is that a check whose name does not describe what it verifies trains readers to re-run on red -- which is how a real regression in this gate family gets waved through (#5617 is that failure, from the other direction). Renames the job to `Lint & Repo Gates` and moves the pinned literal in the `check-required-contexts.mjs` registry (#6865) in the same diff, so the two repo-side halves cannot drift. The self-test's rename ablation now anchors on the new name and mutates it back to `ESLint`, which makes it the regression test for this very change: reverting lint.yml without the registry is red. ⚠️ This is only the repo-side half. A GitHub required status check is matched by check-run name, so the entry in Settings -> Rulesets must be swapped from `ESLint` to `Lint & Repo Gates` by a maintainer immediately after this merges. Merging without that swap leaves every open PR and the merge queue wedged on a permanently-pending `ESLint` that can never report again. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NYgmGheCzM6NrHZN436Cxf --- .github/workflows/lint.yml | 16 ++++++++++- AGENTS.md | 5 ++-- scripts/check-required-contexts.mjs | 42 ++++++++++++++++++++++------- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 719600d207..7dfba8dc25 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -22,7 +22,21 @@ concurrency: jobs: lint: - name: ESLint + # ⚠️ This `name:` IS the required-status-check context in repository + # Settings → Rulesets (a job's check-run name is its `name:`), so it is + # pinned as contract by `scripts/check-required-contexts.mjs` and may only + # be changed together with that registry AND the Settings entry, in one + # maintainer-present sitting (#9325 ruling 2026-08-17). Either half alone + # is an outage: rename-first leaves the old context permanently pending, + # which wedges every open PR and the merge queue; settings-first drops the + # whole gate family to advisory with no signal anywhere, which is #5617 + # verbatim. + # + # It is not called `ESLint` any more because it never was only that: the + # `pnpm lint` step below is one of ~70 sequential gate steps, so a red here + # was routinely read as "a lint problem" when it was a repo gate — three + # mis-routed diagnoses on 2026-08-17 alone (#9258, PRs #9256/#9291). + name: Lint & Repo Gates runs-on: ubuntu-latest permissions: contents: read diff --git a/AGENTS.md b/AGENTS.md index c9166308bf..677c6553cf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -333,8 +333,9 @@ Even inside your own worktree, operate defensively: yet"; `in_progress` is not a pass. Arming a red PR does not queue it, it hides it: every poll then misreads "not on `main` yet" as "queued". Always read *two* things when checking a landing: the queue branch **and** `origin/main`. And **the queue - enforces only the required set** — **ESLint** and **TypeScript Type Check** block by - maintainer decision (2026-08-07); everything else is advisory and rides through, and + enforces only the required set** — **Lint & Repo Gates** (called `ESLint` until the + #9325 rename) and **TypeScript Type Check** block by maintainer decision + (2026-08-07); everything else is advisory and rides through, and an advisory red that lands rides `main`'s merge ref into every following PR until stanched. That is why "arm only on green" is a rule, not a formality. diff --git a/scripts/check-required-contexts.mjs b/scripts/check-required-contexts.mjs index f2b5a3d4f1..fbfe3fbb2f 100644 --- a/scripts/check-required-contexts.mjs +++ b/scripts/check-required-contexts.mjs @@ -157,8 +157,19 @@ export const REQUIRED_CONTEXTS = [ { workflow: 'lint.yml', job: 'lint', - context: 'ESLint', - authorized: '#5617 maintainer ruling 2026-08-07 — applied to the settings the same day', + // Renamed from 'ESLint' under the #9325 ruling: the old name described one + // of the job's steps and mis-routed the diagnosis of every other gate it + // carries. The rename lands in two halves that CANNOT be atomic — this + // registry + the workflow `name:` here, and the required-context entry in + // repository Settings → Rulesets — so it is done in one maintainer-present + // sitting: merge, then swap the Settings entry immediately. Either half + // alone is an outage (permanently-pending, or advisory-with-no-signal = + // #5617). If you are reading this while a required context named 'ESLint' + // is still live in Settings, that swap has not happened yet. + context: 'Lint & Repo Gates', + authorized: + '#5617 maintainer ruling 2026-08-07 (enrolment, applied to the settings the same day); ' + + 'renamed from `ESLint` by the #9325 maintainer ruling 2026-08-17, swapped in the settings in the same sitting as the merge', carries: 'the whole check:* gate family — the job whose red did not block #5584', }, { @@ -540,12 +551,21 @@ async function selfTest() { // Predicted direction: RED, naming the job, the new name and the required // context. A rename is the whole defect #6865 is about, so a green here would // mean the gate is decorative. - const renamedEslint = fixture('rename ESLint', 'lint.yml', (s) => s.replace(' name: ESLint\n', ' name: ESLint (fast)\n')); + // The anchor is the CURRENT name of the gate-family job (renamed away from + // `ESLint` by #9325); the mutation restores the OLD spelling on purpose, so + // this fixture doubles as the regression test for that rename: reverting + // lint.yml alone, without this registry, is exactly the half-landed state + // the #9325 sequencing exists to prevent, and it must be red. + const renamedGateJob = fixture('rename the gate-family job back to ESLint', 'lint.yml', (s) => + s.replace(' name: Lint & Repo Gates\n', ' name: ESLint\n'), + ); assert( - renamedEslint.problems.some((p) => p.includes("job 'lint'") && p.includes('"ESLint (fast)"') && p.includes("'ESLint'") && p.includes('The name IS the contract')), - "renaming lint.yml's ESLint job ⇒ red, naming the job, the new name and the required context", + renamedGateJob.problems.some( + (p) => p.includes("job 'lint'") && p.includes('"ESLint"') && p.includes("'Lint & Repo Gates'") && p.includes('The name IS the contract'), + ), + "renaming lint.yml's gate-family job ⇒ red, naming the job, the new name and the required context", ); - assert(renamedEslint.problems.length === 1, `renaming ESLint produces exactly the one finding — got ${JSON.stringify(renamedEslint.problems)}`); + assert(renamedGateJob.problems.length === 1, `renaming the gate-family job produces exactly the one finding — got ${JSON.stringify(renamedGateJob.problems)}`); // The second-batch half: a name the maintainer approved on 2026-08-09, which // had no assertion of any kind before this script. @@ -609,7 +629,7 @@ async function selfTest() { // ── (6) the merge_group trigger ────────────────────────────────────────── const noQueue = fixture('drop merge_group from lint.yml', 'lint.yml', (s) => s.replace('\n merge_group:\n', '\n')); assert( - noQueue.problems.some((p) => p.includes('merge_group') && p.includes('ESLint') && p.includes('TypeScript Type Check')), + noQueue.problems.some((p) => p.includes('merge_group') && p.includes('Lint & Repo Gates') && p.includes('TypeScript Type Check')), 'a required-context workflow without merge_group ⇒ red, naming every context it would strand', ); assert( @@ -782,7 +802,7 @@ async function selfTest() { const lintJob = uncommented(lintJobStart === -1 ? '' : sources['lint.yml'].slice(lintJobStart, lintJobEnd === -1 ? undefined : lintJobEnd)); assert( /run: pnpm check:required-contexts\b/.test(lintJob), - 'wiring: lint.yml\'s ESLint job must run `pnpm check:required-contexts` — an unwired pin verifies nothing (#4690)', + 'wiring: lint.yml\'s `lint` job (the "Lint & Repo Gates" context) must run `pnpm check:required-contexts` — an unwired pin verifies nothing (#4690)', ); const step = lintJob.split(/\n(?= - name: )/).find((s) => /run: pnpm check:required-contexts\b/.test(s)) ?? ''; assert( @@ -794,8 +814,10 @@ async function selfTest() { assert(/check-required-contexts\.mjs --self-test/.test(wiring), 'wiring: `check:required-contexts` must run this file\'s --self-test first'); assert(/check-required-contexts\.mjs(?! --self-test)/.test(wiring), 'wiring: `check:required-contexts` must also run the real pin, not only the self-test'); // The pin lives in a job it also pins. That is deliberate and worth stating: - // renaming the ESLint job turns this gate red under the NEW name, while the - // old required context stops reporting — the PR is blocked from both sides. + // renaming the gate-family job turns this gate red under the NEW name, while + // the old required context stops reporting — the PR is blocked from both + // sides. That is also why the #9325 rename could not be self-serve: the + // repo-side half is checkable here, the Settings half is not reachable at all. assert( REQUIRED_CONTEXTS.some((entry) => entry.workflow === 'lint.yml' && entry.job === 'lint'), 'wiring: the job this gate runs in is itself registered, so a rename of it cannot be the one rename nothing notices', From b47e0f344e85e9035638de41bbd0ea396da712dc Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Tue, 18 Aug 2026 00:33:48 +0000 Subject: [PATCH 2/2] docs(agents): state the required-set rule self-contained, without an issue pointer `check:pm-skill-id-lint` scans AGENTS.md for /#[0-9]{3,}/ and the previous revision of this sentence carried one, added by the rename commit. The rule it enforces (maintainer ruling 2026-08-12) is that operative agent-protocol text distills its lesson in place -- failure mode, discipline, boundary -- because an issue number invites the reader to dereference history for less than it costs; only a ruling citation keeps a number, and then with date and verbatim quote. So the name history is now prose rather than a pointer, and the paragraph gains the operative half it was missing: a required context is matched by check-run name, so a job rename detaches its gate silently. That is the fact a reader of this paragraph actually needs, and it no longer depends on reading a card. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01NYgmGheCzM6NrHZN436Cxf --- AGENTS.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 677c6553cf..3536241183 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -333,11 +333,13 @@ Even inside your own worktree, operate defensively: yet"; `in_progress` is not a pass. Arming a red PR does not queue it, it hides it: every poll then misreads "not on `main` yet" as "queued". Always read *two* things when checking a landing: the queue branch **and** `origin/main`. And **the queue - enforces only the required set** — **Lint & Repo Gates** (called `ESLint` until the - #9325 rename) and **TypeScript Type Check** block by maintainer decision - (2026-08-07); everything else is advisory and rides through, and - an advisory red that lands rides `main`'s merge ref into every following PR until - stanched. That is why "arm only on green" is a rule, not a formality. + enforces only the required set** — **Lint & Repo Gates** (the whole `check:*` gate + family, formerly published under a name that described only one of its steps) and + **TypeScript Type Check** block by maintainer decision (2026-08-07); everything else + is advisory and rides through, and an advisory red that lands rides `main`'s merge ref + into every following PR until stanched. A required context is matched by check-run + name, so a job rename detaches its gate silently — treat those names as contract. + That is why "arm only on green" is a rule, not a formality. **Re-arm awareness** — none of these is a reason to avoid the queue; all are reasons to confirm a PR is still *in* it: a red queue build **ejects** your entry and drops