{
  "summary": "#312 root cause confirmed in source: gh_status treats the aggregate `gh auth status` exit code as effective-credential validity, discards diagnostics, and aborts fetch before its working gh release carrier runs. run_bounded_command DOES inherit the invoking spt process environment, including GH_TOKEN. No commands, auth checks, secret inspection, edits, builds, or tests performed.",
  "files": [
    {
      "path": "crates/spt/src/cli.rs:10018-10112",
      "description": "Duplicate aggregate-status probes; GhStatus erases failure detail; fixed-string authentication refusal."
    },
    {
      "path": "crates/spt/src/cli.rs:10970-11145",
      "description": "Fetch preflight veto precedes signed-set download; subsequent metadata and artifact verification must remain unchanged."
    },
    {
      "path": "crates/spt/src/cli.rs:9643-9680",
      "description": "Both composite plans use cmd_update_fetch; core failure aborts adapters and restart finish."
    },
    {
      "path": "crates/spt-runtime/src/runtime.rs:960-1010,1027-1034,1075-1127",
      "description": "Monotonic timeout/kill/reap; plain Command child construction inherits environment; no GH-token stripping."
    },
    {
      "path": "crates/spt/src/cli.rs:10560-10639,22491-22537",
      "description": "Existing bounded positive API/download patterns: gh latest-release query and asset downloads."
    },
    {
      "path": "crates/spt/tests/fixtures/gh_fixture.rs:1-74",
      "description": "Existing PATH-injected native gh fixture; currently always passes auth and treats every API invocation as latest-tag query."
    },
    {
      "path": "crates/spt/tests/composite_e2e.rs:38-68,103-230",
      "description": "Reusable signed set, isolated home, fake gh PATH, child-local environment, and observable installed-byte fixture."
    },
    {
      "path": "crates/spt/src/cli.rs:35333-35357",
      "description": "gh_failure_classes_render_os_correct_hints is a wording/render-only test; remove rather than repin changed prose."
    },
    {
      "path": "docs/DEBUG-ROLLOUT.md; docs/adr/0016-platform-targeted-update-sets.md; docs/adr/0036-private-fork-distribution.md",
      "description": "Carrier-independent signatures, channel/version/platform policy, mandated gh carrier, and maintainer-only debug tooling authority."
    }
  ],
  "architecture": "CLI dispatch → cmd_update_composite or direct update fetch → cmd_update_fetch → gh_status → spt_runtime::run_bounded_command → Command::spawn/wait_bounded. Only after Available does fetch download signed metadata and artifacts through gh, verify them, stage, and optionally apply. Authentication preflight is local CLI policy, not release trust authority.",
  "report": "## Exact RCA / environment\n- Source chain: direct fetch dispatch cli.rs:2489; composite FetchApply/Fetch cli.rs:9647-9648 → cmd_update_fetch:10970 → gh_status:10062. `Ok(out) if out.success()` alone yields Available; NotFound yields Missing; EVERYTHING else yields Unauthed (:10069-10075). BoundedOutput::success is strictly status_code == Some(0) (runtime.rs:591-600).\n- The issue's measured working release-list with GH_TOKEN is accepted as ground truth: https://github.com/BigscreenVR/spt-bs-releases/issues/312. GitHub documents aggregate status failure for ANY account on ANY checked host: https://cli.github.com/manual/gh_auth_status. Therefore even another host's stale account can veto this gate. Checking “any success line” is not a safe fix either: a non-effective account may work while the effective environment token fails.\n- Environment question is resolved: runtime.rs:1033 delegates to run_bounded_command_in; :1084-1127 tokenizes argv, calls Command::new, configures stdio/Windows CREATE_NO_WINDOW, then spawn. No env_clear, env_remove, explicit allowlist, or manifest environment filtering occurs on this free-function path. Empty BTreeMap means no template substitutions, NOT empty child environment. Child inherits the invoking spt process environment. GitHub's documented GH_TOKEN > GITHUB_TOKEN > stored credential precedence explains why the release read succeeds: https://cli.github.com/manual/gh_help_environment. This establishes inheritance from spt, not what credentials any independently launched process happens to possess.\n- Refusal loses all useful evidence: stdout/stderr already exist in BoundedOutput, but gh_status discards them, GhStatus::Unauthed carries nothing, render_gh_auth_required takes no parameters, and cmd_update_fetch prints its generic message then returns 1 (:10984-10986). Composite returns that code and skips remaining legs (:9670-9676). No cache opening or download is reached.\n\n## Minimal compatible implementation seam\nRecommended: make `gh_status(repo: &str)` use a positive, read-only release-channel probe FIRST, e.g. `gh release list --repo {repo} --limit 1 --json tagName`, through the existing template-key bounded runner. Pass the already-resolved install_repo() from cmd_update_fetch. This closely matches the user's proven working operation, shares actual fetch repo/host resolution, and avoids requiring user-profile permissions unrelated to release access. Command options are documented at https://cli.github.com/manual/gh_release_list. Success means readable channel, even an empty release list; missing update sets remain downstream download failures. Do not require a latest release just to authorize a pinned `--tag` fetch.\n- Positive success → Available immediately, regardless of stale stored accounts. Spawn NotFound → existing Missing/install hint. Failed/timeout read → preserve nonzero refusal, never mark success because a token exists or a different account passes status. Existing two failure classes can remain; describe inability to authenticate/read the channel rather than asserting every failed request proves invalid credentials (network, scope, and repo access failures also exist).\n- Do NOT use `gh auth token` as the positive authority: it retrieves a token rather than proving it works and places credential bytes in captured stdout. `gh api user` is a positive authentication pattern but tests a different permission surface than release reading.\n- On failed positive probe ONLY, bounded `gh auth status` may supply optional stale-account diagnostics, never the success decision. Carry sanitized optional account information in Unauthed and into render_gh_auth_required; this is an internal enum/render/caller change, not a wire format change. Remove Copy if owned strings are carried.\n- Preserve the current 10-second preflight budget, not two sequential full ten-second waits: record one Instant/deadline, give the positive read the budget and optional diagnostic only its remaining duration; skip diagnostics when exhausted. Preserve NotFound/timeout handling, null stdin, CREATE_NO_WINDOW, and existing 300-second asset / 60-second adapter-version bounds. Existing wait_bounded uses monotonic elapsed time, polls every 20 ms, and kills/reaps on timeout (runtime.rs:986-1004); it is not a strict end-to-end scheduler guarantee. No runtime environment changes are required.\n- Existing positive patterns to reuse: gh_version_command(:10564) builds `gh api repos/{repo}/releases/latest --jq .tag_name`; gh_latest_release_version(:22520) accepts only successful command output; fetch_release_asset_bytes(:10620) likewise accepts successful downloads. None is currently an effective-credential preflight.\n- Related but separate duplicate: gh_available(:10021) also uses aggregate auth status and drives adapter Auto at :10853 and :22287. Explicit Gh skips it. Its stale-account behavior can still make a composite adapter leg fall back to HTTPS. Do not silently expand #312 into adapter policy work; Main should explicitly decide whether that companion is in the wave. If included, use the actual adapter repo in its positive probe and migrate both Auto call sites rather than checking the core release repo for adapters.\n\n## Safe stale-account naming\nUpstream source evidence: https://raw.githubusercontent.com/cli/cli/trunk/pkg/cmd/auth/status/status.go — authEntry.String emits `Failed to log in to <host> account <login> (<source>)`; timeout has a DIFFERENT `Timeout trying to log in...` line, and token-only failures have no account. statusRun evaluates the active token and additional stored accounts and accumulates any failure.\n- Smallest older-gh-compatible seam: pure parser extracts ONLY host/login from explicit failed-account lines in captured stderr/stdout; validate conservative bounded ASCII host/login characters, discard ANSI/control/unrecognized input, and never print whole gh output, token-source paths, Token lines, or error blobs. Do not mistake Logged-in or Timeout lines for confirmed stale accounts. Refer to “gh reports failed account ...”, not “this account caused the release-read failure.” Account metadata is advisory; omission on unknown formatting is safe.\n- Structured `gh auth status --json hosts` is an alternative only with a supported-gh-version assumption. Current JSON has host/login/state/tokenSource; token is omitted without --show-token. Critical trap: JSON status exits ZERO even when unauthenticated, so its exit code must NEVER become the positive probe. Installed gh version/JSON support is unmeasured; no secret-bearing output was inspected. Never auto logout, switch accounts, refresh, modify credential storage, or print --show-token.\n\n## Fixtures / observable regression contracts\n- Extend existing gh_fixture instead of a second fake. Today its auth arm always succeeds and API arm accepts any endpoint, so it cannot distinguish this bug. Add narrowly defined read-probe success/failure and stale-status modes; use a synthetic child-local GH_TOKEN marker and check it inside the fake without logging it. Existing argv log is enough to observe downloads without recording secrets.\n- Use composite_e2e signed_set_json/test-key/home/PATH setup, but drive `spt update fetch` WITHOUT --apply for the new gate regression: assert successful signed metadata/artifact staging despite status exit 1 + stale account, using a deliberately fake token marker. No real auth/network required and no binary swap needed. This catches both the gate bug and lost environment inheritance.\n- Negative case: effective read fails even with another account reported successful → nonzero GhAuthRequired; no download or newly staged set. Optional stale account appears safely in diagnostic; synthetic secret-bearing lines and terminal escapes never reach output. Missing executable remains distinct GhCliRequired. A hanging probe must refuse within the bounded runner policy and never start download. Prefer injected runner/deadline seam for deterministic classification/budget edges, actual fixture for the subprocess/environment/staging contract.\n- Existing runtime bounded_run_kills_on_timeout (:1724-1741) exercises the shared wait_bounded via ManifestRuntime, not gh_status itself. Keep it; no runtime change is needed.\n- Remove gh_failure_classes_render_os_correct_hints (:35337), rather than repinning its contains-prose assertions. Move meaningful failure-class/exit/no-staging evidence into behavioral tests above; retain OS hints in production. resolve_transport tests (:26539-26568) exercise explicit-vs-Auto policy and remain useful if adapter work is approved.\n\n## Authority / uncertainty / scope\nADR-0036 §3 mandates gh carriage and loud missing/unauthed refusal; positive read changes evidence for availability, not the carrier or release trust. Keep verify_update_set_metadata (:11045), verify_update_set_artifact (:11106), signing anchors, channel pinning, rollback floor, consent/apply, and debug restrictions unchanged. Traceability owner is REQ-UPDATE-GH-TRANSPORT (traceable-reqs.toml:2611-2613); replace removed render-only unit evidence with actual classification/parser behavior. Debug runbook was read; its validation instructions were intentionally not executed under this read-only assignment. No LSP device is available in the mounted inventory; references were resolved by repository search. Upstream status format is current trunk, not evidence of the field gh version. No claim of runtime validation or ticket closure is made; this is implementation-ready source RCA."
}