merge-queue triage: excerpt the failure REASON, and aggregate same-signature ejections into one anchor issue - #10140
Conversation
…tes same-signature ejections into one anchor issue Two limbs on the #4859 merge-queue-triage bot, plus the gate that exercises it. Limb 1 — the excerpt now prints the deciding line beside the FAIL line. A timeout and an assertion produce byte-identical FAIL lines and opposite diagnoses; on 2026-08-20 that ambiguity produced a confidently wrong card (#10112) and cost a full dispatch to falsify. Limb 2 — each ejection is keyed by the failing test file path and records the sighting in its own comment. When one key has ejected >= 2 distinct PRs inside 24h the bot files ONE anchor issue for it, or refreshes the open one, and links it from every later victim's comment. The bot still decides nothing: no requeue, no quarantine, no label on anybody's PR. Also: the cross marks in the FAIL matcher must now sit at line start, so a multiplication sign inside a test TITLE stops spending excerpt budget. Fixes #10128 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
`AssertionError` qualifies under both the FAIL matcher and the reason matcher, so without a consumed-line guard the assertion case reports every failure twice and spends half the excerpt budget saying the same thing. Measured on the real captured assertion log: 12 excerpt lines before, 9 after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XqDQYVU5smx29ts9pAErja
|
PM review — Clause-② — ruled
|
|
Correction to my verdict above, on how this PR entered the queue. I wrote "arming once the three in-flight required contexts converge green". I never armed it. The timeline shows Recording it because it corrects a model I have been running on all session: I had been treating arming as the gate a PR passes through to reach the queue. It is not the only path in — a PR that is out of draft, Practical consequence, stated plainly because I raised a flag on this PR and the flag is now weaker than I implied: leaving a green PR un-armed does not hold it. The only holds that actually hold are converting it back to draft, or closing it. If the Generated by Claude Code |
Fixes #10128
Two limbs on the #4859 merge-queue-triage bot, plus the gate that exercises it. Verified at
e4d81f03ca.Limb 1 — the excerpt names the failure REASON
A timeout and an assertion produce byte-identical
FAILlines and opposite diagnoses. On 2026-08-20 that ambiguity produced a confidently wrong card (#10112, "missing build edge") and cost a full dispatch to falsify; the real cause was #10115, and the repair landed as #10120 — no assertion was ever involved.The extractor now looks ahead from each
FAILline for the deciding line —Test timed out in Nms/Hook timed out in Nms/Teardown timed out in Nms/AssertionError/ the firstError:after it — stopping at the nextFAILso one failure cannot borrow the next one's, and prints it labelled under itsFAILline.Before / after on real captured logs of the incident's own test file (
scripts/fixtures/merge-queue-triage/, provenance in the README there). Same file, same revision, same unbuilt closure; the only difference between the two runs is--testTimeout=1. The label is quoted verbatim from the bot's output; that comment's language is Chinese throughout — see "Language" below.Note the defect was asymmetric, and the measurement is sharper than the card:
AssertionErrorwas already a token of the oldFAILmatcher, so an assertion leaked into the excerpt as a detached, unlabelled line, while a timeout produced nothing at all. That is exactly the trap the first responder walked into — a reasonless excerpt reads like the assertion excerpts they had seen before.Three smaller corrections in the same seam, all proven against the excerpt the bot really published on PR #10008 (
scripts/fixtures/merge-queue-triage/incident-32333709633-published-excerpt.job-log.txt, read back through the API):stdout | ...noise admitted only by the multiplication sign inside a test title ((#7986 x #7799/#8022)), spending excerpt budget while the deciding line was absent.Limb 2 — one anchor issue per signature, across PRs
The same test file ejected #10105, then #10003, then #10008 before a human joined the dots at 04:08Z, and the first shared record was hand-filed carrying the wrong diagnosis.
Each ejection is now keyed by the failing test file path and records that sighting in its own comment as a machine-readable marker. The next ejection reads 24 h of those markers back (bounded, newest-first). When one key has ejected >= 2 distinct PRs in the window, the bot files one anchor issue for it — or refreshes the open one, matched on a stable per-key marker in the issue body, the same idempotency idiom as this file's own per-run comment marker — and links it from every later victim's comment.
The ledger is the workflow's own comments, deliberately: the header always said the cross-PR flake evidence lives there, it needs no state outside the repo, and it cannot disagree with what a human reading those comments would conclude.
The boundary is the point, and it is enforced rather than promised. The bot names and aggregates and decides nothing: no re-queue, no quarantine, no label on anybody's PR, no
skipwritten anywhere. The gate's stub layer records and throws on any API the harness does not model, andjudgefails the scenario on that record — so a future call that re-queues or labels something fails the battery without any scenario having had to anticipate it.The gate —
scripts/check-merge-queue-triage-outcome.mjsSame method as the sibling
check-cross-repo-closer-outcome.mjs: the shipped script is read out of the YAML with a real parser (never retyped) and run under doubles the wayactions/github-scriptruns it, as oneAsyncFunctionbody. Extraction failure is a failure, never a skip. No network anywhere in it.18 scenarios / 61 assertions over the shipped script; 78 assertions in
--self-test, driving 15 mutations of the shipped source to red, each asserting its own anchor was present first.E1/E2the two real logs;E3asserts the two captures still share a byte-identicalFAILline, so the corpus cannot drift into two obviously-different logs and leave the claim unproven;E4the published excerpt;E5a reason is never borrowed from the next failure;E6hook/teardown spellings;E7no double-printing.A1the >= 2-distinct-PR positive;A2the refresh, driven as a pair — run 2's world is built from run 1's output, because idempotency is a property of the pair and a hand-written marker would prove each half while leaving the two runs free to disagree about its spelling;A3two different keys produce no anchor (and its ledger deliberately contains another key that has been seen twice, so an ejection cannot file an anchor for a signature it did not hit);A4one PR ejecting twice is not two distinct PRs;A5a redeliveredworkflow_runwrites nothing at all.A6unreadable logs announce "not measured", never "nothing found";A7a truncated ledger reports its counts as lower bounds;A8no anchor is created while its absence is unestablished;A9an anchor refusal costs the job nothing and every fact it carried is in the comment anyway;A11a delivery refusal still fails the job, and the summary now says the sighting was lost with the comment.Permissions, and the invariant
The job gains
issues: write— limb 2 files and refreshes an issue. The gate asserts the grant (P0), because a workflow that files issues without it fails at run time, on a red queue build, where nobody is watching.The header's published invariant — this job is green iff the triage comment is on the PR — is unchanged and now says explicitly why limb 2 does not join it: every fact the anchor carries is printed inline in the comment, so an anchor that could not be written loses no diagnosis. Failing the job on it would be claiming an equivalence that is false.
Language
The bot comment stays Chinese, matching every existing line of it; the anchor issue it files is written in English, per the maintainer's 2026-08-08 ruling on issue and PR text. AGENTS.md records that a merged rule produced half-Chinese, half-English bodies, so the split is per artifact rather than per file.
Testing
Local, at
e4d81f03ca:pnpm check:type-check-debtwas not run locally and this is a declared narrowing: it refuses to re-measure without a full built package closure, and this diff contains zero TypeScript and nopackage.json— it is in the derived list only because.github/workflows/lint.ymlis touched. CI runs it on a built tree.Generated by Claude Code