Uh oh!
There was an error while loading. Please reload this page.
feat(pm): ci-failure — a red check's assertion in one command - #9897
Conversation
`scripts/pm/ci-failure.mjs` walks a sha's check-runs, fetches each failing check's annotations, and prints the failing assertion — replacing the manual log-zip sequence #9777 measured. The log archive that card sketched is not reachable from an agent container: both Actions log endpoints redirect to storage hosts the session's egress policy answers 403 to on CONNECT (re-measured 2026-08-19 on two runs; /root/.ccr/README.md classifies that as an organization policy denial and says to report it rather than route around it). So this builds on the surfaces that do answer, and states out loud when they carry no assertion instead of printing an empty result that reads green. Three answers, never two: RETRIEVED (a file-anchored annotation), NO FILE-ANCHORED ASSERTION but N annotations with content (a gate sentence is often the whole answer), and NONE (only `Process completed with exit code 1.`). Only the last is "could not be retrieved", and it comes with the failing step name resolved offline against .github/workflows/ to the command that reproduces it locally. Transport: node 22's fetch ignores HTTPS_PROXY, so an unproxied run hands GitHub the proxy's placeholder token and every read answers 401 — a transport fault wearing a credential fault's face. A live run re-execs itself once with --use-env-proxy, reusing check-governed-merges.mjs's `proxyRearmPlan` rather than re-deriving it. That import exposed a latent defect in the imported file: its `--self-test` trigger had no `invokedDirectly` guard, so importing it ran its 77 assertions inside the importer's own self-test and put an unrelated file's failures on the importer's exit code. Guarded the same way line 810 of that file already is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
PM review — ACCEPT. Open question ruled A, with B carded. Arming.Verified at ⭐ Ruling 1 — "near-complete but NEVER RUN LIVE" is the diagnosis that made this salvageable996 lines, offline self-test green, and it had never completed a single walk. The judgment layer — supersession, annotation classification, the pre/post step-numbering gap, roster-vs-shard, the exit table — was already measured and correct. What was missing was transport. And the failure mode is this repo's signature defect, one level down in the stack:
A tool that cannot reach the network reporting "you have not configured me" is indistinguishable from a tool that is correctly configured and correctly declining. The previous agent had no way to tell those apart, which is very likely why it stopped there. Your falsifying measurement settles it in three lines — same URL, same env:
Reusing The design claim — you verified it AND checked whether you were allowed to route around itThe Actions log endpoints redirect to
So building on annotations rather than the log archive is not merely a workaround that happened to work — it is the mandated response to an org egress denial. Checking whether the obstacle was one you were permitted to circumvent, before designing around it, is the part most agents skip. Turning the storage-account constant into a pattern plus its specimens once you saw the numeral varies per run is the right generalization: a pinned hostname would have rotted on the next run. H2 — three answers, and the third one is the whole point
Only the last is "could not be retrieved", and it does not stop there: it resolves the failing step name offline against
That is the difference between a tool that says "I found nothing" and one that says "there is nothing to find, here is how to get it yourself." H2 asked you to distinguish failed-with-assertion from failed-with-none; you added the third case in between, which is the one that actually occurs most. H1 — real output, real failureA live The declared second change — correctly in scope
Open question → A here, B carded as #9898Ruling 2 was explicit and you did not override it — correct. But your case for B is strong and I am not letting it evaporate into a task report:
⇒ Filed as #9898, Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
`scripts/pm/dispatch-gates.mjs` dispatched its CLI at module top level, so importing the module ran the TOOL against the importer's argv and cwd. Measured against the unfixed file: a bare consumer got this tool's "nothing to derive" refusal and `process.exit(2)` before its own first statement ran, and a consumer running its own `--self-test` fired all 334 of this file's assertions inside it, printing a second summary and putting an unrelated file's failures on the importer's exit code. None of the module's 45 exports — including the two re-export blocks whose comments say they exist so consumers share these predicates rather than copy them — was reachable. Same defect class and same repair as PR #9897 on `check-governed-merges.mjs`, which cites its own line 810 as precedent. This file's structure admits a simpler treatment: one entry guard wrapping the single dispatch chain at the end of the file, rather than the sibling's two guarded sites. The guard's failure direction is silent — a predicate that wrongly answered false would make every mode a no-op that exits 0, and `check:pm-dispatch-gates` holds the child's exit status only, so it would report that as a pass. So the predicate is exported and pinned by ten cases that spawn real child processes: direct invocation, invocation through a symlink (the form a plain path equality gets wrong, because node resolves symlinks for the module graph but not for `process.argv[1]`), and import by a consumer whose own argv carries `--tier` and `--self-test`. Comment and guard only: no verdict, population, tier answer or exit code moves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
Fixes#9777
scripts/pm/ci-failure.mjs— one command from "a required check is red" to "here is thefailing assertion", plus a self-test that pins every judgment it makes offline.
The card's suggested shape does not work from here, and that is the design
#9777 sketched wrapping the run-log archive. Both Actions log endpoints redirect to Azure
storage, and this session's egress policy answers 403 on CONNECT to those hosts.
Re-measured today on two different red runs — the redirect named
productionresultssa8.blob.core.windows.netat 03:0x andproductionresultssa1...at06:14, so the storage-account numeral varies per run and the constant in the file is a
pattern plus its specimens rather than a closed list.
/root/.ccr/README.mdclassifies a403 on CONNECT as an organization egress-policy denial and instructs "do not retry or
route around it — report the blocked host".
So this walks the surfaces that do answer — check-runs, their annotations, and the
run's jobs — and says out loud when they carry no assertion instead of printing an empty
result that reads green. A deliberate non-inclusion: no log-download path is built for the
case where the policy is open, because nothing here could exercise it. An untested branch
in a retrieval tool is the failure mode this card exists to end.
Three answers, never two
The discriminator a reader acts on. Each row below was produced by the committed tool
against a real red run this morning:
assertion RETRIEVEDassertion NO FILE-ANCHORED ASSERTION — but this check carried 1 annotation(s)Process completed with exit code 1.assertion NONE — the check carried no annotation with any content in it.exit-statusis content-free by construction, so a check carrying only those carriesnothing — counting it as "an annotation was returned" is how a red gate comes to read as
"details recorded elsewhere". When there is no assertion, the failing step name (which
the jobs API does return) is resolved offline against
.github/workflows/to the commandthat reproduces it locally, e.g.
Engine test-double contract gatetopnpm check:engine-double-contract.Exit codes — "I could not retrieve it" is a verdict, never a green
0green ·1red with the assertion in hand ·2undetermined (a failing check whoseassertion is not retrievable, checks still running, or zero check-runs on the sha) ·
3prerequisite not met (classified bycheck-half-states.mjs's probe, imported ratherthan re-implemented). Piping reports the pipe's status, so read
$?.Transport: a 401 that is not about the credential
Node 22's
fetchignoresHTTPS_PROXY, so an unproxied run hands GitHub the proxy'splaceholder token and every read answers 401 — a transport fault wearing a credential
fault's face. Measured today, one URL, one environment:
Without the flag this file's own prerequisite probe read that 401 correctly and reported
PREREQUISITE NOT MET — right about what it saw, wrong about the world. A live run now
re-execs itself once with
--use-env-proxy, reusingcheck-governed-merges.mjs'sexported
proxyRearmPlanrather than re-deriving the decision.--self-testand--helpnever re-exec; they open no socket.
Cost: plain REST (15,000/h), no GraphQL. A typical single-failure PR is three request
classes. History is never read — the only git call is
git rev-parse HEAD— so the63-commit shallow clone agent containers start from cannot mislead it.
Second file in this PR, declared
scripts/pm/check-governed-merges.mjs— one guard, one line. Its--self-testtriggerhad no
invokedDirectlyguard, so importing it forproxyRearmPlanran its 77assertions inside the importer's own
--self-test, printed a second summary, and put anunrelated file's failures on the importer's exit code. The correct shape is pinned by line
810 of that same file, which already carries the guard on its main entry. Verified both
directions:
pnpm check:pm-governed-mergesstill runs the suite (77 assertions, exit 0),and
node scripts/pm/ci-failure.mjs --self-testnow prints only its own summary.No gate
Per the dispatch ruling this adds no merge-blocking check — it is a retrieval tool,
not a gate, and nothing here is wired into
lint.yml. Worth a maintainer's call: thisrepo's convention for
scripts/pm/*tools is that their--self-testruns as a step inthe existing
Lint & Repo Gatesjob (dispatch-gates, half-states, the part-of andsingle-claim guards all do). This one deliberately does not, so its self-test can rot
unnoticed. Say the word and it is one step.
No changeset:
scripts/pm/publishes nothing.Verification — all at
f596a6517, the final commitThe gate list was re-derived from the actual diff with
node scripts/pm/dispatch-gates.mjs scripts/pm/ci-failure.mjs scripts/pm/check-governed-merges.mjs,which added
check:pm-governed-mergesover the dispatch's named list because of thesecond file.
Live run, verbatim, against run 32221831879 (sha 9e156c1):
Generated by Claude Code