Queue reds that were the PR's own fault, split four ways: its checks never ran the test (A), main moved under it (B), machines or flake (C), other (D).
2026-10-05 · merge_group runs 10-03 → 10-05 23:00Z (2,086 runs, 146 culprit rows, 110 PRs) · the 09-30 → 10-02 regime (full suite every train) is not included
Half the PR-fault reds are tests the PR never ran; a third are the machines; one in nine is main moving under the PR. A 47%: the PR lane was capped, so only the queue ran the test. C 38%: a spot runner was lost (35 of 55) or a known flaky test fired. B 12%: passed on its base, broke on newer main — 6 of 17 rows with hard evidence. "Re-validate on fresh main" fixes about one red in ten; a full set on capped PRs and the spot fix cover the other eight.
| Bucket | Rows | Share | PRs | Where | Fix |
|---|---|---|---|---|---|
| A never ran on the PR | 69 | 47% | 49 | Full set 63 · Guard pins 6. 61 of 69: PR lane capped. | Full set on capped PRs, at approval. |
| B main moved under it | 17 | 12% | 13 | Guard pins 8 · affected tests 5 · queue-only tsc 3 (49 red runs). | Re-validate on fresh main; pre-queue type check. |
| C infra or flaky | 55 | 38% | 46 | Spot / runner lost 35 · flaky issue 10 · passed on re-queue 10. | Spot-loss fix; quarantine 4 files. |
| D other | 5 | 3% | 2 | 1 queued while red; 4 unattributable. | — |
Row = PR × check. Per PR (A › B › D › C): A 49 · B 13 · C 46 · D 2. 94 rows excluded as cascades or main-was-red.
Source. scripts/ci-flow/queue-vs-pr-join.ts over every merge_group run of ci.yml in the window: 2,086 runs, 759 with a red cause job, 850 red-job events = 543 inherited from an earlier group in the chain + 295 first-in-chain + 12 ambiguous. Only the first group a failure appears in names a culprit (flow-baseline's real_failure); cascades are never counted. That gives 240 first-fails (one per PR × failing check or test file). For each, the culprit PR's own pull_request runs at the head it was enqueued with were read, including the vitest logs, to answer: did the job run, did the file run, did it pass.
Rules, written before the numbers. Excluded: the same signature first-failing for 2+ PRs on one base (main was red), or an unlanded entry ahead in the chain failing the same signature (collateral). C: a runner/spot/shutdown signature in the job log or in the CI factory's per-run facts, a flaky-test issue naming the file, or the same enqueued PR head later passing that check in another group. A: the job or file did not run on the PR (affected lane capped, always-run step skipped, fence subset pruned the pin, next build deferred). B: ran and passed on the PR, or the test was added/edited on main after the PR's last validation, or a type error that only the queue's cold tsc (inside the Build sub-check) reports. D: the rest.
Where this can be wrong.
61 rows: the PR's affected selection was over the cap, so the PR ran a narrower list and the queue ran the full set (mode=full). 6 rows: the always-run drift/decide step was skipped on the PR (capped or deferred). 2 rows: the guard fence subset pruned the pin on the PR. 57 distinct files; the only repeater is scripts/escalation-crucible/__tests__/registry.test.ts (5 PRs — a registry test that any new entry breaks).
leasing-prospects-route, load-mirror-prospect-detail, load-prospects-projected.reads) failed; the PR's affected lane was capped and ran none of them.scripts/__tests__/guards-for-paths.test.ts; the PR ran the fence subset only and the pin was pruned.tool-dispatch-inventory.drift.test.ts; the PR's always-run drift step was skipped (capped), the queue ran it. Same file hit #16632 two days later.Fix. When the PR lane is capped, run the full set on the PR once — at approval (or at ready-for-review), not on every push. That is 61 of 146 culprit rows (42%) removed before the queue, at one full-set run per capped PR.
11 rows ran and passed on the PR's own CI; 3 are tests that landed on main after the PR was last validated (the PR's tree never had them — a freshness class, not a selection class); 3 are type errors the queue's cold tsc found inside the Build sub-check. Commits behind at queue time, test rows: 3, 9, 9, 18, 20, 22, 25, 25, 25, 28, 28, 131, 323, 323 (median ~25, matching the ~15 median lag of the fleet's PRs only loosely — the B rows skew stale).
ws-lead-outcome replay tests passed on the PR (validated 10-02 21:01Z), red in the queue 10-03 15:37Z, 323 commits behind; main had touched their imports (escalation fix fe2b113a, identity merge 0d8a925a). Confirmed.module-setup-refused.drift.test.ts passed on the PR; 20 commits behind; main touched an import (b868b2b8, maintenance approval wait). Confirmed. This PR then cascaded into 5 groups behind it.staff-asked-application-send.ts(108,14): TS2678 in the PR's own file. The PR's Type Check passed the same head at 03:42Z (run 37094125904); 'no-guest-card' was touched on main by #15845 at 03:24Z and #15943 at 07:07Z. Six PRs behind it went red on the same error (11 runs). Two more episodes like it: live-deps.test.ts (#16321 behind #16356, 29 runs) and tour-lifecycle-cross-surface-agreement.test.ts (#16291 after #16283 landed, 9 runs).Fix. Re-validate on fresh main before queueing — it would have caught the 6 confirmed rows and probably the 3 type errors (49 red runs between them, plus 2 on a fourth file not traced to a culprit — the costliest rows on this page per culprit). For the 8 unconfirmed rows the honest statement is "B or an undetected flake"; a fresh-main re-run also settles that question, so it earns its place either way. The pre-queue type check being built addresses exactly the 3-row class; this page says that class is small in rows but large in cascade.
35 rows: the runner died — "AWS interrupted the EC2 Spot instance" or "The runner has received a shutdown signal" — 4 on 10-03, 16 on 10-04, 15 on 10-05, after the 10-03 move to spot everywhere. 24 of the 35 are on the CI Checks job (type check, lint, DDB escapes, build), the longest-lived single job in the queue leg. 20 rows: flaky — 10 named by a flaky-test issue, 10 where the same PR head passed on re-queue. Four files carry most of the flake: leasing-compare.test.tsx (unhandled error, 4 PRs), ci-unit-tests-writer-vacuous-green.test.ts (4 PRs), self-review-unresolvable-range.test.ts, check-availability.test.ts.
ci-unit-tests-writer-vacuous-green.test.ts, open flaky-test issue, first-failed for 4 different PRs in 3 days. (#15779 · run 37083683201 is the re-queue-passed shape.)Fix. The spot-loss fix (retry a reclaimed job inside the run, or on-demand for the CI Checks job alone) is worth 35 of 146 culprit rows (24%) and is the right fix. The flake half is four files; quarantining them removes ~14 rows.
handoff-mode-write-fence.drift.test.ts was already red on the PR and the PR was queued anyway.Merge-queue contexts aggregator is red; it restates other jobs, so nothing names a cause here. Same shape in #16779.From the CI factory's per-run facts, 09-30 22:40Z → 10-03 00:00Z: 2,926 merge_group runs (1,080 green, 1,449 red, 84 cancelled, 313 unread); red jobs across the 1,090 red runs with facts: Guard pins 551, Full set (one machine) 364, CI Checks 206, Unit Tests (affected) 9. Spot or runner-shutdown signatures in that period: 5 of the 1,090 red runs, against 63 of 774 after the 10-03 move to spot everywhere. These are red-job counts including cascades, not culprit attributions — a like-for-like A/B/C/D for that window needs another join run (~2,500 calls).
Data: ~/.claude/services/ci-factory-mq/health-cache.json (facts per run), flow/flow-baseline-rolling24h.json (control), join output and bucket rules in the analysis session's scratchpad (analyze.py, bevidence.py). Related: ci-rethink-2026-10, ci-velocity-plan-2026-10.