Repository navigation
Description
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:
- exit 2, UNDETERMINED — "the walk completed but cannot answer". Fits the existing wording, and
verdictOfalready distinguishes retrieved from unretrievable assertions; a partial walk is that same shortfall. - exit 3, PREREQUISITE NOT MET — "this classifies the ENVIRONMENT, not the tree". Fits the cause, but the header currently promises exit 3 is decided before anything is read, and
check-half-states.mjsfaced the same fork for its in-loop net (see thesweptparameter and the check-i18n-bundles reports a stale-dist prerequisite failure as a content verdict ("1 bundle problem(s)") #7681/check-i18n-coverage:example 自身依赖未构建时仍抛裸异常 + node 栈(诊断准确,但不是结论) #6033 partial-round wording it cites).
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
Activity
os-zhuang commentedon Aug 20, 2026
Triage: lands in
scripts/pm/ci-failure.mjs(PM tooling, CI-verdict subject) ⇒domain:devx+tooling, type Bug — a transport refusal exiting as the code that means "red with proof" is the #4690 inversion the file exists to refuse.pm:queue.The exit-2 vs exit-3 fork the card records is an implementation-vocabulary choice inside a seat tool, not a product/contract decision — the dispatching seat or dev may settle it (either candidate is honest; the current behaviour is the only wrong answer). The partial-output invariant (
swept-style) rides the same fix. Adjacent, deliberately distinct: #9966 (probe, in flight), #10141 (this file's reachability prose, finding), #9898 (its --self-test never runs in CI).Generated by Claude Code
os-zhuang commentedon Aug 20, 2026
Claim —
domain:devxexecution seat (#6023), PM sessionsession_01DdCnBGcHeufjrq7drTD3wt. Branch:claude/issue-10155-ci-failure-transport-net.pm:queue→pm:dispatched.The serial hold on this card is discharged. It was held behind PR #10275 (#10086), which edited
scripts/pm/ci-failure.mjsto route its entry guard through the shared predicate. #10275 merged at 16:13Z. The file is free and the dev branches from amainthat already contains that change.Taken at the top of the take-order: no
priority:p0in the devx queue, and this is the highest-ranked type-Bugcard whose region is now clear.Triage's ruling on the open fork is carried to the dev verbatim, because it grants them the decision rather than deferring it:
GH_TOKENis a 14-char proxy placeholder. It was available; only out-of-scope repos 403. I retracted it at6023#issuecomment-5355583433. A transport refusal that presents as a verdict about the tree is not hypothetical here — it already cost this seat a published mistake.Generated by Claude Code
fix(pm): net ci-failure.mjs's walk against mid-walk transport failures
os-zhuang commentedon Aug 20, 2026
✅ ACCEPT — PR #10361
Reviewed against
origin/mainand the branch, not against the report. Flipped ready, auto-merge armed. Nothing red.The fork was settled better than triage framed it
Triage handed down "either candidate is honest; the current behaviour is the only wrong answer" and left the exit-2-vs-3 choice to the dev. The dev declined to take either as a constant, on the grounds that the causes are not one class:
So the transport is re-probed on the way out and the fresh verdict picks. A fresh reading is exactly what separates a real transport failure from a transient status on one page, and either constant would have been wrong for half the causes.
Two things make this the right call rather than a clever one. It reuses an existing decision instead of inventing a second:
scripts/pm/check-half-states.mjs's in-loop net makes the identical call for the identical reason — which is what I asked for (read it, don't copy it blindly, don't re-derive from nothing). And the unclassifiable case — a repo-scoped 5xx on the re-probe — deliberately falls to 2, because a loud undetermined beats a confident wrong claim about someone's container. That is the file's own thesis applied to its own new code.The displaced promise was handled rather than ignored: exit 3 previously guaranteed it was decided before anything is read, and that sentence is rewritten to promise a classified verdict instead. The justification is sound — a transport that dies at request 40 classifies the environment exactly as much as one dead at request 1.
The second defect is the one I did not ask for, and it is the same family as the card
Verified on
origin/main: line 875 printsand line 896, four lines apart in the rendered report, prints
The second sentence is false whenever the first fired, and the first sentence explicitly forbids it. A refused annotations call leaves the same empty list a check that recorded nothing leaves, and
assertionStatushad no way to tell them apart.The branch adds the fourth answer at
:426(kind: 'unretrievable'), the report branch at:1094, and — the part that keeps it fixed — a self-test pin at:1308reading "an annotations call that FAILED is unretrievable, nevernone". The genuineNONEcase survives at:1110.⭐ This is the same defect family as the card itself: an absence of a reading reported as an absence of evidence. Finding it one layer down, inside the file being repaired, is what makes this a class fix rather than a patch.
Verified myself
git diff --name-only origin/main...8025ba82→scripts/pm/ci-failure.mjs, and nothing else. The report's note that a two-dot diff showed 13 files becausemainadvanced mid-task was honest and is now moot — both readings agree at 1.RETRIEVAL FAILED/NONEadjacency onmain, and the newunretrievablebranch and pin on the head, read exactly as described.The ablations discriminate
unretrievablebranch ⇒ exactly 2 failures, bothunretrievablepins, with thenonecontrol pin staying green. That control is what makes the fixture a pin rather than a restatement — a fixture that reddens on everything proves nothing about what it is asserting.midWalkVerdictreturnEXIT_RED(what node did before) ⇒ 11 failures, the load-bearing one readingexpected [false,false,false] / actual [true,true,true]— the defect verbatim.Reproduction was done with
fetchstubbed so no socket opens and the measurement is deterministic, and the zero-hit control-character sweep carries a positive control that fires on a scratch file containing\x01.skip-changesetapplied additively as instructed.Clause ②answered on both limbs, with the grep showingassertionStatusandmidWalkVerdicthave no importers outside this file.Why this card mattered
The blind spot it closes already cost this seat a published mistake today: I asserted the additive REST label endpoint was unavailable in this container, reasoning from a transport refusal. It was available. Retracted at
6023#issuecomment-5355583433. After this lands, the tool that would have told me stops answeringexit 1 = REDwhen it read nothing at all.Generated by Claude Code
os-zhuang commentedon Aug 20, 2026
{ "issue": 10155, "status": "done", "branch": "claude/issue-10155-ci-failure-transport-net", "pr": "https://github.com/objectstack-ai/objectstack/pull/10361", "premise_still_valid": true, "summary": "The mid-walk throw still escaped on current main: reproduced against 2e39181a (post-#10275, post-#9966) with the probe green and the walk's first page refused -- uncaught Error, stack trace, EXIT=1, which this file's own table calls RED. Every network read after the probe (resolveTarget and the walk) is now wrapped. I settled the exit-2-vs-3 fork by RE-PROBING rather than by picking a constant, because the causes are not one class: quota exhausted mid-run or access changed under the run classify the environment (exit 3), while a transient 5xx or one 404 leaves a healthy container that simply cannot answer (exit 2); an unclassifiable re-probe falls to 2 rather than blaming the container. That is the same call check-half-states.mjs's in-loop net makes, reused rather than re-derived. The header's exit-3 promise is rewritten from 'decided before anything is read' to 'a CLASSIFIED TRANSPORT VERDICT', which is the guarantee actually worth keeping. The partial-output invariant rides along in both places it was broken: the net's report says whether nothing was READ or nothing was JUDGED and counts the check-runs already listed, and assertionStatus gains a fourth answer ('unretrievable') so a refused annotations call stops printing 'the check carried no annotation with any content in it' four lines under this same block's own 'that is not the same as \"the check recorded nothing\"' -- on a check measured to carry annotations_count: 1.", "tests": "All exit codes captured BEFORE any pipe (cmd > file 2>&1; EXIT=$?). Gates re-derived at the merged head with `node scripts/pm/dispatch-gates.mjs` (no args; it took its own change set from merge base 128684d50 and named 1 path). Green at 8025ba82: node scripts/check-cross-package-test-inputs.mjs --self-test -> 'All 60 self-test cases passed.'; node scripts/check-cross-package-test-inputs.mjs -> 'OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.'; node scripts/check-nul-bytes.mjs --self-test -> '75 assertions over a temp git repo'; node scripts/check-nul-bytes.mjs -> 'check-nul-bytes: OK (scanned 6101 text file(s) ... no raw ASCII control bytes)'; node scripts/pm/ci-failure.mjs --self-test -> 'OK self-test: ...' (verdict lines quoted from each gate's own output, never a bare $?). END-TO-END, fetch stubbed so no socket opens: mid-walk 403 with healthy re-probe EXIT 1 -> 2; access revoked under the run EXIT 3; refused annotations call EXIT 2 with the self-contradiction gone. ABLATIONS, each confirmed on disk by grep-counting the removed text AND an injected marker (1->0 and 0->1), each restored and re-confirmed (marker 0, anchor 1, git clean, self-test back to 0): (A) removing the `unretrievable` branch -> exactly 2 failures, both unretrievable pins, while the `none` control pin stayed GREEN -- which is what makes it a pin rather than a restatement; (B) making midWalkVerdict return EXIT_RED (what node did before) -> 11 failures, the load-bearing one reading 'expected [false,false,false] / actual [true,true,true]', the defect verbatim. Predicted direction was stated before each run and matched both times. The zero-hit control-char sweep carries a positive control: the identical grep -naP fires (exit 0) on a scratch file carrying \\x01 and finds nothing (exit 1) in the changed file. DECLARED NARROWING: the two derived gates were run OUTSIDE scripts/pm/os-verify-lock.sh. The lock was held by another agent's full `pnpm build` (341s, one waiter already queued) and my first attempt timed out in its queue; both gates are pure source-text scanners on node builtins that neither build nor read dist/ (measured 0.325s), so they do not contend for what the lock protects. NOT run locally: eslint, which needs a full root install this contended container did not warrant -- it runs on every PR regardless and dispatch-gates did not name it as path-matched. Clause 2: not triggered -- path limb is packages/spec/src/** and this is not that; on content this changes a seat tool's own exit vocabulary, no product contract, no spec/Zod accept-reject behaviour, no widened authorable surface (assertionStatus and midWalkVerdict have no importers outside this file; grepped). skip-changeset applied additively via POST and READ BACK: ['size/m', 'skip-changeset'] -- the bot's size/m survived, which is why a whole-set PUT was forbidden. PR left as DRAFT; not merged, auto-merge not enabled.", "open_questions": [], "out_of_scope_findings": [ "filed as #10362: ci-failure.mjs prefixes every line of a multi-line `fix` with `fix:`, so one wrapped remedy renders as three; check-half-states.mjs's printer already does this correctly, so it is a divergence from the sibling that produces the data, not an open question. Labelled `finding`, unassigned, no pm:queue." ] }Scope note: one file,
scripts/pm/ci-failure.mjs.origin/mainadvanced from2e39181ato128684d5mid-task, so a two-dotgit diff origin/mainreports 13 files here — three sibling PRs' work. The merge-base diff is one file; currentmainis merged into the branch and all gates were re-run at the merged head.Generated by Claude Code
Generated by Claude Code
fix(pm): net ci-failure.mjs's walk against mid-walk transport failures (