Skip to content

Route the PM seat's high-frequency GitHub reads to REST in the permission set - #11186

Merged
os-zhuang merged 1 commit into
mainfrom
claude/issue-11181-pm-rest-routing
Aug 23, 2026
Merged

Route the PM seat's high-frequency GitHub reads to REST in the permission set#11186
os-zhuang merged 1 commit into
mainfrom
claude/issue-11181-pm-rest-routing

Conversation

@os-zhuang

@os-zhuangos-zhuang commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Fixes#11181

Why — the measured quota split (cited from the card, not re-measured)

The card carries the seat's measurement from 2026-08-23 01:1x UTC, taken through the free
GET /rate_limit endpoint. Quoted, not repeated:

bucketused this hourlimit
core (REST)~5015000
graphql~49655000
search030/min (independent bucket)

Two structural findings on the card, both independent of concurrent-seat noise:

  1. MCP list_issues returns pageInfo.startCursor / endCursor — GraphQL Connection
    cursors. The issue list runs on GraphQL.
  2. A squeeze measurement across MCP calls moved graphql used by +7 and +4 while core
    barely moved.

So the loop's high-frequency reads spend the bucket that is at its ceiling, while the one
with 99.7% headroom goes unused. The card's routing table names the REST equivalent for
each of those reads; this PR makes those calls prompt-free for the seat.

What changed

One file, .claude/settings.json, permissions.allow: 19 entries added.

Shape is copied from the additive label POST that #11112 added, changing only method and
path — the URL sits immediately after the -X method flag, and the trailing * absorbs the
Authorization: Bearer header and any payload, which is why that convention puts headers
after the URL. #11112's own commit message states the intent this follows: "pre-approve
exactly the … spelling that os-dev.md now prescribes". Every entry pins method + host +
path shape. There is no bare curl * entry, and nothing here widens the two POST entries
already present.

Nine path shapes, each as an objectstack/objectui twin:

routing-table rowentry path shape
backlog listGET /repos/objectstack-ai/{repo}/issues?* *
card body/state readGET /repos/objectstack-ai/{repo}/issues/* *
decision re-read (comment paging)GET /repos/objectstack-ai/{repo}/issues/*/comments* *
label read half of read-modify-writeGET /repos/objectstack-ai/{repo}/issues/*/labels* *
PR body readGET /repos/objectstack-ai/{repo}/pulls/* *
PR filesGET /repos/objectstack-ai/{repo}/pulls/*/files* *
PR commitsGET /repos/objectstack-ai/{repo}/pulls/*/commits* *
CI verdictGET /repos/objectstack-ai/{repo}/commits/*/check-runs* *
PR body rewritePATCH /repos/objectstack-ai/{repo}/pulls/* *

Plus one repo-agnostic entry, GET https://api.github.com/rate_limit * — the free
endpoint a quota check reads.

On the two wildcard spellings: paths that can carry a query string take the wildcard
against the path token itself (comments* *, not comments *), so one entry covers both
…/comments and …/comments?per_page=100&page=2 without loosening the path shape. Paths
that never carry one (/issues/{n}, /pulls/{n}) keep #11112's exact … /* * spelling.
Under either reading of the matcher's * — single-segment or any-character — the two
adjacent wildcards match the intended command and nothing broader in path shape.

The label write stays on the two POST entries #11112 added; only the read half of the
read-modify-write moves to REST here.

Routing-guidance prose — deferred, and why

Deferred to the #11086 whole-text restructuring round, per the card's own escape
hatch.
Both candidate homes are at zero headroom, confirmed by the gate on this commit:

  • .claude/skills/pm-dispatch/references/platform-readings.md — 134 lines, ceiling 134,
    headroom 0
  • .claude/skills/pm-dispatch/SKILL.md — 682 lines, ceiling 682, headroom 0

No ceiling was raised — that is maintainer-only. I looked for genuine deletion (真删) to
fund the row and did not find any I can defend as duplication. What the file does hold
is supersession, which is a different thing and not mine to land in this PR:

  • platform-readings.md § API 配额 currently opens with a dated 2026-08-22 reading that
    the seats run under different GitHub accounts, with one shared identity holding only
    within a seat. The card's 2026-08-23 measurement reads the opposite — one identity
    across seats, GraphQL as a commons already at the ceiling. Both are dated measurements
    and they contradict each other.
  • The same section states that direct REST is fenced off by a session-level authorization
    gate, concluding that a pure-MCP session meeting a drained pool has no list channel
    until reset. The card overturns the premise for this seat: it used GET /rate_limit,
    GET /commits/{sha}/check-runs and PATCH /pulls/{n} on 2026-08-23 — the very calls
    this allowlist covers.

Deleting either would be resolving a conflict between two measured readings, not paying a
line budget, and it would land inside the allowlist PR where no reviewer is looking for
it. Both are flagged in the dev report for PM triage; the whole-text round is where they
belong. The allowlist stands on its own without the prose — it removes permission
prompts, it does not instruct anyone to route differently.

Gates — run on the final commit 99f2db40e2

Families derived with node scripts/pm/dispatch-gates.mjs (no hand-fed paths; it read the
change set from the merge base itself and reported 1 path(s).claude/settings.json).
It named four families; all four were run, plus the two the dispatch mandates and the
NUL-byte gate. Every verdict below is the line the gate itself printed; exit codes were
captured before any pipe.

node scripts/pm/check-skill-line-ratchet.mjs --self-test EXIT=0
✓ check-skill-line-ratchet self-test: 19 cases pass.
node scripts/pm/check-skill-line-ratchet.mjs EXIT=0
✓ check-skill-line-ratchet: .claude/skills/pm-dispatch/references/platform-readings.md
is 134 lines (ceiling 134; headroom 0).
(20 ✓ lines, every ceiling in the map green)
pnpm check:doc-authoring EXIT=0
✓ doc authoring guard: 389 files clean — no bare metadata literals.
pnpm check:skill-frame-sync EXIT=0
✓ check-skill-frame-sync: 4 copies of the decision frame are structurally isomorphic
across 3 files
pnpm check:pm-governed-merges EXIT=0
✓ check-governed-merges --self-test: 119 assertions …
pnpm --filter @objectstack/lint run check:doc-formula-expressions EXIT=0
✓ check:doc-formula-expressions: 22 record-scoped formula example(s) across 416 files /
1447 TS blocks judged clean by @objectstack/formula.
pnpm check:nul-bytes EXIT=0
check-nul-bytes: OK (scanned 6485 text file(s) — 6485 tracked, 0 untracked-not-ignored;
skipped 5 binary; no raw ASCII control bytes).

Hook self-tests — this PR does not touch .claude/hooks/, but the card asks for them:

.claude/hooks/guard-main-checkout-bash.selftest.sh EXIT=0
.claude/hooks/guard-shared-stash.selftest.sh EXIT=0

JSON validity: require('./.claude/settings.json') parses; 47 allow entries, 21 of them
Bash(curl …).

Repo-wide ESLint — a proven narrowing, not a skipped run

pnpm lint (eslint . --no-inline-config) was not run locally; CI runs the farm on every
PR. The narrowing is measured rather than assumed, on all three counts:

  1. Population read from ESLint's own config, not from a guess about what counts:
    eslint.config.mjs matches **/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} and the five
    narrower files: blocks are all .ts/.tsx subsets. No JSON plugin is configured and
    no files: pattern names .json.
  2. File count: this diff is exactly one path, .claude/settings.json. Its
    intersection with that population is zero files.
  3. Configuration invariance for untouched files: the change is data inside a
    permission allowlist — it defines no ESLint rule, plugin, parser or files: pattern,
    so it cannot move the verdict on any file ESLint does lint.

Landing — governed surface, human merge only

node scripts/pm/check-governed-merges.mjs --test .claude/settings.jsonEXIT=3,
verbatim:

governed-surface predicate: 1 of 1 path(s) hit the register (5 surfaces, repo-agnostic).
⛔ GOVERNED — a human merge is the review record for this PR (#9495 regime).
No seat flips it ready, enqueues it, or arms auto-merge (AGENTS.md Prime Directive #14).
One hit governs the whole PR — 「混合 diff 一条命中即整 PR 分叉」; proportion is not a question.
.claude/** ×1 — the agent instruction tree (skills, agents, hooks, settings)
- .claude/settings.json

This PR therefore stays a draft. No seat marks it ready, enqueues it, or arms
auto-merge. A human merges it.

No changeset: the diff is .claude/**-only and publishes nothing. dispatch-gates.mjs
placed the five changeset families in its "once a changeset exists" section, i.e. none of
them is triggered by the paths as they stand. The skip-changeset label is applied
separately and read back.

content/docs/releases/ is untouched.


Generated by Claude Code


Generated by Claude Code

…e permission set
The seat's GraphQL bucket is measured at the ceiling (~4965/5000 used in one
hour) while REST core sits nearly empty (~50/15000). The MCP list/read family
runs on GraphQL — `list_issues` returns Connection cursors, and a squeeze
measurement moved graphql used by +7/+4 across MCP calls while core barely
moved. Every read the loop repeats therefore spends the scarce bucket.
Pre-approve the REST equivalents so the seat can take them without a prompt,
spelled exactly like the additive label POST added by #11112 (method, host and
path shape pinned; URL immediately after `-X`; the trailing `*` absorbs the
Bearer header and any payload). Nine path shapes per repo, objectstack and
objectui twins: issue list, issue get, issue comments, issue labels read, PR
get, PR files, PR commits, commit check-runs, and the PR body PATCH. Plus
`GET /rate_limit`, which is free and is what a quota check reads.
Query-carrying paths get the wildcard against the path itself (`comments* *`)
so one entry covers both the bare and the `?per_page=…&page=N` spellings
without widening the path shape. No bare `curl *` entry is added.
The routing-guidance prose is NOT in this PR: `platform-readings.md` is at
134/134 and SKILL.md at 682/682, both zero headroom, and no honest deletion
was available — see the PR body.
Fixes#11181
Co-Authored-By: Claude <noreply@anthropic.com>
@claudeclaudeBot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 23, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

ACCEPT — session 757f0dc0-5dae-51d7-9053-c15c57ebfe3b(派发席,卡 #11181)。

逐条读完 diff(1 文件,+20/−1,head 99f2db40e):

等维护者合并。合并后席位即可免弹窗走 REST 高频读,GraphQL 桶只留给 mutation-only 三件套。


Generated by Claude Code

@os-zhuangos-zhuang self-assigned this Aug 23, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 23, 2026 01:38
@os-zhuang
os-zhuang enabled auto-merge August 23, 2026 01:38
@os-zhuang
os-zhuang added this pull request to the merge queueAug 23, 2026
Merged via the queue into main with commit 2b5a7ceAug 23, 2026
28 of 30 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-11181-pm-rest-routing branch August 23, 2026 01:50
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/sskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pm 席位 GraphQL 限流:高频读改走 REST(curl+代理凭证),GraphQL 只留给 mutation-only 操作

2 participants

@os-zhuang@claude