From 51123e65f65a4f675ce05c10d5d70d8dbb2a8e95 Mon Sep 17 00:00:00 2001 From: Noah Schatz Date: Thu, 6 Aug 2026 11:50:21 +0000 Subject: [PATCH 1/5] fix(ci): require ci / prepublish, so a red pre-publish gate blocks the merge The shared pipeline grew a `prepublish` job on 2026-08-05 (cosyte/.github#35, 6142ac4; its second layer defaulted on in #36, 90936ea). `ci.yml` calls that workflow at `@main`, so this repo started emitting a `ci / prepublish` context on every pull request with no commit landing here, and ruleset 19907924 did not name it. The job could fail and the merge still landed on `main`, the branch that publishes. A green check that cannot block a merge is documentation. What it gates is this package's own worst shipped defect: an offline manifest lint that refuses a dependency specifier no registry can resolve, plus a probe that packs this tree and installs the tarball into a clean directory. `@cosyte/cli@0.0.1` and `0.0.2` reached the registry carrying `file:vendor/*.tgz` specifiers and are permanently uninstallable. The manifest lint would have refused both. The order is load-bearing and is recorded, because it is easy to get backwards. Requiring a context nothing emits does not fail a pull request; it leaves it pending and unmergeable forever. A census of the eight most recent head shas here (#27 through #34) finds `ci / prepublish` zero times: the newest of them merged 2026-08-04T22:22:24Z and the upstream job postdates it, so it could not have been read off a real check run any earlier. It is read off the check run on this pull request, and only then written to the ruleset. `ci.yml`'s banner now covers the hazard that actually bit. Every previous wording of it was scoped to splitting a step out of `verify` locally; the `uses:` reference is unpinned, so a job added upstream emits a new `ci / ` context here with no commit in this repo, and it always arrives unrequired. Not built, and named as a do-not-build with its reason: a gate inside CI that curls this repo's own ruleset. Anonymous GitHub API is 60 requests per hour per IP and hosted runners share IPs, so it would trade a false green for a flaky red on a required context. --- .changeset/quiet-goats-shave.md | 18 ++++++++++ .github/workflows/ci.yml | 35 ++++++++++++------ CHANGELOG.md | 24 +++++++++++++ CLAUDE.md | 14 ++++++-- documentation/agent-notes.md | 63 ++++++++++++++++++++++++++++----- 5 files changed, 134 insertions(+), 20 deletions(-) create mode 100644 .changeset/quiet-goats-shave.md diff --git a/.changeset/quiet-goats-shave.md b/.changeset/quiet-goats-shave.md new file mode 100644 index 0000000..03d31b0 --- /dev/null +++ b/.changeset/quiet-goats-shave.md @@ -0,0 +1,18 @@ +--- +'@cosyte/cli': patch +--- + +Caller workflow and branch protection only, with no runtime impact: the pre-publish check now blocks a merge here instead of merely showing a red X. + +The shared pipeline grew a `prepublish` job on 2026-08-05, so this repo started emitting a +`ci / prepublish` context on every pull request without a commit landing here. The branch ruleset +did not name it, which meant a red pre-publish result reported a failure and merged anyway. That +job is the offline manifest lint plus the pack-and-install probe, and this package is the reason +both exist: two published versions carry local-path dependency specifiers and are permanently +uninstallable. The context was read off a real check run first and then added to the ruleset, in +that order, because requiring a context nothing emits strands every pull request instead of +failing it. + +The banner on the caller now records the hazard it did not previously cover: a context can arrive +in this repo with no commit in this repo, because the `uses:` reference is unpinned, and it always +arrives unrequired. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4d9d13..e5fedf8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,16 +14,31 @@ permissions: contents: read jobs: - # ┌─────────────────────────────────────────────────────────────────────────────────────────┐ - # │ A REQUIRED JOB GATES ALL OF ITS STEPS. The `ci-required-checks` ruleset on this repo │ - # │ requires the CONTEXTS `ci / verify (22, ubuntu-latest)`, `ci / verify (24, │ - # │ ubuntu-latest)` and `ci / actionlint`. Every gate the shared pipeline runs (typecheck, │ - # │ lint, format:check, PHI scan, test, gating coverage, build, attw, dual ESM/CJS smoke) is │ - # │ a STEP inside `verify`, and is required only because that job is. Promote any one of │ - # │ them to its own job and it is silently NO LONGER REQUIRED: the ruleset names contexts, │ - # │ the new context is not in it, and nothing anywhere errors or warns. If you split a step │ - # │ out, add its context to the ruleset in the same change. │ - # └─────────────────────────────────────────────────────────────────────────────────────────┘ + # ┌──────────────────────────────────────────────────────────────────────────────────────────────────┐ + # │ A REQUIRED JOB GATES ALL OF ITS STEPS. The `ci-required-checks` ruleset on this repo requires │ + # │ all four contexts this caller emits: `ci / verify (22, ubuntu-latest)`, `ci / verify (24, │ + # │ ubuntu-latest)`, `ci / prepublish` and `ci / actionlint`. Every gate the shared pipeline runs │ + # │ (typecheck, lint, format:check, PHI scan, test, gating coverage, build, attw, dual ESM/CJS │ + # │ smoke) is a STEP inside `verify`, and is required only because that job is. Promote any one of │ + # │ them to its own job and it is silently NO LONGER REQUIRED: the ruleset names contexts, the new │ + # │ context is not in it, and nothing anywhere errors or warns. If you split a step out, add its │ + # │ context to the ruleset in the same change. │ + # │ │ + # │ AND THE HAZARD IS NOT ONLY A LOCAL SPLIT: THE CALLED WORKFLOW CAN GROW A JOB UPSTREAM. The │ + # │ `uses:` below is pinned to `@main`, so a job added over there starts emitting a new `ci / ` │ + # │ context here with NO COMMIT IN THIS REPO, and it arrives NOT REQUIRED: a red X that does not │ + # │ block a merge. That is not hypothetical. `prepublish` arrived exactly that way on 2026-08-05 and │ + # │ was unrequired here until it was measured off a real check run and added. A `ci / *` context you │ + # │ do not see in the list above is that same event happening again. Decide it, and write the │ + # │ decision down: require it, or record here why it must not be. │ + # └──────────────────────────────────────────────────────────────────────────────────────────────────┘ + # + # `prepublish` is the pre-publish gate: it refuses a dependency specifier no registry can resolve + # (offline manifest lint) and then packs this tree and installs the tarball into a clean directory. + # Both layers are ON by default upstream and this caller passes neither input, so the defaults are + # the whole story. It is the only place a consumer's install question gets asked while the answer + # can still prevent a permanent publish: `@cosyte/cli@0.0.1` and `0.0.2` are on the registry + # uninstallable forever, and this job is what would have refused them. # # Universal gates from the shared pipeline (typecheck, lint, format, test, coverage, build, attw, # dual ESM/CJS smoke, actionlint) on the standard Node matrix. Repo-specific extra jobs (e.g. an diff --git a/CHANGELOG.md b/CHANGELOG.md index e2772a4..042d4e4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,30 @@ still do. Each entry was assigned to the release whose tag first contains it, re ### Fixed +- **A red pre-publish gate showed a red X and merged anyway, because `ci / prepublish` was a + required check nowhere (CI-REQUIRED-CHECKS).** The shared pipeline this repo calls grew a + `prepublish` job on 2026-08-05 (`cosyte/.github#35`, `6142ac4`; its second layer defaulted on in + `#36`, `90936ea`), so `ci.yml` began emitting a `ci / prepublish` context on every pull request + here **with no commit landing in this repo**. Repository ruleset `19907924` did not name it, so + the job could fail and the merge still landed on `main`, which is the branch that publishes. + `ci / prepublish` is now a required context, `integration_id`-pinned like the other six. + - **What it gates is this package's own worst shipped defect.** The job is an offline manifest + lint that refuses a dependency specifier no registry can resolve, plus a probe that packs this + tree and installs the tarball into a clean directory. `@cosyte/cli@0.0.1` and `0.0.2` reached + the registry carrying `file:vendor/*.tgz` specifiers and are permanently uninstallable; the + manifest lint would have refused both. A gate that catches that and then cannot block the merge + reintroducing it is documentation. + - **The order was load-bearing and is recorded because it is easy to get backwards.** The context + name was read off a real `pull_request` check run before the ruleset was written, never off the + workflow's `name:` field. Requiring a context nothing emits does not fail a pull request, it + leaves it pending and unmergeable forever. A census of the eight most recent head shas here + (`#27` through `#34`) finds `ci / prepublish` **zero** times, because the newest of them merged + before the upstream job existed, so it could not have been required any earlier. + - **`ci.yml`'s banner now covers the hazard that actually bit.** Every previous wording of it was + scoped to splitting a step out of `verify` locally. The `uses:` reference is unpinned, so a job + added upstream emits a new `ci / ` context here with no commit in this repo, and it always + arrives unrequired. + - **The shipped documentation sidebar was off the canonical IA spine, and it was holding up the docs site's deploy (CLI-SIDEBAR-IA-NONCANONICAL).** `docs-content/sidebars.json` declared two top-level categories that are not on the spine, **"MCP server"** and **"Reference"**. The docs diff --git a/CLAUDE.md b/CLAUDE.md index 49999a8..1250be1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -254,11 +254,21 @@ Full ruleset, the required-context table and the per-check reasoning: [agent-notes § Branch protection](documentation/agent-notes.md#branch-protection-and-the-limits-of-this-claim). - `main` is protected by the repository ruleset **`ci-required-checks`** (id `19907924`). Before it - existed every check here was advisory, on the branch that publishes. Six required contexts, each - pinned to **`integration_id: 15368`** so a same-named status from another actor cannot satisfy it. + existed every check here was advisory, on the branch that publishes. **Seven** required contexts, + each pinned to **`integration_id: 15368`** so a same-named status from another actor cannot satisfy + it. **Do not quote that count without re-deriving it** (`gh api repos/cosyte/cli/rulesets/19907924`): + it was `6` until `ci / prepublish` was added, and it moves when the called workflow does. - **Read a required context off a REAL check run, never off a workflow's `name:` field.** Requiring a context nothing emits does not fail a PR: it leaves it **pending and unmergeable forever**, with no error and no warning. +- **▶ A `ci / *` CONTEXT CAN APPEAR HERE WITH NO COMMIT IN THIS REPO, AND IT ARRIVES NOT REQUIRED.** + `ci.yml` calls `cosyte/.github/.github/workflows/ci.yml@main` unpinned, so a job added upstream + starts emitting a context here that the ruleset does not name: **a red X that does not block a + merge**, which is the failure this repo's whole protection claim exists to prevent. `ci / prepublish` + arrived that way on 2026-08-05 and was unrequired until measured and added. **Census `ci / *` against + a real check run whenever `.github` moves**, then require it or write down why not, in `ci.yml`'s + banner. Detail: + [agent-notes § Branch protection](documentation/agent-notes.md#branch-protection-and-the-limits-of-this-claim). - **`no-internal-refs` and `no-emdash` are bare JOB IDS**, not ` / `, because they are ordinary jobs in this repo's own workflows. **Renaming the job silently detaches the required check.** Rename the job and the ruleset together, or neither. diff --git a/documentation/agent-notes.md b/documentation/agent-notes.md index 71c7dfc..9058920 100644 --- a/documentation/agent-notes.md +++ b/documentation/agent-notes.md @@ -402,20 +402,67 @@ advisory: `ci`, `codeql`, `scorecard` and `fuzz` could all be red and the merge Required contexts, each pinned to **`integration_id: 15368`** (the `github-actions` app) so that a commit status of the same name posted by any other actor with write access cannot satisfy it: -| context | -| ------------------------------------------ | -| `ci / verify (22, ubuntu-latest)` | -| `ci / verify (24, ubuntu-latest)` | -| `ci / actionlint` | -| `codeql / analyze (javascript-typescript)` | -| `no-internal-refs` | -| `no-emdash` | +| context | added | +| ------------------------------------------ | ---------- | +| `ci / verify (22, ubuntu-latest)` | 2026-07-28 | +| `ci / verify (24, ubuntu-latest)` | 2026-07-28 | +| `ci / actionlint` | 2026-07-28 | +| `ci / prepublish` | 2026-08-06 | +| `codeql / analyze (javascript-typescript)` | 2026-07-28 | +| `no-internal-refs` | 2026-07-28 | +| `no-emdash` | 2026-07-28 | These are the names GitHub actually reports, read off real check runs, **not** off a workflow's `name:` field. Requiring a context nothing emits does not fail a PR; it leaves it pending and unmergeable forever. None of `ci.yml`, `codeql.yml`, `no-internal-refs.yml` or `no-emdash.yml` carries a `paths:` filter, so no PR can skip one. +**Do not quote the row count from memory, and do not carry it into another document.** It was `6` +until `ci / prepublish` was added and it moves whenever the called workflow does. Derive it: +`gh api repos/cosyte/cli/rulesets/19907924 --jq '[.rules[]|select(.type=="required_status_checks").parameters.required_status_checks[].context]'`. + +### `ci / prepublish`, and the hazard that a context can arrive with no commit here + +**▶ A `ci / *` CONTEXT CAN APPEAR IN THIS REPO WITH NO COMMIT IN THIS REPO, AND IT ARRIVES NOT +REQUIRED.** `ci.yml` calls `cosyte/.github/.github/workflows/ci.yml@main`, unpinned. A job added to +that reusable workflow starts emitting `ci / ` on every PR here immediately, the ruleset does +not name it, and so **a red result from it shows a red X and the merge lands anyway** (this repo's +merge queue is `pull_request` with 0 approvals). Nothing errors, nothing warns, and no commit in this +repo records that the surface changed. Every previously written form of the "a required job gates all +of its steps" warning was scoped to a **local** split, so none of them covered this. + +That is not hypothetical. The `prepublish` job was added upstream in `cosyte/.github#35` (`6142ac4`, +2026-08-05) and its second layer was defaulted on in `#36` (`90936ea`, the same day). **The census +that proves it was unrequired**: on the eight most recent `pull_request` head shas here (`#27` +through `#34`), `ci / prepublish` appears **zero** times, because the newest of them (`#34`) merged +`2026-08-04T22:22:24Z` and the upstream job postdates it. So the job could not have been read off a +real check run before this slice, and requiring it earlier would have been the `knowledgebase` +mistake (naming a context nothing emits) rather than a fix. **The order is load-bearing: it has to +run first, then be added, in that order.** It was read off the real check run on the pull request +that shipped this section before the ruleset was written. + +**What it gates, and why leaving it unrequired was the expensive kind of hole.** `prepublish` runs +two layers: an offline **manifest lint** that refuses a dependency specifier no registry can resolve, +and a **pack-and-install** probe that `npm pack`s this tree and installs the tarball into a clean +anonymous directory. Both default on upstream and this caller passes neither input. This package is +the reason that gate exists: `@cosyte/cli@0.0.1` and `0.0.2` were published carrying +`file:vendor/*.tgz` specifiers and are **permanently uninstallable** (ADR 0001, a published version +never moves backwards). The manifest lint would have refused both. A gate that catches that, and then +does not block the merge that reintroduces it, is documentation. + +**It is required, not merely present, deliberately.** The alternative considered and rejected was to +leave it advisory on the grounds that it touches the network on every PR and a registry blip would +red it. That cost is real and is disclosed upstream: the `pnpm install --frozen-lockfile` in this job +has no registry-outage softening, unlike the pack layer's `inconclusive` verdict. It was accepted +here anyway, because an advisory pre-publish gate on the branch that publishes is the exact shape of +"a green check that cannot block a merge". + +**Not built, and it must not be built without answering one question first.** A gate inside CI that +`curl`s this repo's own ruleset and asserts the required set would close the observability gap named +at the end of this section. Anonymous GitHub API is **60 requests per hour, per IP, and hosted +runners share IPs**, so such a gate trades a false green for a **flaky red on a required context**, +which is worse than the hole. Answer the flakiness question with a measurement before writing it. + **`no-internal-refs` is the one that is NOT ` / `, and the shape is worth knowing.** `ci / verify (22, ubuntu-latest)` is prefixed because `verify` runs inside a _called_ reusable workflow, so the context is ` / (matrix)`. `no-internal-refs` is an From e837a518384ff5bbba6e559b0628e8d13ff0bd98 Mon Sep 17 00:00:00 2001 From: Noah Schatz Date: Thu, 6 Aug 2026 11:55:44 +0000 Subject: [PATCH 2/5] fix(ci): delete the stale required-context count from release.yml `release.yml`'s trap note told the next releaser that the "Version Packages" PR needs four contexts. The ruleset had six before this branch and has seven after it, so the number has been wrong through at least two changes without anyone noticing. It is load-bearing text: it documents the escape from a real trap (a version PR opened by `github-actions[bot]` arrives with zero check runs, and a required context that never arrives is pending rather than failing, unmergeable by anyone including an admin), so a wrong count sends someone to declare the escape finished early. The count is DELETED, not incremented. A number in a workflow comment has nothing to keep it honest, and a required context the text does not know about is exactly what the reader came here to satisfy. The derivation is written in its place. The same note now records that the escape got more expensive on 2026-08-06: with `ci / prepublish` required, the empty commit it prescribes no longer just re-runs static gates, it packs the tree and installs the tarball of the version about to be published, against the live registry. --- .github/workflows/release.yml | 28 +++++++++++++++++++++------- CHANGELOG.md | 9 +++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 943bae7..8472e85 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,16 +5,30 @@ name: Release # Requires the NPM_TOKEN and DOCS_REPO_DISPATCH_TOKEN secrets (passed via `secrets: inherit`). # # ▶ KNOWN TRAP, documented rather than bypassed. The `ci-required-checks` ruleset on this repo -# requires four contexts with `bypass_actors: []`. Changesets opens the "Version Packages" PR as -# `github-actions[bot]` using the default `GITHUB_TOKEN`, and GitHub does not start workflow runs -# for events raised by that token, so that PR arrives with ZERO check runs. Four required contexts -# that never arrive are PENDING, not failing, and with no bypass actors nobody, including an admin, -# can merge past them. The escape, done last and immediately before merging, is one commit onto -# `changeset-release/main`, which raises a normal `pull_request` event and produces all four -# contexts: +# requires several contexts with `bypass_actors: []`. THE NUMBER IS DELIBERATELY NOT WRITTEN HERE: +# it was `4` in this comment while the ruleset had `6`, because a count in a workflow comment has +# nothing to keep it honest, and a required context this text does not know about is exactly the +# thing the reader is here to satisfy. Derive it instead, which takes two seconds: +# +# gh api repos/cosyte/cli/rulesets/19907924 \ +# --jq '[.rules[]|select(.type=="required_status_checks").parameters.required_status_checks[].context]' +# +# Changesets opens the "Version Packages" PR as `github-actions[bot]` using the default +# `GITHUB_TOKEN`, and GitHub does not start workflow runs for events raised by that token, so that +# PR arrives with ZERO check runs. Required contexts that never arrive are PENDING, not failing, and +# with no bypass actors nobody, including an admin, can merge past them. The escape, done last and +# immediately before merging, is one commit onto `changeset-release/main`, which raises a normal +# `pull_request` event and produces every context: # # gh pr checkout && git commit --allow-empty -m "chore: run CI on the version PR" && git push # +# ▶ AND THAT ESCAPE GOT MORE EXPENSIVE ON 2026-08-06, WHICH IS WORTH KNOWING BEFORE YOU RUN IT. +# `ci / prepublish` is now required, and its second layer packs this tree and installs the tarball +# into a clean directory over the network. So the empty commit above no longer just re-runs static +# gates on the version PR: it runs a real install of the version about to be published, against the +# live registry, and a registry blip reds it. That is the intended cost. A version PR whose +# pre-publish probe cannot pass is a version that must not publish. +# # Adding a bypass actor would also clear it and is DELIBERATELY REJECTED: it would let a human merge # a red PR on a published healthcare package. The durable fix is to author the Version PR with a PAT # in the shared `cosyte/.github` release workflow, which is tracked upstream, not here. diff --git a/CHANGELOG.md b/CHANGELOG.md index 042d4e4..4b5c864 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,15 @@ still do. Each entry was assigned to the release whose tag first contains it, re scoped to splitting a step out of `verify` locally. The `uses:` reference is unpinned, so a job added upstream emits a new `ci / ` context here with no commit in this repo, and it always arrives unrequired. + - **`release.yml` was telling the next releaser to expect four contexts on the version PR, and + the ruleset had six.** That comment documents the escape from a real trap (a "Version Packages" + PR opened by `github-actions[bot]` arrives with zero check runs, and a required context that + never arrives is pending rather than failing, unmergeable by anyone), so a wrong count there + sends someone to declare the escape finished early. The count is **deleted rather than + corrected**: a number in a workflow comment has nothing to keep it honest, and the derivation + is now written in its place. The same comment records that the escape got more expensive, since + the empty commit it prescribes now runs a real pack-and-install of the version about to be + published. - **The shipped documentation sidebar was off the canonical IA spine, and it was holding up the docs site's deploy (CLI-SIDEBAR-IA-NONCANONICAL).** `docs-content/sidebars.json` declared two From 697395f45ab46b00f9f9906c93d0a8dab96409e3 Mon Sep 17 00:00:00 2001 From: Noah Schatz Date: Thu, 6 Aug 2026 12:05:09 +0000 Subject: [PATCH 3/5] fix(ci): correct the version-PR trap note, which asserted a state this repo is not in Refuter pass 1 on this branch, three INTRODUCED findings, all addressed here. 1. The trap note in `release.yml` says the "Version Packages" PR "arrives with ZERO check runs" and prescribes an empty commit as the escape. That sentence is pre-existing and false for this repo, and the previous commit made it worse by building a new instruction on top of it. The shared release workflow authors that PR with `RELEASE_PR_TOKEN`, a live cosyte org secret this caller supplies via `secrets: inherit`, so the checks do arrive. Measured on version PRs #24, #26 and #31: each carries exactly one commit, authored by `github-actions[bot]`, and all four workflows started on that commit within about ten seconds, at `run_attempt=1`, with no human commit anywhere. The note now states the measured behaviour, keeps the empty commit only as a diagnosed fallback for a genuinely check-less PR, and gives the command that tells the two apart. This matters because the previous wording told a releaser to push an unnecessary commit onto the branch that publishes, and with `ci / prepublish` now required that push runs a real pack-and-install against the live registry, so a blip would red a required context. That is the do-not-build hazard this item names, reached from the other direction. 2. Adding a required context strands every open PR whose head sha already ran. #33 went BLOCKED and #29 is the same shape, both carrying the seven older contexts green and no `ci / prepublish`. The ordering rule protects PRs opened after the write, not head shas that predate it. Disclosed in agent-notes with the remedy, which is one push per branch. 3. "This repo's merge queue" was wrong: there is no merge queue. What lets a red non-required check merge is `required_approving_review_count: 0` on the ruleset's pull_request rule. Corrected, with the correction named. Also records in `ci.yml` that the upstream `expect-unpublished-deps` default is permanently stale here, because this package declares no `@cosyte/fhir`, and that the obvious way to silence it is the manifest edit CLAUDE.md forbids. --- .github/workflows/ci.yml | 17 ++++++++--- .github/workflows/release.yml | 53 +++++++++++++++++++++++------------ CHANGELOG.md | 24 ++++++++++------ documentation/agent-notes.md | 25 ++++++++++++++--- 4 files changed, 84 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e5fedf8..393ecf5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,10 +35,19 @@ jobs: # # `prepublish` is the pre-publish gate: it refuses a dependency specifier no registry can resolve # (offline manifest lint) and then packs this tree and installs the tarball into a clean directory. - # Both layers are ON by default upstream and this caller passes neither input, so the defaults are - # the whole story. It is the only place a consumer's install question gets asked while the answer - # can still prevent a permanent publish: `@cosyte/cli@0.0.1` and `0.0.2` are on the registry - # uninstallable forever, and this job is what would have refused them. + # Both layers are ON by default upstream and this caller passes neither toggle, so the defaults are + # the whole story for whether it runs. It is the only place a consumer's install question gets + # asked while the answer can still prevent a permanent publish: `@cosyte/cli@0.0.1` and `0.0.2` are + # on the registry uninstallable forever, and this job is what would have refused them. + # + # ONE DEFAULT IS PERMANENTLY STALE HERE, AND THE OBVIOUS "FIX" FOR IT IS FORBIDDEN. The upstream + # default for `expect-unpublished-deps` is `@cosyte/fhir=blocked`, and this package deliberately + # declares no `@cosyte/fhir` in any form, so the allowance names a package that is not a consumer + # dependency of this one and the gate reports it as stale on every run. That notice never fails the + # job, and it must NOT be silenced by adding `@cosyte/fhir` to the manifest: declaring it in any + # form alongside `@cosyte/transform` fails the whole install with `ERESOLVE`, which is the measured + # reason it is absent. See CLAUDE.md. If the notice is ever worth silencing, the way is to pass an + # empty `expect-unpublished-deps` from this caller, not to touch dependencies. # # Universal gates from the shared pipeline (typecheck, lint, format, test, coverage, build, attw, # dual ESM/CJS smoke, actionlint) on the standard Node matrix. Repo-specific extra jobs (e.g. an diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8472e85..b35ad03 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,34 +4,51 @@ name: Release # this repo is public) -> docs artifacts -> GitHub release -> repository_dispatch to cosyte/docs. # Requires the NPM_TOKEN and DOCS_REPO_DISPATCH_TOKEN secrets (passed via `secrets: inherit`). # -# ▶ KNOWN TRAP, documented rather than bypassed. The `ci-required-checks` ruleset on this repo -# requires several contexts with `bypass_actors: []`. THE NUMBER IS DELIBERATELY NOT WRITTEN HERE: -# it was `4` in this comment while the ruleset had `6`, because a count in a workflow comment has -# nothing to keep it honest, and a required context this text does not know about is exactly the -# thing the reader is here to satisfy. Derive it instead, which takes two seconds: +# ▶ A KNOWN TRAP THAT IS CURRENTLY LATENT, NOT LIVE. Read the whole note before acting on it: an +# earlier version of it described the trap as live and prescribed a workaround that is not needed +# today, and it was wrong for long enough that nobody noticed. +# +# The shape of it: the `ci-required-checks` ruleset on this repo requires several contexts with +# `bypass_actors: []`. THE NUMBER IS DELIBERATELY NOT WRITTEN HERE: it read `4` in this comment +# while the ruleset had `6`, because a count in a workflow comment has nothing to keep it honest, +# and a required context this text does not know about is exactly the thing the reader is here to +# satisfy. Derive it instead, which takes two seconds: # # gh api repos/cosyte/cli/rulesets/19907924 \ # --jq '[.rules[]|select(.type=="required_status_checks").parameters.required_status_checks[].context]' # -# Changesets opens the "Version Packages" PR as `github-actions[bot]` using the default -# `GITHUB_TOKEN`, and GitHub does not start workflow runs for events raised by that token, so that -# PR arrives with ZERO check runs. Required contexts that never arrive are PENDING, not failing, and -# with no bypass actors nobody, including an admin, can merge past them. The escape, done last and -# immediately before merging, is one commit onto `changeset-release/main`, which raises a normal +# IF the "Version Packages" PR is opened with the default `GITHUB_TOKEN`, GitHub does not start +# workflow runs for events raised by that token, so the PR arrives with ZERO check runs. Required +# contexts that never arrive are PENDING, not failing, and with no bypass actors nobody, including +# an admin, can merge past them. +# +# ▶ THAT `IF` IS NOT SATISFIED HERE, AND IT WAS ASSERTED AS FACT FOR MONTHS WITHOUT BEING CHECKED. +# The shared release workflow uses `secrets.RELEASE_PR_TOKEN || secrets.GITHUB_TOKEN`, +# `RELEASE_PR_TOKEN` is a live cosyte ORG secret, and this caller passes `secrets: inherit`, so the +# PR is authored with the PAT and the checks DO arrive. Measured on the last three version PRs +# (#24, #26, #31): each carries exactly ONE commit, authored by `github-actions[bot]`, and all four +# workflows started on that commit within about ten seconds of it, at `run_attempt=1`, with no +# human commit anywhere. Upstream even emits a `Version PR will land with zero checks` warning only +# when the secret is ABSENT. Verify before you assume it is still true: +# +# gh api "repos/cosyte/cli/actions/runs?head_sha=$(gh pr view --json headRefOid --jq .headRefOid)" \ +# --jq '.workflow_runs[]|"\(.name) \(.event) attempt=\(.run_attempt)"' +# +# So the trap returns only if `RELEASE_PR_TOKEN` lapses, is unset, or loses its scopes. In THAT +# case, and only then, the escape is one commit onto `changeset-release/main`, which raises a normal # `pull_request` event and produces every context: # # gh pr checkout && git commit --allow-empty -m "chore: run CI on the version PR" && git push # -# ▶ AND THAT ESCAPE GOT MORE EXPENSIVE ON 2026-08-06, WHICH IS WORTH KNOWING BEFORE YOU RUN IT. -# `ci / prepublish` is now required, and its second layer packs this tree and installs the tarball -# into a clean directory over the network. So the empty commit above no longer just re-runs static -# gates on the version PR: it runs a real install of the version about to be published, against the -# live registry, and a registry blip reds it. That is the intended cost. A version PR whose -# pre-publish probe cannot pass is a version that must not publish. +# Do not run that as routine. It is a fallback for a diagnosed no-checks PR, and pushing an +# unnecessary commit onto the branch that publishes a healthcare package is not free: since +# 2026-08-06 `ci / prepublish` is required, and its second layer packs this tree and installs the +# tarball into a clean directory over the live registry, so the push runs a real install of the +# version about to be published and a registry blip reds a REQUIRED context. When the PAT is +# working, that probe has already run on the bot's own commit and the extra commit buys nothing. # # Adding a bypass actor would also clear it and is DELIBERATELY REJECTED: it would let a human merge -# a red PR on a published healthcare package. The durable fix is to author the Version PR with a PAT -# in the shared `cosyte/.github` release workflow, which is tracked upstream, not here. +# a red PR on a published healthcare package. on: push: diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b5c864..8f6c0f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,15 +40,21 @@ still do. Each entry was assigned to the release whose tag first contains it, re scoped to splitting a step out of `verify` locally. The `uses:` reference is unpinned, so a job added upstream emits a new `ci / ` context here with no commit in this repo, and it always arrives unrequired. - - **`release.yml` was telling the next releaser to expect four contexts on the version PR, and - the ruleset had six.** That comment documents the escape from a real trap (a "Version Packages" - PR opened by `github-actions[bot]` arrives with zero check runs, and a required context that - never arrives is pending rather than failing, unmergeable by anyone), so a wrong count there - sends someone to declare the escape finished early. The count is **deleted rather than - corrected**: a number in a workflow comment has nothing to keep it honest, and the derivation - is now written in its place. The same comment records that the escape got more expensive, since - the empty commit it prescribes now runs a real pack-and-install of the version about to be - published. + - **`release.yml`'s trap note was wrong twice over, and the second error was the one that + mattered.** It told the next releaser to expect four required contexts on the "Version Packages" + PR while the ruleset had six, and the count is now **deleted rather than corrected**: a number in + a workflow comment has nothing to keep it honest, so the derivation is written in its place. + - **More seriously, it asserted that the version PR "arrives with ZERO check runs" and + prescribed pushing an empty commit as the escape. That is not true of this repo and has not + been for months.** The shared release workflow authors that PR with `RELEASE_PR_TOKEN`, a live + org secret this caller supplies through `secrets: inherit`, so the checks do arrive. Measured + on the last three version PRs: each carries exactly one bot-authored commit, and all four + workflows started on that commit within about ten seconds, at `run_attempt=1`, with no human + commit anywhere. The note now states the measured behaviour, keeps the empty commit only as a + diagnosed fallback for a genuinely check-less PR, and gives the command to tell the two apart. + - Getting this wrong was not free: an unnecessary push onto the branch that publishes now runs a + real pack-and-install of the version about to be released, so a registry blip would red a + **required** context. The note says so at the fallback rather than as general advice. - **The shipped documentation sidebar was off the canonical IA spine, and it was holding up the docs site's deploy (CLI-SIDEBAR-IA-NONCANONICAL).** `docs-content/sidebars.json` declared two diff --git a/documentation/agent-notes.md b/documentation/agent-notes.md index 9058920..e09439a 100644 --- a/documentation/agent-notes.md +++ b/documentation/agent-notes.md @@ -426,10 +426,16 @@ until `ci / prepublish` was added and it moves whenever the called workflow does **▶ A `ci / *` CONTEXT CAN APPEAR IN THIS REPO WITH NO COMMIT IN THIS REPO, AND IT ARRIVES NOT REQUIRED.** `ci.yml` calls `cosyte/.github/.github/workflows/ci.yml@main`, unpinned. A job added to that reusable workflow starts emitting `ci / ` on every PR here immediately, the ruleset does -not name it, and so **a red result from it shows a red X and the merge lands anyway** (this repo's -merge queue is `pull_request` with 0 approvals). Nothing errors, nothing warns, and no commit in this -repo records that the surface changed. Every previously written form of the "a required job gates all -of its steps" warning was scoped to a **local** split, so none of them covered this. +not name it, and so **a red result from it shows a red X and the merge lands anyway**. Nothing +errors, nothing warns, and no commit in this repo records that the surface changed. Every previously +written form of the "a required job gates all of its steps" warning was scoped to a **local** split, +so none of them covered this. + +(There is **no merge queue** on this repo. `gh api repos/cosyte/cli/branches/main/protection` returns +`404 Branch not protected` and the ruleset carries no `merge_queue` rule. What lets a red +non-required check merge is that the ruleset's `pull_request` rule sets +`required_approving_review_count: 0`. An earlier draft of this paragraph said "merge queue" and it +was wrong.) That is not hypothetical. The `prepublish` job was added upstream in `cosyte/.github#35` (`6142ac4`, 2026-08-05) and its second layer was defaulted on in `#36` (`90936ea`, the same day). **The census @@ -441,6 +447,17 @@ mistake (naming a context nothing emits) rather than a fix. **The order is load- run first, then be added, in that order.** It was read off the real check run on the pull request that shipped this section before the ruleset was written. +**▶ THE COST THAT ORDERING DOES NOT COVER, DISCLOSED BECAUSE IT WAS PAID: ADDING A REQUIRED CONTEXT +STRANDS EVERY OPEN PULL REQUEST WHOSE HEAD SHA ALREADY RAN.** The ordering protects PRs opened +_after_ the write, because their head shas produce the new context. It does nothing for a head sha +that ran before it existed: that PR now needs a context nothing will ever post for it, so it goes +`BLOCKED` and stays there. Measured immediately after this write: `#33` (`f69ab63a`) went +`mergeStateStatus: BLOCKED` carrying all seven older contexts green and no `ci / prepublish`, and +`#29` (`95510b9d`) is the same shape. **The remedy is one push to each affected branch**, which +re-runs CI and produces the context; it is not a ruleset problem and must not be fixed by removing +the requirement. Check for this before the next such write: +`gh pr list --state open --json number,mergeStateStatus`. + **What it gates, and why leaving it unrequired was the expensive kind of hole.** `prepublish` runs two layers: an offline **manifest lint** that refuses a dependency specifier no registry can resolve, and a **pack-and-install** probe that `npm pack`s this tree and installs the tarball into a clean From 9d2d4c1813d5f89bd764c3200d794f6233f84b51 Mon Sep 17 00:00:00 2001 From: Noah Schatz Date: Thu, 6 Aug 2026 12:14:17 +0000 Subject: [PATCH 4/5] fix(ci): cut the release.yml trap note out of this slice, and fix four minors Refuter pass 2: REFUTED, five INTRODUCED findings. Taking the shape recommendation rather than rewriting the same note a third time. THE CUT. `.github/workflows/release.yml` is restored to its state on d01696d. Its version-PR trap note has now been rewritten twice inside this one slice and produced a fresh false claim each time. The second attempt said the false sentence had stood "for months" and had been "asserted as fact without being checked". Both are wrong: it entered on 2026-07-28 in 62fba77, so it stood 9 days, and it was true when written and exercised the next day. Version PR #21 (2026-07-29) carries 7373b7cb, bot-authored with zero workflow runs, followed by 23e1fa6c by Noah, "chore: run CI on the version PR", which is that note's own escape command. So it was wrong for about six days, and it was checked by being used. The rewrite deleted the dated instance that proves the fallback works, which was the real cost. That note needs its own item, with its own measurement of when the PAT arrived and which routes still lead back to a check-less PR. Nothing here makes it worse: it is byte-identical to base. What ships is the part that has survived both passes: the census, the ordering, the required-context table, the ci.yml banner, and the ruleset write itself. FOUR MINORS, fixed where the text survives. 1. The stranded set was wrong. Measured per head sha rather than read off mergeStateStatus: #33, #18 and #16 were stranded by this write, each carrying the six older required contexts green. #29 was already unmergeable on a red `ci / verify` on both matrix legs, and #15 predates `no-emdash` and `no-internal-refs` and was already stranded. Neither is this slice's cost. The table now shows the attribution and says to measure it this way. 2. `required_approving_review_count: 0` was given as the reason a red X merges. It is not: a context absent from `required_status_checks` blocks nothing at any approval count, because the ruleset only evaluates contexts it names. The approval count is why no review is needed, a different question. 3. "Declares no `@cosyte/fhir` in any form" was false. It is declared, as a `file:` devDependency on the vendored tarball, which is what lets this repo's FHIR tests run. The operative fact is that `prepublish-check.mjs` does not read devDependencies at all, so the allowance names something that is not a consumer dependency here. 4. The trap's return routes were stated as if exhaustive; they are not, and `persist-credentials` at its default is one more. That text is gone with the cut. --- .github/workflows/ci.yml | 19 ++++++++----- .github/workflows/release.yml | 51 +++++++---------------------------- CHANGELOG.md | 21 +++++---------- documentation/agent-notes.md | 38 ++++++++++++++++++-------- 4 files changed, 55 insertions(+), 74 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 393ecf5..0438cfd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,13 +41,18 @@ jobs: # on the registry uninstallable forever, and this job is what would have refused them. # # ONE DEFAULT IS PERMANENTLY STALE HERE, AND THE OBVIOUS "FIX" FOR IT IS FORBIDDEN. The upstream - # default for `expect-unpublished-deps` is `@cosyte/fhir=blocked`, and this package deliberately - # declares no `@cosyte/fhir` in any form, so the allowance names a package that is not a consumer - # dependency of this one and the gate reports it as stale on every run. That notice never fails the - # job, and it must NOT be silenced by adding `@cosyte/fhir` to the manifest: declaring it in any - # form alongside `@cosyte/transform` fails the whole install with `ERESOLVE`, which is the measured - # reason it is absent. See CLAUDE.md. If the notice is ever worth silencing, the way is to pass an - # empty `expect-unpublished-deps` from this caller, not to touch dependencies. + # default for `expect-unpublished-deps` is `@cosyte/fhir=blocked`. Be precise about why that is + # stale, because the short version is false: `@cosyte/fhir` IS declared in this manifest, as + # `devDependencies: {"@cosyte/fhir": "file:vendor/cosyte-fhir-0.0.0.tgz"}`, which is what lets this + # repo's own FHIR tests run. The operative fact is that `prepublish-check.mjs` does not read + # `devDependencies` at all (a `file:` specifier is fine there, since no consumer installs them), so + # the allowance names a package that is not a CONSUMER dependency of this one and is reported stale + # on every run. That notice never fails the job. + # + # It must NOT be silenced by promoting `@cosyte/fhir` into `dependencies` or `optionalDependencies`: + # declaring it in either, alongside `@cosyte/transform`, fails the whole install with `ERESOLVE`, + # which is the measured reason it is not there. See CLAUDE.md. If the notice is ever worth + # silencing, the way is to pass an empty `expect-unpublished-deps` from this caller. # # Universal gates from the shared pipeline (typecheck, lint, format, test, coverage, build, attw, # dual ESM/CJS smoke, actionlint) on the standard Node matrix. Repo-specific extra jobs (e.g. an diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b35ad03..943bae7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,51 +4,20 @@ name: Release # this repo is public) -> docs artifacts -> GitHub release -> repository_dispatch to cosyte/docs. # Requires the NPM_TOKEN and DOCS_REPO_DISPATCH_TOKEN secrets (passed via `secrets: inherit`). # -# ▶ A KNOWN TRAP THAT IS CURRENTLY LATENT, NOT LIVE. Read the whole note before acting on it: an -# earlier version of it described the trap as live and prescribed a workaround that is not needed -# today, and it was wrong for long enough that nobody noticed. -# -# The shape of it: the `ci-required-checks` ruleset on this repo requires several contexts with -# `bypass_actors: []`. THE NUMBER IS DELIBERATELY NOT WRITTEN HERE: it read `4` in this comment -# while the ruleset had `6`, because a count in a workflow comment has nothing to keep it honest, -# and a required context this text does not know about is exactly the thing the reader is here to -# satisfy. Derive it instead, which takes two seconds: -# -# gh api repos/cosyte/cli/rulesets/19907924 \ -# --jq '[.rules[]|select(.type=="required_status_checks").parameters.required_status_checks[].context]' -# -# IF the "Version Packages" PR is opened with the default `GITHUB_TOKEN`, GitHub does not start -# workflow runs for events raised by that token, so the PR arrives with ZERO check runs. Required -# contexts that never arrive are PENDING, not failing, and with no bypass actors nobody, including -# an admin, can merge past them. -# -# ▶ THAT `IF` IS NOT SATISFIED HERE, AND IT WAS ASSERTED AS FACT FOR MONTHS WITHOUT BEING CHECKED. -# The shared release workflow uses `secrets.RELEASE_PR_TOKEN || secrets.GITHUB_TOKEN`, -# `RELEASE_PR_TOKEN` is a live cosyte ORG secret, and this caller passes `secrets: inherit`, so the -# PR is authored with the PAT and the checks DO arrive. Measured on the last three version PRs -# (#24, #26, #31): each carries exactly ONE commit, authored by `github-actions[bot]`, and all four -# workflows started on that commit within about ten seconds of it, at `run_attempt=1`, with no -# human commit anywhere. Upstream even emits a `Version PR will land with zero checks` warning only -# when the secret is ABSENT. Verify before you assume it is still true: -# -# gh api "repos/cosyte/cli/actions/runs?head_sha=$(gh pr view --json headRefOid --jq .headRefOid)" \ -# --jq '.workflow_runs[]|"\(.name) \(.event) attempt=\(.run_attempt)"' -# -# So the trap returns only if `RELEASE_PR_TOKEN` lapses, is unset, or loses its scopes. In THAT -# case, and only then, the escape is one commit onto `changeset-release/main`, which raises a normal -# `pull_request` event and produces every context: +# ▶ KNOWN TRAP, documented rather than bypassed. The `ci-required-checks` ruleset on this repo +# requires four contexts with `bypass_actors: []`. Changesets opens the "Version Packages" PR as +# `github-actions[bot]` using the default `GITHUB_TOKEN`, and GitHub does not start workflow runs +# for events raised by that token, so that PR arrives with ZERO check runs. Four required contexts +# that never arrive are PENDING, not failing, and with no bypass actors nobody, including an admin, +# can merge past them. The escape, done last and immediately before merging, is one commit onto +# `changeset-release/main`, which raises a normal `pull_request` event and produces all four +# contexts: # # gh pr checkout && git commit --allow-empty -m "chore: run CI on the version PR" && git push # -# Do not run that as routine. It is a fallback for a diagnosed no-checks PR, and pushing an -# unnecessary commit onto the branch that publishes a healthcare package is not free: since -# 2026-08-06 `ci / prepublish` is required, and its second layer packs this tree and installs the -# tarball into a clean directory over the live registry, so the push runs a real install of the -# version about to be published and a registry blip reds a REQUIRED context. When the PAT is -# working, that probe has already run on the bot's own commit and the extra commit buys nothing. -# # Adding a bypass actor would also clear it and is DELIBERATELY REJECTED: it would let a human merge -# a red PR on a published healthcare package. +# a red PR on a published healthcare package. The durable fix is to author the Version PR with a PAT +# in the shared `cosyte/.github` release workflow, which is tracked upstream, not here. on: push: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f6c0f1..8173a0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,21 +40,12 @@ still do. Each entry was assigned to the release whose tag first contains it, re scoped to splitting a step out of `verify` locally. The `uses:` reference is unpinned, so a job added upstream emits a new `ci / ` context here with no commit in this repo, and it always arrives unrequired. - - **`release.yml`'s trap note was wrong twice over, and the second error was the one that - mattered.** It told the next releaser to expect four required contexts on the "Version Packages" - PR while the ruleset had six, and the count is now **deleted rather than corrected**: a number in - a workflow comment has nothing to keep it honest, so the derivation is written in its place. - - **More seriously, it asserted that the version PR "arrives with ZERO check runs" and - prescribed pushing an empty commit as the escape. That is not true of this repo and has not - been for months.** The shared release workflow authors that PR with `RELEASE_PR_TOKEN`, a live - org secret this caller supplies through `secrets: inherit`, so the checks do arrive. Measured - on the last three version PRs: each carries exactly one bot-authored commit, and all four - workflows started on that commit within about ten seconds, at `run_attempt=1`, with no human - commit anywhere. The note now states the measured behaviour, keeps the empty commit only as a - diagnosed fallback for a genuinely check-less PR, and gives the command to tell the two apart. - - Getting this wrong was not free: an unnecessary push onto the branch that publishes now runs a - real pack-and-install of the version about to be released, so a registry blip would red a - **required** context. The note says so at the fallback rather than as general advice. + - **Deliberately NOT in this change: `release.yml`'s version-PR trap note.** It carries a stale + required-context count and a claim about the "Version Packages" PR arriving with zero check runs + whose truth depends on whether `RELEASE_PR_TOKEN` is authoring that PR. Two attempts to correct + it inside this change each produced a fresh false claim about its history, so it is cut out to + its own change rather than rewritten a third time. Nothing about it is made worse here; it is + left exactly as it was. - **The shipped documentation sidebar was off the canonical IA spine, and it was holding up the docs site's deploy (CLI-SIDEBAR-IA-NONCANONICAL).** `docs-content/sidebars.json` declared two diff --git a/documentation/agent-notes.md b/documentation/agent-notes.md index e09439a..4177518 100644 --- a/documentation/agent-notes.md +++ b/documentation/agent-notes.md @@ -431,11 +431,13 @@ errors, nothing warns, and no commit in this repo records that the surface chang written form of the "a required job gates all of its steps" warning was scoped to a **local** split, so none of them covered this. -(There is **no merge queue** on this repo. `gh api repos/cosyte/cli/branches/main/protection` returns -`404 Branch not protected` and the ruleset carries no `merge_queue` rule. What lets a red -non-required check merge is that the ruleset's `pull_request` rule sets -`required_approving_review_count: 0`. An earlier draft of this paragraph said "merge queue" and it -was wrong.) +(Two corrections this paragraph has already needed, kept visible because both are easy to write +again. There is **no merge queue** on this repo: `gh api repos/cosyte/cli/branches/main/protection` +returns `404 Branch not protected` and the ruleset carries no `merge_queue` rule. And +`required_approving_review_count: 0` is **not** the reason a red non-required check merges: a context +absent from `required_status_checks` blocks nothing at **any** approval count, because the ruleset +only ever evaluates the contexts it names. The approval count is why the merge needs no **review**, +which is a different question and does not bear on this one.) That is not hypothetical. The `prepublish` job was added upstream in `cosyte/.github#35` (`6142ac4`, 2026-08-05) and its second layer was defaulted on in `#36` (`90936ea`, the same day). **The census @@ -451,12 +453,26 @@ that shipped this section before the ruleset was written. STRANDS EVERY OPEN PULL REQUEST WHOSE HEAD SHA ALREADY RAN.** The ordering protects PRs opened _after_ the write, because their head shas produce the new context. It does nothing for a head sha that ran before it existed: that PR now needs a context nothing will ever post for it, so it goes -`BLOCKED` and stays there. Measured immediately after this write: `#33` (`f69ab63a`) went -`mergeStateStatus: BLOCKED` carrying all seven older contexts green and no `ci / prepublish`, and -`#29` (`95510b9d`) is the same shape. **The remedy is one push to each affected branch**, which -re-runs CI and produces the context; it is not a ruleset problem and must not be fixed by removing -the requirement. Check for this before the next such write: -`gh pr list --state open --json number,mergeStateStatus`. +`BLOCKED` and stays there. + +**Attribute this by measuring each head sha, not by listing what is open.** Of the five open PRs at +the time of this write, exactly **three** were stranded by it, and the first draft of this paragraph +named the wrong set by reading `mergeStateStatus` instead of the check runs: + +| PR | head sha | state before the write | stranded by this write? | +| --- | ---------- | ------------------------------------------------------- | ----------------------- | +| #33 | `f69ab63a` | six older required contexts green | **yes** | +| #18 | `73758565` | six older required contexts green | **yes** | +| #16 | `6cc21d8a` | six older required contexts green | **yes** | +| #29 | `95510b9d` | `ci / verify` **red on both matrix legs** | no, already unmergeable | +| #15 | `b63cd115` | no `no-emdash`, no `no-internal-refs` (predates both) | no, already stranded | + +All three affected PRs are Dependabot's, and Dependabot regenerates its branches, so **nothing was +pushed to them**: a push onto a branch this slice does not own, to clear a condition this slice +created, is the more intrusive fix. **The remedy, when it is yours to apply, is one push per +branch**, which re-runs CI and produces the context. It is not a ruleset problem and must not be +fixed by removing the requirement. Check before the next such write: +`gh api repos/cosyte/cli/commits//check-runs`, per open PR. **What it gates, and why leaving it unrequired was the expensive kind of hole.** `prepublish` runs two layers: an offline **manifest lint** that refuses a dependency specifier no registry can resolve, From e900ed74c566b2c91c80484c982ecb8f6ee07257 Mon Sep 17 00:00:00 2001 From: Noah Schatz Date: Thu, 6 Aug 2026 12:20:08 +0000 Subject: [PATCH 5/5] docs(ci): record the one time a required check actually blocked this slice Pass 3: NOT REFUTED, all five pass-2 findings cleared. Two nits it recorded rather than raised, both fixed here, plus the demonstration this subject was missing. THE DEMONSTRATION. Everything in the branch-protection notes is about a check that reports without blocking. While shipping exactly that fix, the first thing a required context actually blocked was this author's own prose: editing the pull request body turned `no-emdash` red, twice, on a required context, over four U+2014 characters typed into the body. The PR went BLOCKED and stayed unmergeable until the body was rewritten. It is worth recording because it landed on the half of the gate nothing local can see. `scripts/check-no-emdash.sh` scans tracked files and was green throughout, in the pre-commit hook and in verify.sh, and the branch diff carried zero U+2014. The text was never in a file. The PR body exists only on GitHub and is reached only by the `edited` trigger, so no local run of anything here could have caught it. The standing note that the PR body lands under none of the three merge methods is still true; this is what that deliberate over-strictness buys, observed rather than argued. THE TWO NITS. The CHANGELOG said two attempts each produced a fresh false claim about the note's history; only the second did, the first produced a false operational premise. And the stranded-PR table said five PRs were open when six were, the sixth being the PR performing the write. --- CHANGELOG.md | 6 +++--- documentation/agent-notes.md | 34 +++++++++++++++++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8173a0b..cc763eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,9 +43,9 @@ still do. Each entry was assigned to the release whose tag first contains it, re - **Deliberately NOT in this change: `release.yml`'s version-PR trap note.** It carries a stale required-context count and a claim about the "Version Packages" PR arriving with zero check runs whose truth depends on whether `RELEASE_PR_TOKEN` is authoring that PR. Two attempts to correct - it inside this change each produced a fresh false claim about its history, so it is cut out to - its own change rather than rewritten a third time. Nothing about it is made worse here; it is - left exactly as it was. + it inside this change each produced a fresh false claim: the first a false operational premise, + the second a false claim about the note's own history. So it is cut out to its own change rather + than rewritten a third time. Nothing about it is made worse here; it is left exactly as it was. - **The shipped documentation sidebar was off the canonical IA spine, and it was holding up the docs site's deploy (CLI-SIDEBAR-IA-NONCANONICAL).** `docs-content/sidebars.json` declared two diff --git a/documentation/agent-notes.md b/documentation/agent-notes.md index 4177518..3a7895c 100644 --- a/documentation/agent-notes.md +++ b/documentation/agent-notes.md @@ -455,9 +455,10 @@ _after_ the write, because their head shas produce the new context. It does noth that ran before it existed: that PR now needs a context nothing will ever post for it, so it goes `BLOCKED` and stays there. -**Attribute this by measuring each head sha, not by listing what is open.** Of the five open PRs at -the time of this write, exactly **three** were stranded by it, and the first draft of this paragraph -named the wrong set by reading `mergeStateStatus` instead of the check runs: +**Attribute this by measuring each head sha, not by listing what is open.** Six pull requests were +open at the time of this write, one of them being the PR performing it; of the other five, exactly +**three** were stranded by it, and the first draft of this paragraph named the wrong set by reading +`mergeStateStatus` instead of the check runs: | PR | head sha | state before the write | stranded by this write? | | --- | ---------- | ------------------------------------------------------- | ----------------------- | @@ -490,6 +491,33 @@ has no registry-outage softening, unlike the pack layer's `inconclusive` verdict here anyway, because an advisory pre-publish gate on the branch that publishes is the exact shape of "a green check that cannot block a merge". +### The one time a required check actually blocked something in this slice, and what it caught + +**▶ THE DEMONSTRATION THIS WHOLE SUBJECT HAD BEEN MISSING, AND IT LANDED ON THE AUTHOR.** Everything +above is about a check that reports without blocking. While shipping it, the first thing a required +context actually blocked was **this author's own prose**: editing the pull request body turned +`no-emdash` **red**, twice, on a **required** context, over four `U+2014` characters that had been +typed into the PR body's explanatory sections. The pull request went `mergeStateStatus: BLOCKED` and +stayed unmergeable until the body was rewritten (runs at `12:14:31Z` and `12:14:59Z` failed; +`12:16:34Z` passed). + +**Why it is worth a section rather than a footnote.** It is the concrete counter-example to the +failure this section documents. A red X that does not block a merge is documentation; this was a red +X that stopped a merge dead, and the difference between the two is exactly one line in a ruleset. + +**And it landed on the half of the gate that nothing local can see.** `scripts/check-no-emdash.sh` +scans **tracked files** and was green throughout, both in the pre-commit hook and in `verify.sh`; +`git diff` over the branch carried zero `U+2014`. The offending text was never in a file. The PR +body is a surface that exists only on GitHub, reached only by `no-emdash.yml`'s `edited` trigger, and +**no local run of anything in this repo could have caught it**. So the two halves of that gate are not +redundant: the tracked-file half is the one a worker exercises constantly and the PR-text half is the +one that catches what a worker writes *about* the work. + +**Read alongside the standing note that the PR body lands under none of the three merge methods.** +That is still true, and the gate scans it anyway as deliberate over-strictness. This is what that +over-strictness buys, observed rather than argued: without it, four em dashes would have gone onto a +public pull request on a public repository, and the ban is absolute. + **Not built, and it must not be built without answering one question first.** A gate inside CI that `curl`s this repo's own ruleset and asserts the required set would close the observability gap named at the end of this section. Anonymous GitHub API is **60 requests per hour, per IP, and hosted