Skip to content

check-half-states --probe reports PREREQUISITE NOT MET in a container that reaches the API — Node's fetch bypasses the agent proxy, and the refusal reads as the container's verdict #13544

Description

@claude

What I measured

scripts/pm/check-half-states.mjs --probe refuses to run in the triage Routine container, and its refusal is wrong. The container reaches api.github.com fine; the script does not use the channel that reaches it.

The probe's verdict here

check-half-states: PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential

  `GET /rate_limit` with GITHUB_TOKEN/GH_TOKEN = prox… (len 14) -> HTTP 401.
  …
  Fix:  export GITHUB_TOKEN=<a real GitHub token> and re-run

The same request, same token, same container, via curl

request result
GET /rate_limit with $GITHUB_TOKEN HTTP 200core.limit 15000, remaining 14998
GET /rate_limit without a token HTTP 200, same body
positive control, same host & token: GET /repos/objectstack-ai/objectstack/issues?labels=pm:retriage&per_page=3 HTTP 200, 3 issues returned

The probe reports 401 for the endpoint that answers 200 one process away. The token really is the proxy placeholder (len 14, prefix proxy) — that part of the diagnosis is correct — but the conclusion drawn from it is not: this container's outbound HTTPS goes through an agent proxy that supplies the real credential. curl honours HTTPS_PROXY; Node 22's fetch does not, by default. So the script sends the placeholder straight to GitHub and gets the 401 it then reports as the container's verdict.

The fix, measured

$ NODE_USE_ENV_PROXY=1 node scripts/pm/check-half-states.mjs --probe
✓ check-half-states: transport prerequisite met — api.github.com is reachable and the token authenticates.

$ NODE_USE_ENV_PROXY=1 node scripts/pm/check-half-states.mjs      # EXIT=0
… swept 227 open pm-/p0-labeled issue(s), 335 open issue(s) in the unscoped pass,
  11 open PR(s) … — 217 half-state(s) found.

One environment variable turns "this container needs a real credential" into a complete 217-row sweep.

Why this is p1 rather than a nit

A diagnostic that reports the opposite of the truth, on the exact question of whether a reading is possible. The repo already has a name for the adjacent failure — #4690, "could not read the input" must never look like "input is clean". This is that class inverted: readable looks like unreadable. The inverted direction is the more durable one, because it costs the tool nothing and costs only the caller: a probe that says "you can't" is not argued with.

Two concrete consequences, both live today:

  1. The file's founding claim is stale. The header and half-state-patrol.yml both justify the move to a scheduled Actions caller with: "the live sweep cannot run inside a PM session container at all ([finding] scripts/pm/check-half-states.mjs live sweep cannot run in a PM session container — direct api.github.com is 403 with or without the token, and the docblock's "unauthenticated works" is false here #7412 class 1 — api.github.com refuses that egress in both directions, with and without a token)". Whatever was true when that was measured, it is not true in this container now — and the same header already names the triage Routine container as reachable ([finding] scripts/pm/check-half-states.mjs live sweep cannot run in a PM session container — direct api.github.com is 403 with or without the token, and the docblock's "unauthenticated works" is false here #7412 class 2). The two halves of the docblock disagree and the probe was the thing that should have caught it.
  2. No lane can get the data on demand. The scheduled patrol runs 4×/day and lands a body that (this run) rendered 70 of 266 rows, with "196 further row(s) omitted to fit GitHub's issue-body limit". On-demand self-service is the release valve for that trim, and the probe closes it.

⛔ Not claimed here: that the trim has ever silently dropped a row that mattered. I tried to establish exactly that and failed to — see "What I could not establish" below.

Scope, and one constraint that must not be missed

⚠️ This file is copied VERBATIM into sibling repos (#11217, HALF_STATE_ANCHOR_ISSUE). Any fix must be a no-op on a GitHub Actions runner, where no proxy env vars exist. NODE_USE_ENV_PROXY=1 satisfies that (it configures from env vars that are absent there), but it emits an experimental warning:

(node:1754) [UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental

⇒ Two shapes, executor's choice, but the choice must be measured, not argued:

  • (a) set NODE_USE_ENV_PROXY=1 from within the script (or its shebang/wrapper) — smallest diff, carries the experimental warning;
  • (b) construct an undici ProxyAgent/EnvHttpProxyAgent explicitly and pass it as the dispatcher — no warning, more code, and pins a dependency shape.

Whichever lands, the probe's failure text must also be corrected: it currently prescribes export GITHUB_TOKEN=<a real token>, which is the wrong remedy in a proxied container and will send the next reader looking for a credential they do not need.

Acceptance

  1. --probe returns ✓ in a proxied agent container with the placeholder token unchanged — ⛔ not by supplying a real token, which would be fixing the message rather than the defect.
  2. --probe still returns ✓ on a GitHub Actions runner (the existing scheduled run stays green).
  3. --probe still returns the PREREQUISITE NOT MET path, with exit 3, when the transport genuinely cannot reach api.github.com — the refusal must survive, or the fix has replaced a false negative with a false positive.
  4. Non-vacuity, on the real configuration. Prove (1) by running the full sweep in that container and showing a non-zero half-state count against the live board — ⛔ a fixture, a recorded response, or a probe-only ✓ does not discharge this. The reference reading taken while filing: 217 half-state(s), exit 0, 2026-08-30T19:3xZ.
  5. Correct the two docblocks (this file's header and half-state-patrol.yml) where they assert the sweep cannot run in a PM/agent container. ⛔ Do not delete the scheduled caller — it is right for its own reasons (check-half-states is a healing mechanism with no standing caller — eight malformed claims and an unenumerated two-lane backlog went unseen until the maintainer asked #9844: an alarm nobody runs is silence); this only restores the on-demand path beside it.

What I could not establish, stated so nobody inherits it as fact

I expected to find that the anchor's size trim had silently swallowed H18 — the predicate that names the triage lane's own duty — because the published body (13:42:19Z) renders zero H18 rows while the live board carries 13.

Measured, and it does not hold. The three old carriers (#12104, #12237, #12892) were labeled pm:retriage at 15:01:28Z–15:01:35Z, and every bare-label carrier was filed from 14:12Z onward — all after the 13:42Z sweep. The only possible carrier at that moment (#13216) was ~1.4h old against a 2h threshold, so H18 was correctly silent. The trim is real and self-declared; it is not established that it has ever hidden an H18 row, and this card does not claim it.

Provenance

Filed by the triage seat (#6015) from readings taken in-container at 2026-08-30T19:3xZ, origin/main at 74049254. ⛔ Nothing was edited and no PR was opened. ⚠️ Filed over the REST channel to preserve the body intact (#12886 / #13385 truncate on the MCP channel); that channel records the author as claude[bot] rather than os-project-manager — same seat, different transport.


Generated by Claude Code

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions