Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* Source: https://github.com/modelcontextprotocol/modelcontextprotocol
* Pulled from: https://raw.githubusercontent.com/modelcontextprotocol/modelcontextprotocol/main/schema/draft/schema.ts
* Last updated from commit: f68d864a813754e188c6df52dcc5772a12f96c63
* Last updated from commit: 93671a3f2bac3bc11b0eb6327c2d029e272b2871

Check warning on line 6 in packages/core-internal/src/types/spec.types.2026-07-28.ts

View check run for this annotation

Claude / Claude Code Review

Nightly workflow's 'gh pr edit' refreshes only --body, so this PR's title permanently references the pre-split filename spec.types.ts

The nightly workflow refreshes this PR with `gh pr edit $EXISTING_PR --body ...` (no `--title`), so this PR — created before #2252 split the spec snapshot into per-revision files — is permanently titled "chore: update spec.types.ts from upstream", referencing a file that no longer exists anywhere in the tree, even though the body and HEAD commit message already use the new spec.types.2026-07-28.ts name. One-line fix in .github/workflows/update-spec-types.yml: pass the same `--title` on the edit

Check notice on line 6 in packages/core-internal/src/types/spec.types.2026-07-28.ts

View check run for this annotation

Claude / Claude Code Review

No rollover guard: after 2026-07-28 publishes upstream, the nightly cron will stamp next-revision draft content into spec.types.2026-07-28.ts and force-push it into this PR

Pre-existing pipeline gap, time-bound (the 2026-07-28 publication date is 18 days out): the 2026-07-28 anchor is draft-tracked (`UPSTREAM_SCHEMA_DIRS` maps it to `schema/draft/`, no `RELEASED_REVISION_PINS` entry) and `fetch-spec-types.ts` writes fetched bytes verbatim with no check that the content matches the revision label — so once upstream publishes `schema/2026-07-28/` and `schema/draft/` rolls over to the next revision's working directory, the nightly cron will silently stamp next-revisio

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The two prior comments covering this exact situation (#3485357818's rule-4 remediation and #3487174588's content-noop verification) were written against the previous re-pull (2ffc3fa2), but this PR now records upstream commit 93671a3f in the anchor header — so following the earlier comment's command verbatim (pnpm fetch:schema-twins 2ffc3fa2...) would stamp the manifests with a third commit and recreate the rule-4 anchor↔manifest provenance skew. Any twin/fixtures manifest refresh landed with this PR must pin 93671a3f (or whatever the anchor records at merge time) instead; re-verified against the new head, the upstream delta between f68d864a and 93671a3f is formatting-only (schema.json hash unchanged, no examples touched), so the close-as-no-op / land-with-manifest-refresh options from the prior comments remain valid.

Extended reasoning...

What this corrects

This re-pull moves the anchor's provenance header (packages/core-internal/src/types/spec.types.2026-07-28.ts:6) from f68d864a813754e188c6df52dcc5772a12f96c63 to 93671a3f2bac3bc11b0eb6327c2d029e272b2871. The two most recent review comments covering exactly this situation were written against the previous re-pull (2ffc3fa2d00e637ae7c3d1438aaa0561ddfda184) and are now stale in ways that matter for whoever acts on them:

  1. #3485357818 (rule-4 anchor↔manifest skew) instructs: "run pnpm fetch:schema-twins 2ffc3fa2d00e637ae7c3d1438aaa0561ddfda184 … so both manifests record the same commit as the anchor." Following that command verbatim against the current head would stamp packages/core-internal/test/corpus/schema-twins/manifest.json (and, via fetch:spec-examples, test/corpus/fixtures/2026-07-28/manifest.json) with 2ffc3fa2 while the anchor header says 93671a3f — recreating the very anchor↔derived-artifact provenance disagreement that lifecycle rule 4 (packages/core-internal/src/types/README.md, marked OPERATIVE) forbids, just at a different pair of commits.
  2. #3487174588 (verified content-noop) based its close-as-no-op recommendation on upstream schema/draft/schema.ts and schema.json being byte-identical between f68d864a and 2ffc3fa2. That specific verification does not automatically carry over to the new head.

Re-verification against the new head (93671a3f)

  • Between f68d864a and 93671a3f, upstream schema/draft/schema.ts does change (96,322 → 96,256 bytes, 63 diff lines) — but every changed hunk is pure union-type re-wrapping (multi-line | A | B | C; collapsed to a single line) with zero semantic or JSDoc change. Prettier in scripts/fetch-spec-types.ts normalizes this away, which is why the regenerated anchor body is byte-identical and this PR's diff is still header-only.
  • schema/draft/schema.json at 93671a3f is unchanged: 178,613 bytes, sha256 14398c3dd2c66b9c3f6661fc7a7eaa24174952ed1598d0b7f011b686ba5c4c83, exactly matching the hash pinned for the 2026-07-28 twin in schema-twins/manifest.json — so the vendored twin oracle remains content-current.
  • The upstream compare f68d864a…93671a3f (16 commits, 27 files) touches no file under schema/draft/examples, so the fixtures corpus at test/corpus/fixtures/2026-07-28/ is also content-current.

Concrete step-by-step consequence of following the stale command

  1. A maintainer decides to satisfy rule 4 by landing a manifest refresh with this PR and copies the command from #3485357818: pnpm fetch:schema-twins 2ffc3fa2d00e637ae7c3d1438aaa0561ddfda184.
  2. Both manifests now record source.commit = 2ffc3fa2.
  3. The anchor header on this PR's head says 93671a3f.
  4. main after merge is again in the state rule 4 forbids — anchor and derived twins disagreeing on their upstream commit — only now across three different SHAs (f68d864a in history, 2ffc3fa2 in the manifests, 93671a3f in the anchor). Nothing in CI catches it: schemaTwinConformance.test.ts hashes only twin bytes and never cross-checks source.commit against the anchor header.

What remains valid, and the recurring-churn point

The prior comments' bottom-line options remain valid for the current head — the content is verified unchanged, so the PR can either be closed as a no-op or landed together with a 2026-07-28-only twin/fixtures manifest refresh, pinned to 93671a3f (or whatever commit the anchor records at merge time), not the SHA quoted in the earlier comment. This is now the second consecutive header-only refresh PR (f68d864a → 2ffc3fa2 → 93671a3f), which reinforces the already-suggested workflow fixes: make the drift check ignore the provenance line (git diff -I 'Last updated from commit:') or have fetch-spec-types.ts skip the write when the regenerated body is unchanged, so content-neutral upstream touches of schema/draft/schema.ts stop generating these PRs.

Why nit

The PR is already held and both prior findings are already on record; nothing newly breaks. This comment corrects the SHA in the porting/remediation instructions and re-establishes the content-noop verification against the new revision so the maintainer acting on the earlier comments doesn't recreate the provenance skew — the same stale-checklist-correction tier already established on this PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The nightly workflow that maintains this PR rebuilds its head branch from scratch on every drift detection (git checkout -B update-spec-types from a fresh checkout of main, commit only the regenerated anchor, then git push -f --no-verify), so any other commit pushed to this branch — in particular the twin/fixtures manifest refresh that #3485357818 and the 2026-07-05 SHA-correction prescribe landing in this PR to fix the rule-4 anchor↔manifest skew — is silently wiped at the next 04:00 UTC run, with no CI signal that the remediation regressed. This is pre-existing behavior in .github/workflows/update-spec-types.yml (untouched by this diff), but it invalidates the accepted in-PR remediation plan: either the workflow should preserve/rebase existing branch commits (or regenerate the twins/manifests itself with per-revision pinning), or the manifest refresh must be merged the same day / with the cron paused.

Extended reasoning...

What the bug is

The nightly cron in .github/workflows/update-spec-types.yml does not update the existing update-spec-types branch — it recreates it from scratch every time drift is detected. The "Create Pull Request" step (lines 72–75) runs on a fresh actions/checkout of main (the remote branch is never fetched), then:

git checkout -B update-spec-types      # recreate branch from main HEAD + regenerated working tree
git add packages/core-internal/src/types/spec.types.2026-07-28.ts   # ONLY the anchor file
git commit -m "chore: update spec.types.2026-07-28.ts from upstream"
git push -f --no-verify origin update-spec-types

Standard git semantics: the branch is rebuilt as main + exactly one commit containing exactly one file, and the force-push discards any other commit present on the remote PR branch — companion fixes, test carve-outs, and specifically the rule-4 manifest refresh that the standing review comments direct maintainers to land here.

Why the trigger is guaranteed for the life of this PR

The drift gate (line 53) runs git diff --quiet on the regenerated anchor against main's copy. While this PR is unmerged, main's anchor header still records f68d864a, and the regenerated file records the latest upstream SHA (currently 93671a3f) — so the header line differs every night (even when upstream content is byte-identical, per the header-only-churn finding in #3487174588), has_changes=true fires, and the branch is force-recreated. This isn't hypothetical: this PR's own head has already been rewritten repeatedly (f68d864a2ffc3fa293671a3f) by exactly this mechanism, invalidating review state each time.

Step-by-step failure scenario

  1. A maintainer follows #3485357818 (with the corrected SHA from the 2026-07-05 follow-up) and pushes a commit to update-spec-types refreshing packages/core-internal/test/corpus/schema-twins/manifest.json and test/corpus/fixtures/2026-07-28/manifest.json to pin 93671a3f. Rule 4 is now satisfied on the PR branch — anchor and manifests agree.
  2. At the next 04:00 UTC run, the workflow checks out main, regenerates the anchor, and the drift check fails (main's copy still says f68d864a), so has_changes=true.
  3. git checkout -B update-spec-types recreates the branch from main + the single anchor commit; git push -f overwrites the remote. The maintainer's manifest commit vanishes with no notification.
  4. Nothing in CI catches the regression: schemaTwinConformance.test.ts only hashes twin bytes against the manifest's own sha256 entries and never cross-checks manifest.source.commit against the anchor header (a grep of the test tree for Last updated from commit returns only scripts/fetch-spec-types.ts). The PR later merges in exactly the rule-4-violating anchor↔manifest provenance-skew state the remediation was supposed to prevent.

Why existing comments don't cover this

#3485357818 asks for the manifest refresh to land in this PR and separately suggests the cron should regenerate the twins alongside the anchor; #3487174588 covers the header-only drift churn and the fetch scripts' single-SHA/path-filter defects; the 2026-07-05 comment corrects the target SHA to 93671a3f. All of them implicitly assume a commit pushed to this branch would stick — none observes that the workflow's branch-recreation + force-push makes any human commit on this branch non-durable across nightly runs, which is the piece that defeats the prescribed remediation path.

How to fix

Options, roughly in order of robustness: (a) have the workflow fetch the existing remote branch, regenerate the anchor on top of it, and push only if the regenerated anchor differs from the branch's copy (rather than recreating from main) — preserving extra commits; (b) have the workflow itself run fetch:schema-twins / fetch:spec-examples with the same SHA and git add the manifests+twins so the branch is always rule-4-consistent without human commits (dovetails with #3485357818's longer-term suggestion, but must use per-revision pinning per #3487174588's caveats about the fetch scripts); or (c) at minimum, document that the manifest refresh must be merged the same day as it's pushed, or with the cron paused. Option (b) combined with a header-aware drift check (git diff -I 'Last updated from commit:') resolves both this finding and the recurring header-only churn.

Severity

Filed as a nit: the defective mechanism lives in the pre-existing workflow file, which this diff does not touch, and merging this one-line sync breaks nothing by itself. But it materially corrects the remediation plan two standing review comments prescribe, so it belongs on record as companion/process scoping alongside the other accepted workflow findings on this PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 Two pre-existing gaps in the fetch-spec-types.ts pipeline that stamps this provenance line (neither introduced by this PR): (1) fetchLatestSHA() resolves the draft anchor's commit via an unauthenticated api.github.com call (60 req/hr shared per runner-IP) and the nightly workflow passes no GITHUB_TOKEN to that step, so intermittent 403 rate-limits fail the job before the drift check and silently skip that night's refresh; (2) the sibling frozen anchor spec.types.2025-11-25.ts:6 records commit 357adac4 while RELEASED_REVISION_PINS pins 0168c57f, so the documented no-arg 'pnpm run fetch:spec-types' produces an unexpected header-only dirty diff on the frozen anchor, contrary to README lifecycle rule 1. Fix by sending 'Authorization: Bearer $GITHUB_TOKEN' when set (and exposing github.token to the fetch step), and by realigning the 2025-11-25 header/pin (content at both commits is verified byte-identical, so a header-only commit suffices).

Extended reasoning...

Gap 1 — unauthenticated SHA lookup silently skips nightly refreshes

fetchLatestSHA() (scripts/fetch-spec-types.ts:51-65) resolves the floating draft anchor's target commit — the SHA recorded on the header line this PR changes — via a bare fetch() to https://api.github.com/repos/modelcontextprotocol/modelcontextprotocol/commits?path=schema/draft/schema.ts, with no Authorization header (a repo-wide grep for Authorization/GITHUB_TOKEN under scripts/ returns nothing). The nightly workflow's "Fetch latest spec types" step (.github/workflows/update-spec-types.yml:47-48) sets no env: block, so GITHUB_TOKEN is never available to the script even if it tried — only the later "Create Pull Request" step gets GH_TOKEN (line 64).

Why it fails: unauthenticated GitHub REST calls are limited to 60 requests/hour per origin IP, and GitHub-hosted runners share a small egress IP pool across all tenants, so the quota is routinely exhausted by other workloads on the same IP. When that happens fetchLatestSHA() gets HTTP 403, throws Failed to fetch commit info: 403, and main()'s catch does process.exit(1) — the job fails before the "Check for changes" step, so no refresh PR is opened or updated that night. This failure class was reproduced during review: an unauthenticated api.github.com call from a shared runner IP returned "API rate limit exceeded". Note the subsequent content fetch (raw.githubusercontent.com, fetch-spec-types.ts:68) is not subject to this API quota — the SHA lookup is the only fragile call in the pipeline.

Impact: fail-safe but silent staleness — no wrong content is ever written, but the nightly refresh is skipped, and cron-failure notifications go only to whoever last touched the workflow file, so repeated skips can go unnoticed while the draft anchor drifts behind upstream (contrary to lifecycle rule 2 in packages/core-internal/src/types/README.md, which says the anchor floats via refresh PRs). Contributors running pnpm run fetch:spec-types from shared/corporate/CI egress IPs hit the same 403s.

Step-by-step proof:

  1. Nightly cron fires; the runner's shared egress IP has already burned its 60 unauthenticated requests/hour on other tenants' jobs.
  2. fetchLatestSHA() issues fetch('https://api.github.com/repos/.../commits?path=schema/draft/schema.ts') with no auth header → HTTP 403 API rate limit exceeded.
  3. response.ok is false → throw new Error('Failed to fetch commit info: 403 ...').
  4. main()'s catch logs and process.exit(1) → the step fails; "Check for changes" and "Create Pull Request" never run.
  5. No refresh PR that night; the failure email goes only to the last workflow-file editor. Next successful night self-heals — but nothing surfaces the skipped nights.

Fix: in fetchLatestSHA() (and the identical pattern in fetch-spec-examples.ts's fallback, plus fetch-schema-twins.ts), send Authorization: Bearer ${process.env.GITHUB_TOKEN} when set — authenticated Actions calls get 1,000 req/hr/repo — and add env: { GITHUB_TOKEN: ${{ github.token }} } to the "Fetch latest spec types" step; optionally retry with backoff on 403. This folds naturally into the workflow hardening already proposed in #3485357818/#3487174588.

Gap 2 — frozen 2025-11-25 anchor header disagrees with its governing pin

packages/core-internal/src/types/spec.types.2025-11-25.ts:6 records Last updated from commit: 357adac47ab2654b64799f994e6db8d3df4ee19d, but scripts/fetch-spec-types.ts:43-45 pins RELEASED_REVISION_PINS['2025-11-25'] = '0168c57fc74aba6e6dcf8f0b7191db3caaa5ad65'. README lifecycle rule 1 says released anchors regenerate only from the pinned commit "so the anchor and the automation that maintains it can never disagree about the source of truth" — yet on main today they already disagree.

Upstream schema/2025-11-25/schema.ts was fetched at both SHAs during verification: byte-identical (66,611 bytes each, empty diff), so the divergence is provenance-only — the anchor content is correct. But the consequence is real: updateSpecTypes() uses the pin whenever no explicit SHA is given, and main() with no args iterates all SUPPORTED_VERSIONS, so both the documented no-arg pnpm run fetch:spec-types and the per-file instruction printed in the frozen anchor's own header (pnpm run fetch:spec-types 2025-11-25) regenerate the frozen anchor and flip its header 357adac4 → 0168c57f — an unexpected dirty diff on a file the lifecycle policy says only changes through a deliberate, reviewed repin. A contributor doing an unrelated draft refresh with the no-arg command would see (and could accidentally commit) a change to the frozen anchor.

Why it's silent: no test cross-checks any anchor header commit against RELEASED_REVISION_PINS or the schema-twins manifest (grep for "Last updated from commit" under packages/core-internal/test/ returns nothing) — the same silent-provenance class as the rule-4 anchor/manifest skew already reported in #3485357818.

Fix: a one-line alignment — regenerate the 2025-11-25 anchor from the pin so its header records 0168c57f (content unchanged, so this is a header-only commit), or move the pin to 357adac4 if that is the intended freeze point. Optionally add a test asserting each released anchor header's recorded commit equals its pin (and the draft anchor's equals the twins-manifest commit), so provenance-only divergences fail loudly.

Why pre-existing

This PR's diff touches only the 2026-07-28 anchor's provenance header. Neither fetch-spec-types.ts, the workflow, nor the 2025-11-25 anchor/pin are modified here — this PR is the pipeline's output, and the findings concern the mechanism that produced it and stamps the exact line the diff changes (the same in-scope basis as prior accepted pipeline findings on this PR). Both issues are fail-safe: the rate-limit failure never writes wrong content, and the header/pin skew is provenance-only with verified-identical content. Nothing here should block this merge; both belong in the already-planned pipeline hardening.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 Pre-existing pipeline gap (in .github/workflows/update-spec-types.yml, not this diff): the nightly refresh branch is pushed and the PR created/updated with the default GITHUB_TOKEN, and GitHub suppresses workflow runs for events created by that token — so the pull_request-triggered gate workflows (main.yml, conformance.yml, examples.yml) never run on this or any refresh PR, and the drift-guard comparison suites structurally cannot produce the red-CI signal that README lifecycle rule 3 relies on. Fix by pushing/creating the PR with a GitHub App installation token or PAT (or explicitly dispatching the gate workflows after pr create/edit), or at minimum document that refresh PRs carry no CI verdict and must be validated locally before merge.

Extended reasoning...

What the bug is

The nightly cron that produces and maintains this PR authenticates every write with the repository's default GITHUB_TOKEN: the branch push (git push -f --no-verify origin update-spec-types, update-spec-types.yml:75) uses the credentials persisted by actions/checkout@v7 (no token: input, default persist-credentials: true), and the PR is created/updated via gh with GH_TOKEN: ${{ github.token }} (line 64). GitHub's documented platform behavior ("Automatic token authentication") is: "events triggered by the GITHUB_TOKEN, with the exception of workflow_dispatch and repository_dispatch, will not create a new workflow run" — a deliberate, long-standing guard against recursive workflow runs (the same limitation peter-evans/create-pull-request prominently documents under "Triggering further workflow runs"). So the pull_request opened event from gh pr create and every subsequent synchronize event from the nightly force-pushes never trigger any workflow.

The code path

  1. Cron fires at 04:00 UTC; drift is detected; the workflow force-pushes update-spec-types using the checkout-persisted GITHUB_TOKEN and runs gh pr create/gh pr edit with github.token.
  2. GitHub attributes the resulting pull_request opened/synchronize events to the GITHUB_TOKEN actor and, per the rule above, creates zero workflow runs from them.
  3. All three per-PR gate workflows — main.yml (the check:all/test matrix where the drift-guard suites, incl. spec.types.2026-07-28.test.ts, run), conformance.yml, and examples.yml — trigger only on pull_request, push filtered to main/v2-2026-07-28, and workflow_dispatch (verified by reading their on: blocks; no merge_group or schedule anywhere in .github/workflows/). Nothing compensates.
  4. Result: no refresh PR produced by this cron ever gets a pull_request-triggered CI verdict. The permissions: block (contents: write, pull-requests: write) is irrelevant — the suppression keys on token identity, not scopes.

Why this defeats the repo's own design

packages/core-internal/src/types/README.md lifecycle rule 3 states: "A refresh PR that breaks the comparison suites is the desired signal — it is fixed in that PR, not bypassed." That signal structurally cannot appear: the comparison suites are pull_request-triggered and never execute on the bot's PR. The workflow even sets LEFTHOOK: 0 / --no-verify with the comment "spec drift that breaks typecheck should still open a PR so it can be fixed there" — explicitly deferring validation to PR CI that never runs. It also reframes this PR's review record: the many prior comments describing red CI reflect local reproduction only; this PR has never had a pull_request-triggered CI verdict of any kind.

Concrete failure scenario

  1. Upstream lands a semantic schema.ts change; the nightly job opens a refresh PR whose anchor change breaks spec.types.2026-07-28.test.ts (exactly the situation documented at length in this PR's history).
  2. The PR shows no checks at all — not red, absent. A maintainer merging on the strength of an apparently unobstructed PR breaks main; the breakage only surfaces afterwards in main.yml's push-triggered run on main.
  3. Alternatively, if branch protection marks these workflows as required checks, every cron refresh PR is permanently stuck at "Expected — waiting for status". A human push to unstick it does not stick either: per the already-filed branch-recreation finding, the next 04:00 UTC run force-recreates the branch from main and wipes the human commit, re-suppressing the synchronize event.

Why nothing prevents it

No other trigger covers the update-spec-types branch: the gate workflows' push triggers are filtered to main/v2-2026-07-28, and there is no merge_group in the repo. And nothing surfaces the gap — a PR with zero checks looks clean, not broken. (Per the repo's recurring-catch on unverified CI claims: a live check-runs API probe for this PR's head was rate-limited from the runner IP — itself corroborating the previously filed fetchLatestSHA rate-limit finding — so this claim rests on GitHub's first-party documented GITHUB_TOKEN event semantics, not on inference about third-party actions.)

How to fix

In order of robustness: (a) push the branch and create the PR with a GitHub App installation token or a PAT — pass it as actions/checkout's token: input so the persisted push credential is the app/PAT, and set GH_TOKEN accordingly; events from those tokens trigger workflows normally. This folds naturally into the token-hardening already proposed for the rate-limit finding. (b) Keep github.token but explicitly kick the gates after pr create/edit — all three gate workflows already declare workflow_dispatch (e.g. gh workflow run main.yml --ref update-spec-types), accepting those runs won't attach to the PR as pull_request checks. (c) At minimum, document in the workflow header and README rule 3 that refresh PRs carry no CI verdict and must be validated locally before merge.

This is pre-existing: the entire mechanism lives in update-spec-types.yml, which this diff (a one-line provenance-header bump) does not touch — the same pipeline tier as the force-push branch-recreation and rate-limit findings already on this PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The nightly workflow refreshes this PR with gh pr edit $EXISTING_PR --body ... (no --title), so this PR — created before #2252 split the spec snapshot into per-revision files — is permanently titled "chore: update spec.types.ts from upstream", referencing a file that no longer exists anywhere in the tree, even though the body and HEAD commit message already use the new spec.types.2026-07-28.ts name. One-line fix in .github/workflows/update-spec-types.yml: pass the same --title on the edit branch as on the create branch. (Pre-existing workflow behavior, not introduced by this diff.)

Extended reasoning...

What the bug is

The nightly refresh workflow maintains this PR asymmetrically:

  • On first creation it runs gh pr create --title "chore: update spec.types.2026-07-28.ts from upstream" --body "$PR_BODY" ... — the title matching the per-revision anchor filename introduced when feat(core): per-revision spec reference types and the 2026-07-28 wire contract surface #2252 split the single spec.types.ts into spec.types.<revision>.ts snapshots.
  • On every subsequent nightly refresh of an already-open PR it runs gh pr edit "$EXISTING_PR" --body "$PR_BODY" — body only, never --title.

gh pr edit updates only the fields it is explicitly given, so the title can never self-heal. This PR (#2027) was created 2026-05-07 by a pre-#2252 revision of the workflow, so its title is chore: update spec.types.ts from upstream — a path that no longer exists anywhere in the repository (the only spec snapshot files today are packages/core-internal/src/types/spec.types.2025-11-25.ts and spec.types.2026-07-28.ts).

Step-by-step proof

  1. The old (pre-feat(core): per-revision spec reference types and the 2026-07-28 wire contract surface #2252) workflow creates PR chore: update spec.types.ts from upstream #2027 titled chore: update spec.types.ts from upstream (visible in the PR metadata, created 2026-05-07).
  2. feat(core): per-revision spec reference types and the 2026-07-28 wire contract surface #2252 renames the anchor to spec.types.2026-07-28.ts; the workflow's title and commit-message strings are updated to match (current update-spec-types.yml: commit message at ~line 74, --title at ~line 91).
  3. Every nightly run since then detects drift, force-pushes the branch, finds the open PR via gh pr list --head update-spec-types --state open, and takes the gh pr edit "$EXISTING_PR" --body "$PR_BODY" branch at ~line 88 — the title argument is simply never passed.
  4. Result: a permanent three-way divergence — the PR title carries the old filename, while the PR body (rewritten nightly: "This PR updates packages/core-internal/src/types/spec.types.2026-07-28.ts...") and the HEAD commit message (751db78: chore: update spec.types.2026-07-28.ts from upstream) both carry the new one.

Why nothing prevents it

gh pr edit only modifies explicitly-passed fields, and no automation or check compares the PR title against the workflow's canonical title string. The same divergence will recur for any future title-format change in the workflow while a refresh PR is open.

Impact

Cosmetic but user-facing and un-self-healing: the PR list, notifications, and any changelog/merge tooling that consumes PR titles record a filename that does not exist in the tree — misleading for anyone searching history for changes to spec.types.2026-07-28.ts by PR title. If the repository's squash-merge default uses the pull request title, the stale filename would also land in the merge-commit headline.

How to fix

Factor the title into a variable shared by both branches and pass it on the edit path, e.g. in .github/workflows/update-spec-types.yml:

gh pr edit "$EXISTING_PR" --title "$PR_TITLE" --body "$PR_BODY"

One-line change, no behavior risk.

Note: the defective mechanism lives in .github/workflows/update-spec-types.yml, which this diff does not touch — this is a pre-existing pipeline issue (same tier as the force-push branch-recreation, GITHUB_TOKEN CI-suppression, and rate-limit findings already on this PR) and should not block the merge.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟣 Pre-existing pipeline gap, time-bound (the 2026-07-28 publication date is 18 days out): the 2026-07-28 anchor is draft-tracked (UPSTREAM_SCHEMA_DIRS maps it to schema/draft/, no RELEASED_REVISION_PINS entry) and fetch-spec-types.ts writes fetched bytes verbatim with no check that the content matches the revision label — so once upstream publishes schema/2026-07-28/ and schema/draft/ rolls over to the next revision's working directory, the nightly cron will silently stamp next-revision draft content into spec.types.2026-07-28.ts and force-push it into this open PR. Suggested guards: parse the fetched content's LATEST_PROTOCOL_VERSION and abort on mismatch for draft-tracked revisions, and have the nightly job probe upstream for schema/2026-07-28/schema.ts and fail with a "revision published — freeze the pin and retarget" message.

Extended reasoning...

The mechanism

The 2026-07-28 anchor is draft-tracked: UPSTREAM_SCHEMA_DIRS maps '2026-07-28''draft' (scripts/fetch-spec-types.ts:25), RELEASED_REVISION_PINS (line 43) has only a 2025-11-25 entry, and the nightly workflow unconditionally runs pnpm run fetch:spec-types 2026-07-28 with no SHA argument (.github/workflows/update-spec-types.yml:48). That means every night the pipeline resolves fetchLatestSHA() against the latest commit touching schema/draft/schema.ts and updateSpecTypes() writes whatever it finds verbatim under the 2026-07-28 label — nothing in the script verifies that the fetched draft bytes actually correspond to the revision named in the file.

The revision's namesake publication date is 2026-07-28 — 18 days from this re-pull. When upstream publishes, schema/2026-07-28/ appears in the spec repo and schema/draft/ immediately becomes the working directory for the next protocol revision. This is exactly how the current cycle worked: schema/2025-11-25/ froze and schema/draft/ accumulated the 2026-07-28 changes this PR has been tracking since May. From that moment, every nightly run fetches next-revision draft content, relabels it as 2026-07-28, and — via the branch-recreation behavior already on record — force-pushes it into this PR, whose title, body, and two months of review record all describe it as the 2026-07-28 revision.

Why the existing guards don't close this

  1. The documented process is purely manual. README lifecycle rule 1 and the workflow header say the freeze ('when its schema moves out of schema/draft/') 'must land in the same commit that retargets this workflow' — but nothing detects that schema/2026-07-28/ now exists upstream. This PR has been open since 2026-05-07 without merging; assuming a same-day manual repin is not safe.
  2. The only in-repo tripwire is narrow. spec.types.2026-07-28.test.ts:851 asserts expect(LATEST_PROTOCOL_VERSION).toBe('2026-07-28'), where the constant is imported from the anchor itself. It only trips if upstream bumps the draft's version constant — and historically the constant lags the release cut (the draft carried 'DRAFT-2026-v1' for months before being stamped '2026-07-28' mid-June, with draft edits continuing to land afterwards). Any post-release straggler or next-revision draft change that doesn't touch the constant is invisible to this assertion.
  3. Even when the tripwire fires, it can't fire on this PR. Per the already-recorded GITHUB_TOKEN finding, no pull_request-triggered workflow ever runs on the bot's refresh PR, so the mislabeled content arrives with no CI verdict; the assertion first runs post-merge on main.
  4. The blast radius extends to the whole revision-anchored oracle chain. fetch-spec-examples.ts:35 hardcodes EXAMPLES_PATH = 'schema/draft/examples' with no per-revision mapping, and the schema-twins manifest's 2026-07-28 entry pins upstreamPath: 'schema/draft/schema.json' — so a maintainer dutifully performing the rule-4 atomic manifest refresh prescribed by the standing review comments would vendor next-revision schema twins and fixtures under the frozen 2026-07-28 label, corrupting the conformance oracle for a released revision in the same commit.

Concrete step-by-step scenario

  1. 2026-07-28: upstream publishes schema/2026-07-28/ (copying the draft) and merges the first post-release change into schema/draft/ — next-revision work or a straggler — without yet bumping the draft's LATEST_PROTOCOL_VERSION constant.
  2. 04:00 UTC next day: the cron fetches the draft at the new SHA, regenerates spec.types.2026-07-28.ts with next-revision content, sees drift vs main, force-recreates update-spec-types, and overwrites this PR's head. Title and body still say 2026-07-28; no CI runs.
  3. A maintainer — possibly acting on the standing 'close as no-op or land with a manifest refresh' guidance written against the current content-noop state — merges, or first refreshes the twins/fixtures from the same draft SHA per rule 4.
  4. main now carries next-revision spec content (and possibly twins/fixtures) labeled 2026-07-28. The vitest version pin catches it only if/when upstream has bumped the draft constant; otherwise the frozen-revision oracle is silently wrong and the SDK's 2026-07-28 wire drift guard starts enforcing against the wrong revision.

How to fix (folds into the already-planned pipeline hardening)

  • Cheap deterministic guard in fetch-spec-types.ts: for draft-tracked revisions, parse the fetched content's LATEST_PROTOCOL_VERSION and abort loudly when it does not equal the target revision (catches the constant-bumped case before writing anything).
  • Release detection in the nightly workflow: probe for the existence of schema/2026-07-28/schema.ts upstream (one raw.githubusercontent HEAD request) and, when found, fail the job with a 'revision published — freeze the pin and retarget this workflow' message instead of opening/updating refresh PRs.
  • On publication: add RELEASED_REVISION_PINS['2026-07-28'], flip UPSTREAM_SCHEMA_DIRS to schema/2026-07-28, update fetch-spec-examples.ts's EXAMPLES_PATH and the twins-manifest upstreamPath, and retarget the workflow — in one commit, per lifecycle rule 1.
  • Give fetch-spec-examples.ts a per-revision path mapping like fetch-spec-types.ts instead of the hardcoded draft path.

This is a pre-existing gap — the entire mechanism lives in .github/workflows/update-spec-types.yml and scripts/, which this one-line provenance-header diff does not touch — so it should not block this merge. It is recorded because it directly determines this open PR's future head content, the deadline is concrete, and the guards are cheap.

*
* DO NOT EDIT THIS FILE MANUALLY. Changes will be overwritten by automated updates.
* To update this file, run: pnpm run fetch:spec-types 2026-07-28
Expand Down