Repository navigation
tooling(pm): three ordered read paths for check-clause2-carriers, so a seat's ACCESS stops deciding whether clause-② is checkable - #15523
Merged
Conversation
… a seat's ACCESS stops deciding whether clause-② is checkable
The clause-② carrier check could only be run where the reading did not matter:
the sessions that land pairs read GitHub through MCP, and there the checker's
answer was exit 2 — "the pair could not be formed" — which its own output
correctly refuses to call a clearance. So precondition ② was unchecked exactly
where all the measured defects happened.
The reads now go down a declared ladder, and the ladder is reported:
(i) GITHUB_TOKEN / GH_TOKEN when the environment exports one — still first,
the largest budget and the only path that reads a private board. A
credential the API REFUSES (401/403) is now retired for the rest of the
run instead of failing the read: a stale token must never make a PUBLIC
pair unreadable.
(ii) the same request with no `authorization` header. The reader always
omitted the header when no token was exported; what is new is that the
path is declared, reachable from a refused token, and reported.
(iii) `--pair-json FILE` / `--pair-json -` — the pair pre-fetched into a
document and judged with no network at all, which is the path an
MCP-only seat has. A named document is the WHOLE input: never half a
document and half a live board.
Judging is untouched: the closed yes/no vocabulary, the four-valued read, the
carrier set and the exit register are exactly as they were. A key the document
omits reads null — UNJUDGED, never an absent label or an empty thread — so a
hand-assembled input can only make this file say LESS about a pair, never more.
Every refusal now names which paths were tried, and every run prints the
`x-ratelimit-remaining` it actually saw rather than the budget its regime is
supposed to have.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
This was referenced Sep 4, 2026
os-steve
marked this pull request as ready for review
September 4, 2026 20:07
os-steve
enabled auto-merge
September 4, 2026 20:07
This was referenced Sep 4, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #14965 — the TOOLING half (direction A) only. The claim-template half is a separate branch and is not addressed here.
What changed
scripts/pm/check-clause2-carriers.mjsnow reads a pair down a declared ladder instead of one path, and reports which rung answered: (i)GITHUB_TOKEN/GH_TOKENwhen the environment exports one — still the first choice, the largest budget and the only path that can read a private board, except that a credential the API refuses (401/403) is now retired for the rest of the run instead of failing the read, because a stale token must never make a public pair unreadable; (ii) the same request with noauthorizationheader, which these public boards serve — the reader always omitted the header when no token was exported, so what is new is that this path is declared, reachable as a fallback from a refused credential, and reported; (iii)--pair-json FILE(or--pair-json -for stdin) — the pair pre-fetched into a document and judged with no network at all, which is the path an MCP-only seat has, and a named document is the WHOLE input, never half a document and half a live board.Judging is untouched: the closed
yes/novocabulary,CLAUSE2_KEY_LINE/CLAUSE2_NEAR_MISS_LINE, the four-valued read (malformed/misplaced/absent/unreadable), the carrier set and the exit register 0/2/3/4 all keep their meanings. The document path is deliberately asymmetric: a key the document omits readsnull, which every caller already treats as UNJUDGED, so a hand-assembled input can only make this file say less about a pair, never more. Every refusal now names which paths were tried, and every run prints thex-ratelimit-remainingit actually saw.The premise, re-measured — and the half of it that was wrong
The card's premise is that the checker "needs its own token and exits 2". Read against the tree, the reader never checked for a token: it already omitted the header when none was exported. Measured from this container, 2026-09-04:
GET /repos/…/pulls/15460→ HTTP 200, withx-ratelimit-limit: 15000,x-ratelimit-remaining: 14576,x-ratelimit-used: 424,x-ratelimit-resource: core;GITHUB_TOKENandGH_TOKENunset →--pair 15460exits 0, the same verdicts as the token run.But 15000 is not the anonymous budget. GitHub documents 60/hour per IP for anonymous core, and a header-less
GET /userfrom this container answers with an identity; the egress proxy also overrides anauthorizationheader the caller sends (a deliberately invalid token still reads 200 as that identity). So "token-less reads work here" is true and "these reads are anonymous" is not — both regimes are in play depending on where the checker runs, which is exactly why the run now prints the remaining count it saw instead of stating a budget it did not measure. That override is also why the token-retirement rung cannot be produced natively in this container and was proved with a harness instead (row G below).Direction A's real gap was therefore not a token check. It was (a) no offline path at all for a seat with no network, (b) a refusal that did not say which paths had been tried, and (c) a budget nobody printed. This PR closes those three.
Test matrix
All rows at
20c27b3ef; each exit code captured before any pipe.pnpm check:pm-clause2-carriers(the file's own self-test, offline)--pair 15460, token exportedrate limit seen (core): 14404 of 15000 remaining--pair 15460,GITHUB_TOKEN/GH_TOKENunset--pair 15460, token exported, harness answering 403 to any request carryingauthorization(header-less requests reach the real API)token … was refused (HTTP 403); falling backline, then the public read carries the run; report:token: present but REFUSED (HTTP 403) — retired for the rest of this run--pair 15460 --pair-json pair.json, no token--pair-json -)read from stdinglobalThis.fetchreplaced by a thrower, inherited throughNODE_OPTIONSso the proxy re-exec inherits it too--pair-jsonremedy. (The first attempt at this control passed for the wrong reason —--importdid not survive the script's proxy re-exec — which is why the stub is injected throughNODE_OPTIONS.)--pairon a merged PR (pair cannot be formed)The offline rows judge the same pair as the live rows and return the same verdicts, which is the property that matters: path (iii) is not a second reader, it is the same predicates fed from a document.
Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.list→ exit 0: "✓ dispatch-gates --ran: 25 derived famil(ies) accounted for — 25 run, 0 NOT-MEASURED." Re-derived aftergit fetch origin main: same 25. Every one of the 25 exited 0 (slowest:check:pm-dispatch-gates, 329 s).scripts/orscripts/pm/and the derivation marks their silence as evidence in neither direction:check:pm-label-desc-cap,check:pm-governed-prose,check:pm-skill-ratchet,check-published-list-mirrors.mjs— all exit 0.node scripts/pm/check-governed-merges.mjs --test scripts/pm/check-clause2-carriers.mjs→ exit 0: "NOT governed — ordinary queue landing applies to a PR with exactly this file list."eslint scripts/pm/check-clause2-carriers.mjs --no-inline-config --format json→ exit 0, and the JSON reports 1 file linted, 0 errors, 0 warnings. The file is inside eslint's own configured population (--print-configresolves rules for it rather than reporting it ignored), and the flat config enables no type-aware linting for any file — noparserOptions.project, noprojectService, which the config says of itself ateslint.config.mjs:326— so this diff cannot move the verdict on a file it does not touch. The repo-wide scan is CI's run.grep -naPfor control characters over the changed file: no hits;check:nul-bytesgreen in the union above.Scope
One file:
scripts/pm/check-clause2-carriers.mjs. No.claude/**, noskills/**, noAGENTS.md, no other script, no changeset (an internal PM gate publishes nothing) — henceskip-changeset.🤖 Generated with Claude Code
https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox
Generated by Claude Code