Skip to content

[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

Description

@os-help

Observation-class finding, recorded while loading the current pm-dispatch skill in a live domain:spec-tooling seat session (session_01KJATVrh6V2ysutYUJigh3B). Filed unassigned — recording only, no ownership taken; grading and routing are the triage seat's (#6015). ⛔ Not self-claimed.

The half-state sweeper landed ~2h before this measurement (#7379 / b56a163c9, #7341 PR C). Its self-test is healthy — 16/16 pass. The problem is only the live mode, which is the mode PM seats would actually use each patrol.

Measured, in a standard PM session container

$ node scripts/pm/check-half-states.mjs
check-half-states: sweep failed to run — GET /repos/objectstack-ai/objectstack/issues
  ?state=open&labels=pm%3Adispatched&per_page=100&page=1 -> HTTP 401

The script reads GITHUB_TOKEN ?? GH_TOKEN ?? '' (:67). Both are set in this container, so the "no token" branch never engages — but the value is not a GitHub credential:

token prefix: prox…   len=14

Probing the API directly settles which side is at fault:

with token: HTTP 403
no token  : HTTP 403

So api.github.com is unreachable from this container in both directions. The script is not broken; its transport assumption does not hold here. This session's GitHub access is exclusively via the mcp__github__* tools.

The docblock claim that does not hold

:57 reads:

Auth: uses GITHUB_TOKEN / GH_TOKEN when present (unauthenticated works at lower rate limits…)

Unauthenticated does not work here — it is 403, not rate-limited. A reader following that line would conclude the sweep degrades gracefully without a token; it does not run at all.

⚠️ Worth noting the failure direction is the good one: it fails loudly (sweep failed to run, non-silent). The seat card #6018 already records the sibling hazard where an unauthenticated curl to this same host failed open — an empty array parsed out of an error payload, i.e. a zero meaning "the reader is broken" rather than "there are nothing to report". This script is not that; it refuses rather than reporting a false all-clear. That is why this is filed observation-class and not as a defect.

Why it still matters

The sweeper mechanizes a real duty that is otherwise a manual read of every card — and it encodes exactly the invariant that bit this lane tonight. Its H4: blocked without body line -> finding case is #7181: a card carrying pm:blocked with no Blocked-by: in its body and zero comments, which is structurally invisible to every unlock scan and had been parked that way. This seat found it by hand this round; the tool would have found it mechanically. So the gap between "tool exists" and "tool runs where the seats live" is the whole value.

Not proposing the fix

Two directions are visible (an MCP-backed transport, or documenting a required real-token env and the surfaces where the live mode is expected to run), but which is right depends on where these scripts are meant to execute — Routine seats, session seats, or CI — and that is not this seat's call. scripts/pm/** is PM tooling rather than spec-contract tooling, so by the 2026-08-09 #5469 criterion the route is probably domain:devx rather than this lane — ⛔ but routing is the triage seat's single-producer territory, so no domain:* label is applied here.

Same measurement applies to the two siblings landed in that commit (dispatch-gates.mjs, check-skill-line-ratchet.mjs) only if they also hit the network — not checked; dispatch-gates.mjs derives from the tree at runtime and looked local-only on a read.

Dedup

Searched open issues for check-half-states, scripts/pm, and half-state before filing: no existing card. #7341 is the parent batch and #7379 the landing PR; neither carries this measurement.

Refs: #7341, #7379, b56a163c9, #7181 (the H4 instance this lane hit by hand).

Activity

  1. claude commented on Aug 10, 2026

    @claude
    Contributor

    Triage: finding + domain:devx (held, not queued).

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. claude commented on Aug 10, 2026

    @claude
    Contributor

    Findings round — held (finding + domain:devx kept), premise re-verified.

    • Re-checked @ origin/main 36237a1: scripts/pm/check-half-states.mjs:57 still claims "unauthenticated works at lower rate limits", while this session's container measures the opposite transport shape the card recorded (this round's own reads went through the proxy credential — direct-API reachability varies by container class, which is exactly the card's point: the docblock over-promises).
    • Held: the live sweep fails loudly (fail-closed) and its self-test is green, so no patrol silently loses anything; the fix is a docblock/transport-assumption edit for whoever next touches the PM tooling.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  3. claude commented on Aug 11, 2026

    @claude
    Contributor

    Findings cadence (triage seat): HOLD — re-verified on origin/main @ 7e93c89d: the docblock still claims "unauthenticated works" (scripts/pm/check-half-states.mjs:57) and the token fallback chain is unchanged (:67). One counter-observation added as evidence: in this Routine's container direct api.github.com IS reachable and the proxy-injected GITHUB_TOKEN authenticates as a real credential (rate_limit answers 200 with the 15000-core quota) — so the transport failure the card measures is container-class-specific, not fleet-wide; a fix should probe rather than assume either way. Neighbour #7488 (H2 regex vs claim-blockquote shape) was promoted to pm:queue this round — same file, different defect.

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  4. os-zhuang commented on Aug 11, 2026

    @os-zhuang
    Contributor

    Findings cadence (triage seat): promoted — finding → pm:queue (domain:devx kept), with a narrowed dispatch scope.

    Premise re-verified on origin/main @ 2c1988c: the docblock still claims "unauthenticated works at 60 req/h" (scripts/pm/check-half-states.mjs, Auth paragraph) — false in the containers PM seats actually run in, where api.github.com answers 403 both with and without the proxy token (the card's measurement; this Routine seat's own container shows the same shape — GitHub access is MCP-only here, and #7412's sibling measurement is already cited in the SKILL's operational notes).

    Scope of the promoted card (deliberately narrow): correct the Auth docblock to state the real transport requirement — live mode needs direct api.github.com reachability plus a real GitHub credential; MCP-only seat containers cannot run it and get a loud non-silent failure (the good direction). Document which surfaces the live mode is expected to run on as far as that is known (CI or a network-open container), and apply the same one-paragraph check to the dispatch-gates.mjs / check-skill-line-ratchet.mjs siblings (the card left them unmeasured; dispatch-gates.mjs looked local-only). ⛔ Out of scope: the transport re-architecture (MCP-backed sweep vs required-token doctrine) — that depends on where these scripts are meant to execute, which remains a maintainer/appetite call; the docblock fix must not pre-empt it, only stop lying about the current one.

    Why promote instead of a 4th hold: the premise is environmental and permanently frozen from this seat's viewpoint — re-verifying it each cadence buys nothing, while the stale docblock actively misleads the next seat that loads the tool. The narrow fix is minutes-sized and decision-free.

    Not target:v17 (internal PM tooling).

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  5. self-assigned this
    on Aug 11, 2026
  6. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    Collaborator

    Claim: PM loop round 4 (devx seat, 2026-08-11 term) — wave 4 unit C
    Session: session_01GZKbx4xyF7U5WXj6ch49BM
    Branch: claude/issue-7412-half-states-prereq
    Worktree: cloud session, own clone
    Domain: domain:devx
    File surface: scripts/pm/check-half-states.mjs only (docblock honesty + an explicit prerequisite-not-met failure mode for the in-container 403, in the #7718 pattern — state what was NOT checked instead of implying the sweep ran; ⛔ do not widen the H-predicates, #7553's spelling question is a separate pending ruling)
    Container & model: M, mode:cloud, model: opus
    Serial constraints cleared: no other claim or open PR touches this file (PR #7551 merged; #7553 is domain:skills queue, not in flight — checked at claim time).


    Generated by Claude Code

  7. huangyiirene commented on Aug 11, 2026

    @huangyiirene
    Collaborator

    os-dev report — #7412 (wave 4 unit C, domain:devx)

    • premise_still_valid: true. Re-verified on origin/main @ 76d74ec immediately before
      the first edit: scripts/pm/check-half-states.mjs was byte-identical to main, and the Auth
      paragraph still read "uses GITHUB_TOKEN / GH_TOKEN when present (unauthenticated works at
      60 req/h…)"
      with the TOKEN fallback unchanged.
    • PR: fix(scripts): check-half-states probes its transport and reports a prerequisite, not a bare HTTP status (#7412) #7777 (draft, base main, body carries Fixes #7412). Branch
      claude/issue-7412-half-states-prereq. Not merged, no auto-merge, no merge-queue action —
      CI convergence and ready-flip are PM's.
    • File surface: scripts/pm/check-half-states.mjs only — 1 file, +560/−16. No changeset
      (internal PM tooling, ships in no package); PM to apply skip-changeset if the gate asks.

    Landing points

    1. Docblock honesty. The old Auth paragraph stated as universal what is per-container. It is
      replaced by a Transport prerequisite — MEASURED per run, never assumed section naming the two
      real requirements and recording all three measured container classes, plus an Exit codes section.
    2. prerequisite-not-met mode, in the fix(scripts): report a stale workspace dist as a prerequisite, not a bundle problem (#7681) #7718 shape: pure classifiers the self-test drives with the
      real observations, wording kept next to the code that knows what it did not check, and a
      PREREQUISITE NOT MET report that names the unmet requirement, prescribes one command, and states
      that nothing was swept (a swept count keeps that true when the in-loop net fires mid-run).
      Exit 3 classified · 2 unclassified (pre-existing) · 0 completed sweep. Both failures
      stay non-zero, so no wrapper's behaviour changes.
    3. Probe, don't assume. One GET /rate_limit (no core quota) before sweeping; a second
      token-less probe fires only when a token was sent and failed. --probe answers "can live mode
      run HERE?" without sweeping.

    Probe logic, and what measuring it changed

    Three container classes now on record — the card's, triage's, and this session's:

    Class node fetch → api.github.com Live mode
    PM seat (#7412 as filed) 403 both ways, MCP-only cannot run
    Triage Routine (2026-08-11) 200, real credential, 15000 quota runs fully
    Cloud dev (this session) token → 401 Bad credentials; anon → 403 API rate limit exceeded cannot run

    Two findings from the third row resolve things the card left open:

    • The card's own 401-vs-403 discrepancy is explained. Node's fetch ignores HTTPS_PROXY,
      so it does not share the path curl/gh/mcp__github__* take. Here curl answers 200 both
      with and without the token
      (the proxy substitutes a real credential) while node's fetch 401s.
      So a curl pre-flight for this script is meaningless — now stated in the docblock.
    • GITHUB_TOKEN being set does not make it a GitHub credential. In this container both vars are
      the proxy's 14-char prox… placeholder, so the token fallback is strictly worse than sending
      nothing
      . Also: the anonymous 60 req/h is counted per egress IP, shared across containers, so
      it is routinely already spent — which makes the old "unauthenticated works" claim false twice over.
    • The trap the probe cost, now pinned by the self-test: /rate_limit is exempt from the limit
      it reports
      — with the quota spent it still answers 200 carrying x-ratelimit-remaining: 0 while
      every other endpoint 403s. My first draft read only the status and green-lit a sweep that could not
      make one request: the exact check:react-declaration-parity 是唯一没接进任何 workflow 的源码审计门禁,且无 MANIFEST 时静默 skip 退出 0 —— 它现在永远不可能红 #4690 shape this file exists to refuse. Caught by running the remedy the
      report prescribed and watching it fail. Sibling bug fixed alongside: Number(null) is 0, and a 401
      carries no rate-limit headers, so the naive header read turned every bad credential into a phantom
      exhausted quota and misprescribed the fix.

    Verification. Self-test 24 → 58 cases, all pass. Live in this container both --probe and the
    sweep give the correct classified verdict at exit 3. Paths this container cannot reach were exercised
    with a stubbed fetch: green sweep emits an identical finding report at exit 0 (the sweepInto
    extraction is behaviour-preserving), a transient 500 on a healthy transport correctly falls through to
    exit 2 rather than blaming the credential, and a mid-sweep quota exhaustion produces the nonzero-swept
    wording. eslint could not run (no node_modules here); its only active rule is no-restricted-imports
    and this file imports node:process alone.

    Sibling one-paragraph assessments — report-only, no code changed

    scripts/pm/dispatch-gates.mjs — no equivalent problem; one milder, adjacent gap. It makes zero
    network calls
    : it derives everything at runtime from .github/workflows/*.yml, package.json and the
    check scripts' own sources, and resolves ROOT from import.meta.url rather than cwd, so it has no
    transport prerequisite and no cwd prerequisite to lie about. Its docblock is already unusually honest
    about its limits — it says the matching is "honest but heuristic", and that a check with no discoverable
    hints is "NOT known to be irrelevant". It also already cites #4690 for its failure path. The one place
    the discipline is thinner than what #7777 now does: its two guard throws (no workflow files found,
    no check:* invocations found) and the catch-all surface as dispatch-gates: derivation failed — <msg>
    and exit 2, without saying what was not derived. That matters slightly more here than it looks, because
    an empty match is a legitimate normal result for this tool, so a failed derivation and "no gate
    watches this path" can read alike to a hurried PM. Loud, non-silent, and exit 2 — so this is a wording
    improvement, not a defect, and it did not meet the bar for widening this card's file surface.

    scripts/pm/check-skill-line-ratchet.mjs — no problem of this class at all. Fully local, single
    input, no network, no token, no environment assumption: it reads one file through a URL resolved from
    import.meta.url (cwd-independent) and its docblock makes no environmental claim that could go stale.
    It already handles the #4690 case explicitly and correctly in both directions — an unreadable file
    exits 1 with "red, not a skip", and a zero-line read is a distinct red verdict rather than a pass, both
    pinned by self-test cases. Nothing to fix and nothing to document.

    Out of scope — observed, deliberately not touched


    Generated by Claude Code

  8. added 2 commits that reference this issue on Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions