Skip to content

tooling(pm): three ordered read paths for check-clause2-carriers, so a seat's ACCESS stops deciding whether clause-② is checkable - #15523

Merged
os-steve merged 1 commit into
mainfrom
claude/issue-14965-clause2-carriers-reach
Sep 4, 2026
Merged

os-steve merged 1 commit into
mainfrom
claude/issue-14965-clause2-carriers-reach

Conversation

@claude

@claude claude Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

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.mjs now reads a pair down a declared ladder instead of one path, and reports which rung answered: (i) GITHUB_TOKEN / GH_TOKEN when 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 no authorization header, 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/no vocabulary, 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 reads null, 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 the x-ratelimit-remaining it 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:

  • a header-less GET /repos/…/pulls/15460 → HTTP 200, with x-ratelimit-limit: 15000, x-ratelimit-remaining: 14576, x-ratelimit-used: 424, x-ratelimit-resource: core;
  • the unmodified script with GITHUB_TOKEN and GH_TOKEN unset → --pair 15460 exits 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 /user from this container answers with an identity; the egress proxy also overrides an authorization header 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.

# run result
— pnpm check:pm-clause2-carriers (the file's own self-test, offline) 0 — 174 cases (150 pre-existing + 24 new), new battery pinned at its landed floor, roster size floor raised with it
A --pair 15460, token exported 0 — 4 pairs legible; report: token served 9 reads, rate limit seen (core): 14404 of 15000 remaining
B --pair 15460, GITHUB_TOKEN/GH_TOKEN unset 0 — same 4 verdicts; report: token-less public read served 9 reads
G --pair 15460, token exported, harness answering 403 to any request carrying authorization (header-less requests reach the real API) 0 — one token … was refused (HTTP 403); falling back line, then the public read carries the run; report: token: present but REFUSED (HTTP 403) — retired for the rest of this run
C --pair 15460 --pair-json pair.json, no token 0 — same 4 verdicts; report: document served 9 reads, public read 0
C2 the same document on stdin (--pair-json -) 0 — identical verdicts, read from stdin
C3 C with globalThis.fetch replaced by a thrower, inherited through NODE_OPTIONS so the proxy re-exec inherits it too 0 — identical verdicts with provably zero network reads
ctrl the positive control for C3: the same stub, no document 3 — PREREQUISITE NOT MET, and the read-path line names the --pair-json remedy. (The first attempt at this control passed for the wrong reason — --import did not survive the script's proxy re-exec — which is why the stub is injected through NODE_OPTIONS.)
E --pair on a merged PR (pair cannot be formed) 2 — unchanged wording, plus "which reads were tried, and on which path, is the line below" and the path report

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 after git fetch origin main: same 25. Every one of the 25 exited 0 (slowest: check:pm-dispatch-gates, 329 s).
  • Also run, because their rosters sit under scripts/ or scripts/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."
  • Lint is a declared narrowing, not a skip: 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-config resolves rules for it rather than reporting it ignored), and the flat config enables no type-aware linting for any file — no parserOptions.project, no projectService, which the config says of itself at eslint.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 -naP for control characters over the changed file: no hits; check:nul-bytes green in the union above.

Scope

One file: scripts/pm/check-clause2-carriers.mjs. No .claude/**, no skills/**, no AGENTS.md, no other script, no changeset (an internal PM gate publishes nothing) — hence skip-changeset.


🤖 Generated with Claude Code

https://claude.ai/code/session_019RfFHiRCSs3JXLK4cwcfox


Generated by Claude Code

… 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
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 4, 2026
@github-actions github-actions Bot added the size/m label Sep 4, 2026
@os-steve
os-steve marked this pull request as ready for review September 4, 2026 20:07
@os-steve
os-steve enabled auto-merge September 4, 2026 20:07
@os-steve
os-steve added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit b8229e2 Sep 4, 2026
36 checks passed
@os-steve
os-steve deleted the claude/issue-14965-clause2-carriers-reach branch September 4, 2026 20:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants