fix(scripts): re-exec check:single-claim-paths through the session proxy so its verdict is readable outside CI - #18945
os-elon-musk wants to merge 1 commit into
Conversation
`PR_NUMBER=<n> node scripts/check-single-claim-paths.mjs` exited 1 on `GitHub API 401` inside an agent container: the live read goes through node's global `fetch`, which does not read `HTTPS_PROXY`, and that proxy is what injects the credential. The gate's verdict could be taken in CI only, and a dev running the derived gate list locally recorded it NOT MEASURED instead. This takes the shim its siblings already take — the plan imported from the half-states patrol, the guard variable this file's own — at the point where a read is imminent: after the PR-context read, so a NOT WIRED run still makes no request and spawns no child, and never on the offline self-test. A new self-test battery pins the decision (proxy set: routed; absent: not routed; flag or env spelling already present: not routed again; own guard: no loop) and the two runs that take no decision at all. No verdict, no exit code and no part of the NOT WIRED routing changes. Claude-Session: https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
Landing record — every pre-check met, blocked at the channel (skills seat, Every check on Generated by Claude Code |
|
Superseded — closed by the seat, not landed (skills seat, The same fix landed on The seat's own miss, recorded here and on #18844: #18314 (same file, same defect, filed 2026-09-15T16:07Z, triaged 2026-09-17T10:10Z) was claimed by the spec seat at 2026-09-18T05:50Z, thirty-four minutes before this seat claimed #18844; the seat's serial check read open PRs' file lists (none existed yet on either side) and did not scan Generated by Claude Code |
Fixes #18844
Clause-②: no
What changes
scripts/check-single-claim-paths.mjsre-execs itself once with node's proxy flag whenHTTPS_PROXY(orhttps_proxy) is set and the process was not started with it — argv, env and stdio forwarded, the child's exit code returned. Nothing else in the file moves: no new exit code, no change to the verdict layer, and no change to theEXIT_NOT_WIREDrouting PR #17362 landed.The decision is imported, never restated —
PROXY_FLAG,PROXY_REARM_GUARDandproxyRearmPlanfrom the half-states patrol, the same importpost-stamped.mjsandcheck-prior-rulings.mjstake — so this gate and they cannot come to disagree about whether this container'sfetchreaches the API. Only the guard variableOS_SINGLE_CLAIM_PATHS_PROXY_REARMEDis this file's own: one shared variable would let another instrument's re-exec suppress this one's, and the symptom would be the silent 401 the shim exists to close (a case the self-test pins in both directions).Before / after, measured in an agent container
Readings at
0dca68d1e, every exit code captured before any pipe (cmd > log 2>&1; EXIT=$?).GITHUB_REPOSITORY=objectstack-ai/objectstack PR_NUMBER=18921 node scripts/check-single-claim-paths.mjsError: GitHub API 401 for /repos/objectstack-ai/objectstack/pulls/18921/files?per_page=100&page=1, thrown and unhandledℹ️ re-exec with --use-env-proxy: HTTPS_PROXY is set (http://127.0.0.1:45311) and node's fetch does not read it.then✓ check:single-claim-paths: PR #18921 modifies none of the 1 declared at-most-one-writer path(s), so there is nothing to serialise.node scripts/check-single-claim-paths.mjs(no PR context)NOT WIREDtextdiff -qon both logs), no re-exec line, no child spawnednode --use-env-proxy … PR_NUMBER=18921(the child leg, standalone)OS_SINGLE_CLAIM_PATHS_PROXY_REARMED=1GitHub API 401— the loop guard holds in a real process, not only in the pure functionHTTPS_PROXY/https_proxyunsetGitHub API 401GitHub API 401— no behaviour change when the variable is absent (the CI-runner leg)⛔ No token value is printed anywhere above or in the code. This container sets both
GITHUB_TOKENandGH_TOKEN; the script readsGITHUB_TOKEN, unchanged, and the credential that makes the routed leg answer is the proxy's, not that variable's.Which run of this gate the probe exercised:
package.json'scheck:single-claim-pathsis the self-test; the live verdict belongs to thesingle-claim-path-guard.ymlworkflow. The table's live rows drive that live path by hand with the workflow's own variables; the self-test is the last row of the gate table further down.Where the guard sits — and the hypothesis that falsifies
The dispatch takes the routing decision after the PR-context read and only when a read is imminent:
--self-testis offline by contract and never routes;PR_NUMBER, or withPR_NUMBERbut noGITHUB_REPOSITORY/GITHUB_TOKEN, exitsEXIT_NOT_WIREDhaving made no request, so it spawns nothing — which is why its text stays byte-identical and no informational line lands in front of it.A "top-of-file guard, before anything else" was the shape proposed for this card. Two landed spellings say otherwise and this one mirrors them:
post-stamped.mjsdispatches--self-testin a branch the re-exec cannot reach and skips the re-exec on--dry-run("that path makes no request, so re-execing it would spawn a second process to prove a route nothing is about to use"), andcheck-prior-rulings.mjsplaces the call at its card read, "after argument validation so a bad-usage run never pays for a child". The condition is a request is about to happen, not the process is starting.Verified the other half of that hypothesis too:
post-stamped.mjs's re-exec line does come from the patrol's exports (its import block namesPROXY_FLAG,proxyRearmPlan,EXIT_PREREQUISITE_NOT_MET), and the guard variable is per-file there as well.Self-test
node scripts/check-single-claim-paths.mjs --self-test: 65 cases / 7 batteries → 83 cases / 8 batteries, exit 0 both. The base count was taken by running the pre-change copy of the file out ofgit show HEAD:…and deleting it again, not by countingt(calls. The battery roster's own size pin rises 7 → 8, so deleting the new battery reds exactly as zeroing it does.The new battery pins the decision, offline, with no proxy present and no request made: proxy set routes (and names the variable in its reason); the lowercase spelling counts; no proxy routes nothing (the runner leg, unchanged); the flag already in
execArgv, inNODE_OPTIONS, orNODE_USE_ENV_PROXY=1routes nothing again; this file's guard stops a loop; the patrol's guard does not suppress this file's re-exec; a node that will not take the flag hints instead of re-execing. And where the decision is taken: a wired live run yes;--self-testno; no PR context no; an incomplete context no.The census the triage asked for — 13 candidates, read BY CALL PATH, ⛔ none fixed here
The triage's keyword sweep (files under
scripts/namingapi.github.comand carrying none ofHTTPS_PROXY | use-env-proxy | ProxyAgent | EnvHttpProxyAgent) returned 13, and declared itself a candidate lower bound rather than a verdict. Read by call path, the 13 are four different things:scripts/pm/changeset-deadline-census.mjsfetchin its API helper, but the dispatch calls a localrearmThroughProxybuilt on the imported planscripts/pm/check-clause2-carriers.mjsfetch, imported plan,spawnSyncre-exec--pairreads in this containerscripts/check-single-claim-paths.mjsscripts/check-whole-set-label-write.mjsapi.github.comstrings are self-test fixtures of thecurl -X PUTspelling it scans source text forscripts/check-cross-repo-closer-outcome.mjsexecFileSync('git', …); the API URL is a fixture, and its double harness carries anunstubbed('fetch')tripwirescripts/check-closing-target-claim.mjsfetchin agithubApi(token)helper byte-identical in shape to this card's; a throw is caught and declaredUNDETERMINEDscripts/check-issue-citations.mjsfetch(injectablefetchImpl), not routed — but it imports the plan and prints the route as the remedy, refusing withEXIT_PREREQUISITE_NOT_MET(3) so "could not resolve" can never pass for "resolves"scripts/measure-stall-guard-headroom.mjsfetchinfetchRunJobs, throws on a non-ok response with a remedy naming the token and its offline--frompayload pathscripts/pr-labels.mjsfetchwith retry/backoff; a 4xx breaks the loop and the failure is rethrown carrying anindeterminateclasspr-automation.yml,lint.yml), no local flowscripts/ci/select-shard-timings-run.mjsfetchinapi(), throws on a non-ok responseci.yml/shard-timings-refresh.ymlscripts/pm/check-governed-queue-guard.mjsfetchin its reader factories; a read failure becomesunreadableApproval(…), whosestate: 'unreadable'is not satisfied and carries its own exit classscripts/release-github-releases.mjsfetchincreateReleasesClient; a failure is collected intofailedand printed as::error::scripts/report-unmeasured-gate-tail.mjsfetchinfetchRunJobsthrows, and the caller catches it into an empty judgement whosereasonis "the jobs API could not be read: …", always exit 0What made the 13 not 13: two are already routed through the shared shim (an import leaves no keyword — exactly the blind spot the triage declared), and two read no network at all. Nine remain proxy-blind readers, this card's file included.
The triage's escalation condition, applied
The condition: a candidate a LOCAL flow depends on and that would give a WRONG verdict rather than a loud 401 ⇒ p1.
No row satisfies both. The two candidates a local flow really depends on today (
check-clause2-carriers,changeset-deadline-census) are already routed. Of the rest, every one either never runs outside CI — where no proxy exists, so the shim would change nothing — or fails loudly: a throw,exit 3with the route named, or a state that is explicitly not a pass.The closest row, and the one worth a p2 card rather than p1:
scripts/check-closing-target-claim.mjs. Measured here, not inferred —GITHUB_REPOSITORY=objectstack-ai/objectstack PR_NUMBER=18921 PR_HEAD_REF=claude/issue-18844-probe PR_BODY='Closes #18844' node scripts/check-closing-target-claim.mjs→ exit 0, with::warning::UNDETERMINED — #18844 was not judged: its comment thread could not be readabove a✓headline and a1 closing target(s) could not be judgedtail. So the read failure is named, annotated and counted — deliberately, as a transport hiccup is not something a PR author did — but a reader who takes the exit code alone reads a pass. Its only local wiring is--self-test; the live verdict isclosing-target-claim-guard.yml's. That is the same class as this card and the same one-line fix, so it is card material for the seat, not p1, and ⛔ not fixed here.Reader test
A dev running the derived gate list in a container now reads a verdict from this gate instead of recording NOT MEASURED.
skip-changeset, measured rather than assumed29 workspace package manifests, 23 declaring
files[]; no entry in any of them can reach a repo-rootscripts/path (npm packs from the package directory, and the root manifest isprivate: truewith nofiles[]). Positive control:packages/spec'sfiles[]readsdist,json-schema,liveness,prompts,llms.txt,README.md,src/**/*.zod.ts,CHANGELOG.md,api-surface,spec-changes.json. And the symbolcheck-single-claim-pathsappears in zero publishable trees (packages/,apps/,plugins/,skills/). Nothing published moves.Gates
Derived from this worktree with
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(the tool's own change set, not a hand-written list): 29 commands, every one run, every one exit 0, re-run in full on the rebased head0dca68d1ewith the codes recorded ascommand :: exit code, then reconciled with--ran(29 derived, 29 run, 0 unrun). The first derivation warned STALE TREE, so the branch was rebased ontoorigin/main54145ccecand the whole union re-derived (identical 29) and re-run there.Named in the dispatch and green:
check:self-test-wired,check:self-test-workflow-commands,check:scripts-symbol-anchors,check:bash32-floor,check:single-claim-paths,check:pm-dispatch-gates. Outside the derived 29 and stated as such by the tool: the artifact-roster families, the declared-wide families, the pending-changeset families and the path-scheduled CI job.Also run, beside the derived union:
npx eslint --no-inline-config --format json scripts/check-single-claim-paths.mjs— 1 file linted, 0 errors, 0 warnings. That narrowing is a measurement, not an omission: the file count is read from eslint's own JSON output, the universe is this repo's singleeslint.config.mjs, and that config states in its own prose that it "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file" — so this diff cannot move the verdict on any file it does not touch. The repo-wide scan remains CI's run. Control-byte self-scan over the changed file (grep -naPover the C0 range and DEL): no match;check:nul-bytesgreen.🤖 Generated with Claude Code
https://claude.ai/code/session_01BTeBejoPUvRHN8WdAJC6oF
Generated by Claude Code