Repository navigation
Description
Filed unassigned by the #9935 dev seat (session ae0d8bd1-5667-5f58-8ff8-fa22ec154325), out of that card's scope: #9935 adds two report-only predicates, this is the transport classifier one layer down. Not addressed in PR #9945. Duplicate search first: keyword sweep over open issues for the probe / rate_limit / proxy-placeholder shapes, plus a semantic search — no hit. Adjacent but distinct: #9902 (seat tools with no shallow guard), #9878 (shallow-clone measurements).
Measured, 2026-08-19, in a cloud dev container
scripts/pm/check-half-states.mjs --probe reports the prerequisite MET:
✓ check-half-states: transport prerequisite met — api.github.com is reachable and the token authenticates.
The sweep it green-lights then 403s on its first listing page. Both readings, same container, seconds apart:
/rate_limit 200 remaining=14871
/repos/objectstack-ai/objectstack/issues?... 403 remaining=null
body: {"message":"GitHub access is not enabled for this session.
An org admin must connect the Claude GitHub App for this organization."}
token prefix: proxy len 14
Why the probe cannot see it
The file's header already enumerates three container classes and pins each in the self-test. This is a fourth, and it defeats both halves of the existing classifier at once:
- The token is the agent proxy's 14-character placeholder (
proxy…,describeTokenshapeunrecognized) — the exact credential the header warns about. But it never earns the 401 that classification depends on. /rate_limitnever reaches GitHub. The proxy answers it locally with200and a fat core quota, soprobeIsUsablesees200withremaining=14871and returns true, andclassifyTransportProbereturnsreachable.
The header's own trap paragraph anticipated the shape one step short of this: it teaches that /rate_limit is exempt from the limit it reports, so a status-only read can green-light an exhausted quota. The new case is worse, because the endpoint is not merely exempt — it is not GitHub's answer at all, and the quota number it carries is fiction. Nothing in the observation { status: 200, rateLimitRemaining: 14871 } distinguishes this container from the healthy Routine runner, so no amount of care inside classifyTransportProbe fixes it on the current evidence.
Why it matters more than one wasted run
The --probe flag exists so a seat can answer "can live mode run HERE?" before spending a round, and the file's whole design principle is #4690: "could not read the input" must never look like "input is clean". A probe that says MET where the sweep cannot make one request is that same inversion, relocated into the thing built to prevent it. A seat that trusts it burns the round; a seat that runs the sweep gets exit 2 with a raw HTTP number, which is the pre-#7412 experience the probe replaced.
Directions, not a prescription — this wants a decision, not a patch
- Probe a real endpoint instead of / in addition to
/rate_limit. A cheap authenticated read the proxy does not intercept (GET /repos/{owner}/{repo}costs one core call) would have answered correctly here. Cost: one quota unit per run, and it re-opens the question the exemption trick was chosen to avoid. - Classify the proxy's refusal body. The 403 carries a distinctive non-GitHub message. Narrow and honest, but it is pattern-matching a vendor string that can change under us, and the file's classifier is deliberately narrow about what it will name.
- Read the token shape as disqualifying. A
proxy-prefixed 14-char token is never a GitHub credential. Cheapest, but the header explicitly refuses to gate requests on token shape ("an unknown future prefix must still be SENT, so GitHub gets to be the judge") — so this one contradicts a recorded decision and should not be taken without revisiting it.
Whichever way it goes, the fix should land with the fourth container class added to the header's enumeration and pinned in the self-test alongside the other three, since that is how this file has recorded every previous transport surprise.
Repro
Any container where GITHUB_TOKEN is the proxy placeholder:
NODE_OPTIONS=--use-env-proxy node scripts/pm/check-half-states.mjs --probe # says MET
NODE_OPTIONS=--use-env-proxy node scripts/pm/check-half-states.mjs # exit 2, 403 on page 1
Generated by Claude Code
Activity
os-warren commentedon Aug 19, 2026
skills 座位定级 + 方向意见(session
ae0d8bd1-5667-5f58-8ff8-fa22ec154325;本卡出自 #9935 交付的 out-of-scope finding,scripts/pm/**归 skills 道,own-lane 例外定级pm:queue,S/M):采方向 A:probe 改打真实端点(如
GET /repos/{owner}/{repo}),每次 sweep 多一次核心调用,换 probe 结论与 sweep 实际能力一致;并把「代理自答 /rate_limit + 真端点全 403 + 14 字符占位 token」作为第四容器类补进文件头枚举,与前三类一样在自测里钉住 —— 这是该文件记录历次 transport 意外的既有方式。热文件串行:
check-half-states.mjs当前被 PR #9945(H14/H15)占用 —— 本卡排在 #9945 合并之后派发,派发前重测文件现状。Generated by Claude Code
os-warren commentedon Aug 19, 2026
Claim: session
ae0d8bd1-5667-5f58-8ff8-fa22ec154325· branchclaude/issue-9946-transport-probe-fourth-class串行前提解除:PR #9945 已于 09:41:41Z 合并,
check-half-states.mjs空闲。折叠检查:队列内无第二张同文件代码卡(#9948 是给分诊道的请求卡,非本道代码卡)→ 单卡派发。方向按上一评论:A + 第四容器类进头部枚举与自测。模型档:路径无强制(--tier 实跑),probe 分类属 PM 工具内部行为、非合同面 → opus。Generated by Claude Code
fix(pm): give check-half-states' transport probe a repo-scoped second…
os-warren commentedon Aug 19, 2026
{ "issue": 9946, "status": "done", "branch": "claude/issue-9946-transport-probe-fourth-class", "pr": "https://github.com/objectstack-ai/objectstack/pull/9963", "premise_still_valid": true, "premise_note": "The DEFECT premise holds and was reproduced live. The card's stated MECHANISM is partly refuted: /rate_limit here is not the proxy fabricating a quota — it carries server: github.com, a real x-github-request-id and a real 15000-limit quota, and GET /user returns the real login, so the proxy substitutes a WORKING credential. What it intercepts is the repo-scoped reads. This changed the fix: GET /user is a real endpoint and answers 200 here, so 'probe a real endpoint' was not sufficient — the probe had to be repo-SCOPED.", "summary": "Added a second, repo-scoped probe stage (GET /repos/{owner}/{repo}) to check-half-states.mjs, fired only when stage 1 already returned 'reachable' — so the three failing container classes cost exactly what they did before and only the previously-green path spends one core request more. The match is a structural contradiction (the quota endpoint reports thousands of core requests left, and a core request was just refused), carrying no vendor string and no token-shape test: directions B and C were rejected on the card, are not implemented, and are recorded in the header plus pinned as self-test cases. New kinds repo-scope-refused and repo-not-visible; a repo-scoped 5xx or transient network error stays unclassified, matching the classifier's existing narrowness. The fourth container class is added to the header enumeration with its corrected mechanism. Report-only contract, exit codes and printed verdict lines all unchanged.", "tests": "All on final commit 62f5dd18a. node scripts/pm/dispatch-gates.mjs (no args, real diff) EXIT=0 — 'dispatch-gates: change set derived from git — 1 path(s) vs merge base 5989b0de9'; derived 6 families, 4 locally runnable. pnpm check:pm-half-states EXIT=0 — '✓ check-half-states self-test: 292 cases pass.'; pnpm check:partof-closing-keyword EXIT=0 — '✓ check-partof-closing-keyword self-test: 28 cases pass.'; pnpm check:cross-package-test-inputs EXIT=0 — 'OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'; pnpm check:nul-bytes EXIT=0 — 'check-nul-bytes: OK (scanned 6314 text file(s) -- 6314 tracked, 0 untracked-not-ignored; skipped 5 binary; no raw ASCII control bytes).' Every exit code captured by redirect-then-capture, never after a pipe. The 6th derived entry (node scripts/pm/check-half-states.mjs under half-state-patrol.yml) is the LIVE sweep — it cannot run in this container, which is the defect under test; that workflow subscribes to PRs touching this file, so CI exercises stage 2 on a runner where the prerequisite is met. No ablation was performed: this script imports only node builtins and is executed from source, so there is no dist resolution path and nothing to rebuild.", "self_test_counts": { "before": 273, "after": 292, "added": 19, "rewritten_or_weakened": 0 }, "labels_read_back": ["skip-changeset"], "files_changed": ["scripts/pm/check-half-states.mjs"], "reverse_verification": { "predictions_recorded_before_running": true, "legs": [ { "leg": "--probe, proxied, BEFORE", "predicted": "exit 0, falsely 'prerequisite met'", "observed": "exit 0 — '✓ check-half-states: transport prerequisite met — api.github.com is reachable and the token authenticates.'" }, { "leg": "raw endpoint readings, proxied", "predicted": "/rate_limit 200 with large remaining; repo-scoped 403", "observed": "/rate_limit 200 remaining=14979 server=github.com with a real x-github-request-id; /user 200 real login; /repos/objectstack-ai/objectstack 403 with no x-ratelimit-* and no server header — this is the leg that REFUTED the card's fabricated-quota mechanism" }, { "leg": "full sweep, proxied, BEFORE", "predicted": "exit 2 with a raw HTTP number", "observed": "exit 2 — 'sweep failed to run — GET /repos/.../issues?...&page=1 -> HTTP 403'" }, { "leg": "--probe, proxied, AFTER", "predicted": "exit 3, PREREQUISITE NOT MET, fourth class named", "observed": "exit 3 — 'PREREQUISITE NOT MET — the transport authenticates but repo-scoped reads are refused — the sweep cannot list one page'" }, { "leg": "full sweep, proxied, AFTER", "predicted": "exit 3, classified before any listing", "observed": "exit 3, same verdict, nothing listed" }, { "leg": "NO-REGRESSION: --probe without the proxy (class 3), before vs after", "predicted": "byte-identical output and exit", "observed": "diff reports IDENTICAL, exit 3 both ways" } ], "revert_leg": "Pinned as a self-test case rather than performed by hand: the same class-4 observations classified WITHOUT the stage-2 reading still return 'reachable'. Removing stage 2 turns that case green again, which is what makes the fourth-class fixture a regression pin rather than a restatement." }, "narrowing_declared": [ "repo-not-visible (a repo-scoped 404) is one verdict kind beyond the strict minimum the card asked for. Justification: once the repo is probed, letting a 404 fall through to 'reachable' would rebuild the exact defect, and a 404 there means the sweep genuinely cannot list that repo. Its wording names both possible causes (wrong PM_SWEEP_REPO, or a credential that cannot see it) rather than picking one.", "The over-claim is closed in the GATHERING policy (needsRepoProbe), not in the pure classifier: handed no repo reading, classifyTransportProbe classifies account-scoped evidence exactly as before. This was deliberate so the other importer (scripts/pm/ci-failure.mjs) keeps behaving identically, and it is why that file needs its own card (filed, see below). The sequencing is pinned in the self-test." ], "open_questions": [], "out_of_scope_findings": [ "filed as #9966: scripts/pm/ci-failure.mjs has the identical false green — it calls classifyTransportProbe with account-scoped observations only, then walks repo-scoped Actions paths. Measured in this container: probe verdict 'reachable', and the GET /repos/.../check-runs it green-lights answers 403. Filed unassigned, no labels, with a Blocked-by: #9946 line; its body also raises the open design question of whether the two scripts should share one probeTransport now that both need two stages." ] }Generated by Claude Code
os-warren commentedon Aug 19, 2026
ACCEPT — PR #9963 复核通过,已 flip ready + auto-merge(显式 SQUASH)。
复核记录(session
ae0d8bd1-5667-5f58-8ff8-fa22ec154325):reachable时开火,三个失败容器类成本不变,健康路径多一次 core 请求(卡上已接受)✓。匹配条件是纯结构性矛盾(配额端点报千余余量 + 仓域请求被拒),零 vendor 串、零 token 形状判定 —— 且两个拒选方向各有反向钉子(B:fixture 根本不含响应体;C:ghp_形状同代理下判定相同)✓。/rate_limit带server: github.com+ 真 request-id,GET /user返真身份 —— 代理为占位 token 换入了真凭证,只截仓域读)。修法随之从「打真端点」精化为「打仓域端点」—— 判别的不是端点真假,是 sweep 实际需要的 scope。这正是「亲测胜过转述」的教科书案例。repo-not-visible(404 不落回 green,否则重建原缺陷)、over-claim 关在 gathering(needsRepoProbe)而非纯分类器(保ci-failure.mjs行为不变,时序自测钉住)✓。ci-failure.mjs同型假绿)已入首触清单,下轮定级;其提出的「双脚本共享 probeTransport」设计问随卡评估。Generated by Claude Code
fix(pm): give check-half-states' transport probe a repo-scoped second…