Skip to content

ci-failure.mjs's walk has no transport net: a mid-walk 403 escapes as an uncaught throw, and node's exit 1 collides with this file's own EXIT_RED #10155

Description

@os-zhuang

Filed unassigned by the #9966 dev seat (session session_01DdCnBGcHeufjrq7drTD3wt), out of that card's scope: #9966 is scoped to the transport probe, and this is the walk one layer down. Duplicate search first: keyword sweep over all 231 open issues for the ci-failure / uncaught / walk / transport shapes, plus a semantic search — no hit. Adjacent but distinct: #9966 itself (the probe's false green, being fixed now), #10141 (this file's reachability claim about raw logs), #9898 (this file's --self-test runs nowhere in CI).

Measured, 2026-08-20, in an agent container

Against scripts/pm/ci-failure.mjs as it stands on origin/main (2d3860df9), pointed at a repo this session's egress proxy refuses repo-scoped:

$ PM_SWEEP_REPO=objectstack-ai/objectui node scripts/pm/ci-failure.mjs --sha 2d3860df9aad...
Error: GET /repos/objectstack-ai/objectui/commits/2d3860df9aad.../check-runs?per_page=100&page=1 -> HTTP 403
at rest (.../ci-failure.mjs:608:19)
at async checkRunsFor (.../ci-failure.mjs:619:18)
at async walk (.../ci-failure.mjs:690:15)
EXIT=1

Why the exit code is the defect, not the stack trace

rest() throws a plain Error on any non-ok status, and nothing between it and the top level catches. Node exits 1 on an uncaught exception — and this file's own exit table assigns:

 * 1 RED failing checks, and the assertion text was retrieved for
* EVERY one of them. The output is the answer.

So a container that could not read one byte about the tree hands its caller the code that means "the tree is red and here is the proof". Any script branching on $? — the reading the header explicitly instructs (Piping hides all of it. Read $?) — reads a transport refusal as a confident verdict about the tree. That is the #4690 inversion this file is built to refuse, arriving through the one path the file does not guard.

Note the shape is strictly worse than a missing verdict: exit 2 (UNDETERMINED) would be honest, and the walk never reaches it.

Scope note — #9966 does not close this

#9966 adds a repo-scoped probe stage, so the class-4 container now exits 3 before the walk starts, and the repro above stops reproducing. What remains uncovered is any transport failure arriving mid-walk, after the probe passed: quota exhausted between the probe and the last annotations call, a repo whose access changes under the run, a transient 5xx, a 404 on an annotations URL. The probe cannot cover those by construction — it fires once, at the start.

The decision this needs (which is why it is not a rider on #9966)

What should a mid-walk transport failure exit as? The two candidates are already both in the file's vocabulary and mean different things:

A related sub-question: a mid-walk failure after some checks were read must not print a partial result that reads complete — the same invariant swept keeps true one file over.

Generated by Claude Code

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions