CI rethink: stop testing the same change seven times

A research brief with diagrams.

Session 002 · 2026-10-03 · Proposed — pending Fede's picks

Contents

  1. Minutes and dollars: start vs after tonight (Oct 5)
  2. Audit, Oct 5 evening: fewer red trains, smarter test selection
  3. Machine sizes, Oct 5: right-size the CI boxes
  4. Overnight report Oct 4 → 5: before → after
  5. Thin merge queue (decided 2026-10-03)
  6. Incident: big trains rejected for 9 h (Oct 4) — RCA
  7. Second incident, 4:12 PM: a green train turned main red
  8. Fresh levers we are not using yet (research, Oct 4)
  9. Proposed: a standing 24/7 CI/CD agent — options + playbook
  10. The big moves
  11. Sequencing, agreed with Smith
  12. Diagrams 1 and 2: one change, today and after
  13. Diagram 3: six months of cost
  14. Diagram 4: PR size, CI cost and review quality
  15. What we delete, what we keep
  16. What our own sessions do locally that drives CI load
  17. What our sessions do locally, and what changes
  18. What is still missing
  19. Decisions for Fede
  20. What the talks, podcasts and books add
  21. Sources
  22. Full analysis (long)

Minutes and dollars: start vs after tonight (Oct 5)

Session 002 · 2026-10-05 night · M = measured, E = estimate

Fede's ask (2026-10-05): minutes and $ per day when optimization started, after tonight, and where the next 50% is. Projected vs actual: the actual column is refilled each morning from real runs.

Minutes are machine-minutes from the runner job records. Dollars are runner compute only, from AWS billing; add about $47/day of fixed cost (dashboard estimate) for the all-in number. Days are UTC.

Headline

Per weekday
Start (Fri Oct 2), minutes299k (M)
Start (Fri Oct 2), $$609 (M)
After tonight, minutes164k–174k (E); Mon Oct 5 was 181k (M)
After tonight, $$110–150 (E), +$30–90 if the merge-queue checks stay on full-price machines
Why the start day was so high, projected vs actual, and waste from red runs

Oct 2 was the first full weekday on the new runner fleet, and it was an incident day: 1,522 of 2,232 merge-queue runs went red, and $352 went to full-price machines. Most of the drop to about $190 on Oct 5 (E; $134 billed so far) came before tonight: cheap spare-capacity machines everywhere, the disk fix, and the guard tests moved out of the queue gate. Both days are weekdays, so no scaling is needed.

Projected vs actual

ItemProjected (E)Actual so far (M)Still pending
Runner $/day$188 → $85–100Needs a full day; our own estimate is $110–150Oct 6 billing
Monthly$5,600 → $2,600–3,000—Same
Merge-queue checks job10.6 → ~6 minGreen runs 13.2 min on Oct 5 (565 jobs) → 14.8 min after tonight's change (15 jobs); still 8-core, now full-price16-core change
PR open → live2.5 h → 1.5 h—Dashboard, after merges
Peak queue throughput~2×Peak 22 merges/h on Oct 5; 23/h at 01Z Oct 6After merges
Guard tests run once per train−$23 to −33/dayGuard jobs per queue run 6.0 → 4.0 (first 30 min, early)Full day
No full-price machines−$25/dayOpposite so far: full-price pace ~$12 → ~$100/day in the hour after tonight's checks changeChecks back to cheap machines
Full set on 8 × 8-core−$13/dayOne trial runPR not merged
Guard tests on 3 × 8-core−$5 to −11/day—PR not merged
Type check beside the build+$5 to +10/day—Open, Fede's call

Waste from red merge-queue runs

Oct 2Oct 5
Red runs1,522 of 2,232238 of 793
Their minutes112k18.4k
Their $ (E)~$327$26–45; dashboard ≈$44 plus 37 PR-hours lost
Red trains that only echoed another PR's break—2 of 3 (M)
Merge queue's share of all minutes58%37%

Sources: CI cost ledger (dollars); runner job tables, 1.29M jobs since Sep 29; the bigger-box, red-trains and always-run audits; the CI factory and CI/CD architecture pages; GitHub PR states at 03:15Z Oct 6. All session 002.

Audit, Oct 5 evening: fewer red trains, smarter test selection

Session 002 · 2026-10-05 night · four read-only audits of GitHub data (Sep 5 → Oct 5) · every number carries the audit's own label: measured or estimate

One line: the merge queue's full suite mostly catches a PR's own tests, which never ran on the PR because a 200-file cap switched them off. Remove the cap, run the real selection on the PR, and most breaks get caught before the queue.

Audit: what we found, what's left, and the test setup itself

1. What the team asked

From the raw transcript (Fede and Gera, Oct 5): Gera's first priority is “never have main red,” because “things that are supposed to break need to break in your PR,” and one head failure “took down 58 of the previous PRs, roughly $10.” He will only cut tests “once 90% of the time we're never failing merge queue.” Both agreed the fix is a smarter blast-radius algorithm, “rather than just what files did you touch,” with “something at the merge too.” Fede's target: “speedy and low cost,” a CI budget of about $2,000 a month. Gera's pain: about three hours from PR open to live.

QuestionShort answerWhere
Back-end tests for a front-end change?No, already not. Front-end-only PRs run a median of 8 test files (measured, 122 sampled).2b
Why so many red trains?Big PRs run none of their own tests until the queue, so the queue is where they fail.2b, 2c
Can the queue drop the full suite (nightly only)?No. It catches ~14 real breaks a day nothing else catches.2a

2a. The queue's full suite catches ~14 real breaks a day

For 33.5 hours (Oct 2 10:34 PM UTC → Oct 4 8:05 AM UTC) every train ran the full suite. In that window 20 PRs broke a test that the affected tests, the guards and the type checks all passed: about 14 a day (measured). Over the whole 72 hours it was 39 to 48 PRs, 13 to 16 a day. “Real” means the PR pushed a fix before merging. With the full suite nightly only, each would have merged and turned main red, so nightly-only was rejected.

Queue runs (trains), measuredFull suite on every train (33.5 h)Full suite on big trains only (29.9 h)Whole 72 h
Trains1,194 (691 green)852 (556 green)2,132
Failed only in the full suite211155366
PRs with a real break only the full suite caught2019 confirmed to 2839 to 48
Innocent PRs thrown out with them82735
Flake or no cause found538

Live examples: #15837 broke four prospect-list tests (run); #16231 broke three tour tests (run); #16659 broke the first-email compose test (run). One got through: #15779 merged unchanged and the after-merge lane traced main red to it (#15926).

2b. The 200-file cap switches a PR's own tests off for 47% of PRs

When a PR's change selects more than 200 test files, the PR runs zero of its own unit tests and the check goes green as “Full suite deferred to main” (measured, from the code). That happened to 666 of 1,430 sampled PRs, 46.6% (measured, 14 days). The suite grew from 5,736 to 8,277 files in those 14 days, so 200 is now 2.4% of it. Of the 48 real queue catches above, 47 came from capped PRs (estimate) and 34 broke a test file the PR itself edited (measured). The queue is catching tests the PR should have run.

of the 48 real breaks the queue caught48 PR ran zero of its own tests (cap)47 Broke a test the PR itself edited34 In the PR's own selection, no cap43
Remove the cap and 43 of 48 would have failed on the PR itself (estimate); the other 5 are file-reading tests (section 4).
Layer the PR changedPRs, 14 daysOver the cap (sampled, measured)Median tests selected (measured)
Front end4154 of 122 (3%)8
Back end1,491308 of 461 (67%)854
Shared / hub file431153 of 154 (99%)4,035
Mixed1,355199 of 408 (49%)125
Scripts / CI only5870 of 1751
Tests only3490 of 1011
All4,692666 of 1,430 (46.6%)54
Top 10 files that push a PR over the cap (estimate from the import graph; examples measured)
Capped PRsFileTests it selects aloneLive example
287shared data types (types.ts)4,721#10666: 3,205 (job)
153any package.json (forces everything)all#14138: 7,320 (job)
142Clara conversation manager (in the loop)1,419#11225: 478 (job)
118generated route list788#11074: 530 (job)
116Clara leasing tools (in the loop)1,419#12647: 808 (job)
108data-layer front door (data/index.ts)3,941#13102: 3,551 (job)
97JSON repository3,941#11747: 3,529 (job)
85portfolio registry keys3,130#10960: 887 (job)
72data store3,941#13843: 3,765 (job)
65guest-card first-email compose (in the loop)1,419#10855: 469 (job)

Worked examples, today vs no cap: a toast-message change, #16066, selected 312 tests and ran 0 on the PR (job); a data-store change, #15724, selected 4,051 and ran 0 (job); a small renewal-offers screen, #16237, ran its 1 test (job). “In the loop” = part of the 218-file import loop (section 4).

2c. How it got this way: three cost cuts

Each cut made sense alone. Stacked, they mean a big PR gets no unit tests until the queue, so the queue is where it fails, and every train behind it is thrown away.

WhenCutWhat a big PR ran after it
Jul 14#3732 caps runaway “affected” selections at 200 files (from repo history)Full suite in the queue instead of its selection
Sep 18#9480 full suite once per review round on big PRsFull suite on the PR, once per review round
Sep 30#14402 full unit shards on main only, not on PR verdictsZero of its own unit tests on the PR; full suite only in the queue and after merge

2d. 30 days of red main: what broke, and what is fixed

Main was red about 145 minutes a day through September, doubled the last week, and is at 154 a day now that a lane after merge runs the full suite and reverts (estimate, commit-time method). Machines failing to start, the live voice-platform tests and a test-runner timeout are fixed. Real regressions, guard tests, queue cascades and the after-merge lane dying are still open.

145Sep 5–11 141Sep 12–18 145Sep 19–25 297Sep 26–30 692Oct 1 (switch day) 154Oct 2–5 Confirmed red-main minutes per day (estimate). Until Oct 1 read from push CI on main; from Oct 2 from the after-merge full-suite lane.
Same level as September, but customers no longer see it: releases only move to commits the full suite passed.
Breaking pointRed-main episodes (measured)Red min (estimate)StatusExample
Real regressions that pass PR checks46 + 27 after-merge~1,940 + 1,653open by design ~1.3/day#16246
Guard / drift / inventory tests56 + 7~1,930 + 370partly fixed#16405
Machines failing to start42~930fixed none since Sep 30run
Live voice-platform tests41~620fixed deleted (#15270)run
Production deploy promote failed8~400open, low raterun
After-merge lane dies, no test to blame7214 (measured)open, rising Gera draft #16854#16847
Queue cascades (innocent PRs thrown out)818 cancelled trains, Oct 3–58,564 wall-min (measured)open Gera #16802#16581
Test-runner worker timeout on green shards9–12~110fixedrun
Voice-harness gate test5~145fixedrun
Type check / lint on main12~205fixedrun
CI fixes by lane, Sep 5 → Oct 5 (merged PRs, measured; classes by a model pass, ±10%)

Gera's lane mostly made the machinery tell the truth (review verdicts, guard results, runner deaths no longer read as code failures). Fede's lane mostly changed shape and cost (merge queue, after-merge lane, one run per change). Both worked the queue cascade.

ClassGeraFede lane
Review loop6120
Guards, ratchets, pins5317
Runners, infra, deploy5062
Cascade (queue, freeze, auto-merge, auto-revert)4849
Selection (which tests run where)3325
Flake57
Cost365
Total253245

Red trains by cause: the queue's first 5 days (Sep 30 → Oct 5)

Every queue run (train), Sep 30 10:40 PM UTC → Oct 5 11:34 PM UTC: 2,550 of 4,828 trains were not green (measured). “Now” = since the last fixes went live, Oct 4 4:42 PM UTC (877 trains in 30.9 h), scaled to a day. An echo is a train that failed on the same file as a different PR's train still running ahead of it, so it carried someone else's break (84% of echo PRs later went green with no new commit, measured). And since the queue stops a run the moment any job goes red, “cancelled” almost always means red: only 131 of the 2,550 (5%) were cancelled with nothing red (measured).

Cause (measured)5 daysNow, per dayLive example
Echo of a broken PR ahead, full suite696106#16836 behind #16733 (run)
Echo, guard tests91724#16750 behind #16745 (run)
Echo, type check and affected tests10717#16387 behind #16321 (run)
The PR's own change (a real catch)11527#16836 broke the judgment-feed test (run)
Culprit put back in the queue unchanged11016#16836 again, 10 min later (run)
Corrected Oct 5, 10:40 PM: a re-check of the 20 post-Oct-4 trains found 15 a day were misclassified (the PR had pushed a new commit, so the rule correctly let it back in) and about 1 a day slipped through a known fail-open (#16506, head time could not be read). The rule is already on main (#15897, #16475) and saves about 31 red trains a day (measured).
Spot machine reclaimed by AWS3812#16787 (run)
Cancelled with nothing red (cascade or PR left the queue)13112#16356 (run)
Repeat of a break whose PR had already left the queue4911#16742 after #16733 (run)
Flake, or fixed on main meanwhile314#16807 (run)
Test runner crashed, no test failed181#16785 (run)
Type-check machine disk full2980#15428 (run)
Type-check machine died (out of memory, inference)400#16356 (run)
Total not green2,550230no train timed out

Now, 2 of every 3 red trains are echoes. Each real catch leaves about 5 echo trains behind it (measured, counting echoes behind echoes).

What the fixes so far did (measured, per 100 trains before vs after each merge, because daily volume moved 3–4×). Three worked:

FixWhat it countsBefore → after, per dayPer 100 trains
#15452 bigger disk on the type-check machineDisk-full reds196.5 → 018.4 → 0
#16389 type check after buildType-check machine deaths10.7 → 01.0 → 0
#15860 bookkeeping guards leave the queue gateTrains thrown out by guard tests351.8 → 33.729.1 → 5.3
Four went the wrong way or did nothing (measured)
ChangeWhat it countsPer 100 trains, before → afterWhy
#15399 full suite back in the queueFull-suite echoes4.5 → 18.4went up it catches real breaks, and each one fans out
#16325 / #16400 thin queue (full suite on big trains only)Full-suite reds15.8 → 21.7no gain per train
#15881 queue jobs on spot machines, retry on reclaimSpot-reclaim reds0.5 → 1.5went up 3×
#15203 auto-quarantine flaky testsFlake trains0.6 → 0.7no change

Two rules against re-queueing an unchanged PR only half worked: #15897 took re-entries from 2.6 to 1.7 per 100 trains, #16475 from 2.4 to 1.6 (20 h of data). Both still open: #16581 (don't throw out bystanders) and #16592 (a spot kill is not a code failure).

What would cut it further. Three measured fixes together remove 59 red trains a day. Corrected Oct 5: the re-queue rule is already live, so the two remaining measured fixes (bookkeeping guards 19, spot as infra 12) remove up to 31 a day, before removing overlap. Shifting real catches onto the PR adds an estimated 68. Together that takes today's ~230 a day to about 100 (estimate); what is left is mostly echoes behind echoes and repeats with no single owner.

FixRed trains a day savedLabel
Never re-queue a PR at a commit that already failed (31/day) already live; the remaining gap is #16506, about 1/day, Gera's call~1measured (corrected Oct 5)
Take the last three bookkeeping guards out of the queue's full suite (they run after merge instead)19measured
Treat a spot reclaim as infra and re-queue it (#16592); on-demand machines for the queue's short jobs12measured
Those two together (was 59 with the re-queue rule), overlap not recomputedup to 31measured
Run the PR's real test selection before it enters the queue (move 2 below)~68estimate, if perfect
Don't throw out bystanders (#16581)0measured. stops 122 innocent PRs in 30.9 h from being thrown out Corrected Oct 5: in 31 h GitHub removed 58 PRs for failed checks; only 6 followed an echo train and all 6 were real culprits; 100 of the 122 “victims” merged without leaving the queue. GitHub already rebuilds trains without the failing PR. #16581 to be closed with the timeline evidence
All of the above~230 → ~100 leftestimate

Flakes are rare: at most 31 trains in 5 days (an upper bound, since “fixed on main” looks the same). Two tests tracked as flaky, #15851 and #15852, failed here mostly as echoes behind real breaks (22 of 24 and 9 of 10 trains, measured). Source: red-trains audit, session 002, Oct 5.

3. What's left, in order

Fede said “go ahead” on moves 1–4 and “go refactor” on move 7 tonight. Moves 1–4 are fixes to how CI already works, so they ship with no switch.

MoveDaily cost todayExpected effectRiskStatus
1. Full-suite machines skip before they boot on normal trains~348 normal trains a day still boot two 64-core machines that exit in ~46 s (measured sample ×4)9–12 machine-hours a day, ~$10–15 (estimate, not checked against the bill)Low; a missing signal now runs the suite instead of skipping itapproved building tonight
2. Remove the cap; the PR runs its real selection, split across machines47% of PRs run zero own tests; 47 of 48 real queue catches were those PRs−67% test files run in the queue; 43 of 48 caught on the PR (estimate)Big PRs select 1,353 files at the median, 4,766 at p90 (measured), so needs splittingapproved building tonight; builds on Gera's #16060 (runs a capped PR's own tests and direct importers), which is a partial step
3. File-reading tests declare their inputs5 of 48 real breaks were tests the import graph cannot see48 of 48 with move 2 (estimate)Lowapproved building tonight; #5549
4. Skip cache keyed on content, not on the moving base1 of 141 PR runs skipped since #15856; 74 of 215 jobs (34%) re-ran on the same commit (measured)Up to 34% of those jobs skippable (upper bound, estimate)A missed input gives a stale pass; needs move 3 firstapproved building tonight
5. Queue cascades: cancel orphaned trains (a train no longer in the queue is cancelled as “superseded”) (corrected Oct 5: was “don't throw out ~122 bystanders”; 58 PRs removed in 31 h, only 6 after an echo train, all 6 real culprits)299 of 877 trains rebuilt mid-run, about 1,700 run-minutes a day discarded, 110 of them counted red (measured)About 1,700 → under 100 discarded run-minutes a day; about 85 → 0 orphan trains counted red (estimates)Lowbuilding tonight #16581 to be closed with the timeline evidence; #16802 (spot as infra) stays the fix for real wrong ejections: spot 10, guard flake 3, runner crash 1 in 31 h
6. Cut the ~940 always-run tests by layer941 test files on every normal PR, whatever it changed (measured from config); daily cost not measuredA front-end PR runs near its own 8 instead of ~949A cross-layer guard stops running on that PR (still runs in the queue and after merge)planned, not yet approved
7. Import-structure refactor (218-file loop, five catch-all data files)290 PRs in 14 days were over the cap only because of the loop (estimate)Far fewer PRs over any capWide change across leasing and Clara codeapproved architecture; plan in progress
8. Remaining bookkeeping guards out of the queue's full suite (three left)24 trains in 30.9 h (measured)19 red trains a day saved (measured)A bookkeeping slip shows up after merge instead of in the queueplanned, not yet approved

Acceptance bar from the team meeting: queue green rate up, breaks caught on the PR instead of the queue, dollars per day toward $65 (the $2,000-a-month target), and hours from open to live. Each PR below reports all four.

4. Is the test setup itself wrong?

Partly. Picking tests by what a change imports works; the code is wired so that a change imports almost everything. One in five source files (1,976 of 9,627) selects more than 200 tests on its own (estimate). A front-end/back-end split would not help: back-end tests are 68% of all test files (5,640 of 8,277), and a back-end PR's layer suite is a median 89% of the full suite (estimate).

ProblemWhat we measuredEffect
Import loop218 files in one loop (68 leasing, 29 Clara agent, 18 escalation, 14 tool handlers, 12 workflow steps). 919 PRs in 14 days touched it; for 290 it was the only reason they went over the cap (estimate)Touch any one file, select the same ≥1,419 tests
Catch-all data filesFive data-layer files each select 3,900+ tests alone: shared types 4,721 (1,081 direct importers), data helpers 4,323 (546), data front door 3,941 (2,366), JSON repository 3,941, store 3,941 (estimate)Removing the front door alone leaves 1,966 of the 1,976 big files still big: the tangle sits underneath
Always-run set941 test files: 715 always-run plus 697 fence pins, 471 in both (measured from config)~17× the median PR's own selection (54); ~120× a front-end PR's (8)
File-reading tests5 of 48 real breaks were tests that read a file or a recording instead of importing it, e.g. #16174, #16750 (measured); none is a guard pinInvisible to selection; caught only by the full suite (#5549)
Skip-cache keyKeyed on tree plus base commit; the base moves ~335 merges a day. 1 of 141 runs skipped (measured)The cache almost never hits

Sources: queue full-suite evidence, blast-radius audit, 30-day CI history audit and the pre-boot skip design, all session 002, Oct 5 (on the office Mac mini under the 002 reports folder). Jul 14 cap date from repo history.

Machine sizes, Oct 5: right-size the CI boxes

Right-sizing CI machines saves about $39/day (about 20% of runner time) and 1.6 min per merge train; no change raises cost.
The 64-core full-set machines are busy (95%) but do not scale: 32→64 cores gave 1.23x, and per core they do about 3.9x less than the 8-core machines running the same suite after merge.

Cheaper (building now)

JobNow → recommended$/dayMin per train
1. Full set (merge queue)2 × 64-core → 8 shards × 8-core−$23 (est.)−1.6 (est.)
2. Guard pins6 × 4-core → 3 × 8-core−$5 to −$11 (est.)−0.4 (est.)
3. 2-core x86 helper jobs (code review, gates, watchdogs)2-core x86 → 2-core ARM−$5.5 (est.)0
4. Unit tests (affected), merge queue8-core → 4-core−$2.3 (est.)0 median; up to +1 at p90 (est.)
5. cloud-test4-core → 2-core ARM−$0.5 (est.)0
6. Post-merge green/culprit report8-core → 2-core ARM−$0.3 (est.)0
7. Post-merge guard pins3 × 2-core x86 → 3 × 4-core ARM−$0.15 (est.)post-merge lane −2 (est.)

Costs more (not doing)

JobChangeSpeed gainCost change
PR test with the always-run step4-core → 8-core13.2 → ~8.4 min (est.)+10% at measured prices, +31% at flat per-core (about +$0.8 to +$2.5/day)
Full set2×64 → 3×64, or a 96/192-core box~6.2 → ~4.5 min (est.)+11% or more
Post-merge unit-full2 × 8-core → 4 × 8-core10.4 → ~5.6 min (est.)+14%

Type check beside the build (Fede's pick): measured +$20–25/day at on-demand prices; a builder is measuring the real memory peak to find a cost-neutral 16-core box; yes/hold asked.

Overnight report, Oct 4 → Oct 5 (goal: half the CI minutes, same speed)

Session 002 · written 2026-10-05 07:30 MT · numbers from the CI ledger (Oct 4 covers 11:22–24:00; Oct 5 covers 00:00–06:10) and the hourly overnight watch

One line: machine time per merged PR is down a third (1.10 → 0.75 machine-hours), merges got faster, not slower. PR checks are at the goal (−49%); the merge queue is not yet (−14%).

Per merged PROct 4 (before)Oct 5 overnight (after)Change
Machine-hours, all CI1.10 h0.75 h−32%
  of which PR checks0.57 h0.29 h−49%
  of which merge-queue trains0.47 h0.40 h−14%
Queue trains per merge2.11.7fewer rebuilds
Trains thrown away (cancelled)37%28%44% at midnight → 5% by 4 AM
Merges per hour10.918.5faster
Queue depth (median)28.510shorter line
Median good train13.7 min15.9 minslower — watching (more hub-sized full runs)
What went in overnight (12 merged, all live, no switches)
What broke, and what it taught us
Three decisions for Fede (yes / hold)
  1. #16492 moves the Willows and Fairhaven test properties' AI onto the subscription token pool you retired Sep 27 (same 8 stale tokens). Merging it as-is turns their AI off. Recommendation: hold; refresh the pool or re-cut it to keep test properties on the metered key.
  2. Pause the nightly topic-label sweep schedule in production Temporal (its code is gone; the schedule still fires and fails). Recommendation: yes.
  3. Turn the “@claude review” workflow back on (off by hand since Sep 30) so workflow-only PRs can be reviewed. Recommendation: yes, or keep it off and we always ship a test file with workflow changes.

Hour-by-hour rows, incident log and every decision taken overnight: ~/.claude/logs/ci-overnight-2026-10-05.md and ~/.claude/logs/ci-decisions.log on the office Mac mini.

Thin merge queue

Thin merge queue: full details

The decision

  • The merge queue stops running the whole test suite on every train. It runs the tests the change actually touches (tier 1).
  • A change to a widely shared module (more than 200 test files selected) still runs everything in the queue (tier 2).
  • After merge, the full suite runs on the newest main every 15 minutes. When it goes red, a bot finds the commits that broke it and opens a revert for each one at once.
  • The release branch (what customers run) only ever moves to a commit the full suite blessed.
  • This reverses the 2026-10-01 decision that put the full suite back in the queue.

How a change flows

PRtargeted tests Thin queuechecks + touched tests~4 min main Full suiteevery 15 minon the newest main greenred Release advancesto the blessed commit Revert botone revert per culprit Release = last blessed commit, then deploys
The queue only guards the obvious. The full suite guards the tip, and customers only get blessed commits.

Why: what the queue was catching

88 sampled queue failures, 10-01 to 10-03 (every 2nd of 175, across 87 PRs).

Failed checkReal bugFlake
Type check / lint / build540
Guard pins533
Unit tests (touched)13
Full suite01
  • 69% of merge-queue machine time on 10-02 went to failed or cancelled trains: 6,507 vCPU-min, 43.6% of that day's CI machine time.
  • Queue wait: median 80 min, p75 101 min (93 of 468 merges on 10-03).
  • 27% of PRs were kicked out and re-queued at least once.
  • Merges: about 20 per hour on average this week (398 to 582 a day), 32 per hour at the peak hour.
  • Selector sanity: 4 recent queue runs selected 1, 8, 42 and 7 test files.

Before and after

BeforeAfter
Queue run~11 min~4 min (estimate)
One failure in a trainre-runs 11 min for everyone behind4 min
Merge ceiling~44 per hour~120 per hour (8 per train)
Queue wait, median80 mintarget under 30 (to be measured)
Full suite runsonce per trainonce per 15 min, on the newest tip

What we watch for 24 hours

  • Cancelled trains and gate failures (added after the Oct 4 incident; two in a row = alert).
  • Heavy-lane reds per day (last three days: 21, 11, 3).
  • Revert-bot reverts.
  • Queue wait median.
  • Merges per hour.
  • The AWS CI line.

Kill switch: revert the queue change with GitHub's Revert button.

Measured: first 75 minutes (live Sat Oct 4, 2:22 AM MT) — incomplete, see the incident below

Correction (10:20 AM MT): this table was built from a sample with no hub-sized change in it. The “0 of 18 trains ran the full set” row was the bug, not the saving: the full set was being skipped on the trains that needed it, and those trains were failing. Read the incident section before trusting any number here.

Before (10-01..03)First 75 min
Queue wait, median80 min17 min (4 merges; quiet Saturday, small sample)
Train time~11 min10 min — the app build is the long pole, not the tests
Full set in the queueevery train (2 shards × ~5 min on 64-core machines)0 of 18 trains → ~640 core-minutes saved per train
Heavy-lane reds21 / 11 / 3 per day0
Auto-reverts—0
Red main—0 of 4 pushes

Found while measuring: GitHub fires the heavy lane's every-15-minutes schedule about once an hour (runs at 22:29, 23:28, 00:25, 01:44, 03:06, 06:23, 08:41; repo-wide only 5 scheduled runs fired between 08:00 and 09:40). GitHub throttles schedules on busy repos. Until fixed, a bad merge is caught in ~1 hour, not 15 minutes. Fix in flight: the heavy lane also starts after each completed main CI run, paced to one run per ~15 minutes. No switch.

Sources: GitHub run and job records for ci.yml (merge_group) and post-merge-heavy.yml, PR timelines for queue wait. The estimate row above (“~4 min”) did not hold: the train is bounded by the Next.js build, so the saving is machine time and flake-free trains, not minutes per train.

Incident: the thin queue rejected every big train for 7.5 hours (Sat Oct 4, from 2:23 AM MT)

What happened, in one line: after the thin-queue change merged at 2:21 AM, every train that included a hub-sized change (one touching more than 200 files, which is most of the stacked team-notify work) failed its final gate and was kicked out of the queue. Small trains passed, so the queue looked alive while ~30 approved PRs sat still. Fede noticed at 8:00 AM. A first fix merged at 10:05 AM and did not help; the real fix is #16400.

Time (MT)What
2:21 AMThin queue merges (#16325).
2:23 AMFirst big train fails the gate: “full-set job concluded skipped”. 19 of the next 31 trains fail the same way; 12 small trains pass.
2:22 – 3:37 AMMy first-hour measurement runs. All 4 merges in the window are small PRs. The table above is written from them.
3:16 AMWeb moved to deploy from release; heavy-lane fixes; I watch heavy-lane reds, reverts, queue depth. None of those gauges count a cancelled train.
8:00 AMFede: “why are 38 PRs approved and not moving?”
8:12 AMRoot cause found. Fix PR opened (#16396); backup revert opened (#16397), not armed. Admin merge refused by GitHub (queue enforced).
8:45 – 9:56 AMFix PR fights CI plumbing: a type error in its own test (my miss), a spot runner dying mid-test (which made the review bot withdraw its approval), and a debounce-cancelled duplicate run that GitHub read as the verdict. Each needed a manual nudge.
10:05 AMFix #1 merged. 15 PRs re-armed; 9 trains running within a minute. Small trains pass.
10:15 AMFirst big train fails the same way. Diff against the pre-thin-queue workflow finds the real cause (success() + a skipped upstream job). Fix #2 (#16400) opened 10:38 AM, urgent.

Why the first attempt failed (and the second). Compared with the version that worked before the thin queue, the full-set job’s gating changed in exactly one way: it now also waits on the test job (needs: [plan, test] instead of [plan]). test itself waits on a job that is skipped on every queue train. GitHub’s job-level success() is false when anything in that chain was skipped, so the job’s condition was false on every train, regardless of the mode. The first fix (#16396, 10:05 AM) moved the tier decision inside the job but left success() in the condition, so it changed nothing: the first big train after it (10:15 AM) skipped the job 1 second after test finished. The second fix (#16400) names the two results explicitly (needs.plan.result == 'success' && needs.test.result == 'success'). A drift test forbids success() on that job and has a planted control.

Correction: an earlier version of this section blamed the mode == 'full' conjunct and said GitHub’s evaluation was “unknown”. That was a guess written under time pressure and it was wrong; the diff against the working version answered it in two minutes. Lesson on the page: diff against the last version that worked before theorising.

A second, independent cause was also live overnight and I missed it in this write-up: the merge-queue build machine ran the site build and the type check at the same time and ran out of memory, killing the job (“The runner has received a shutdown signal”, e.g. 3:09 AM). Gera’s #16389 (merged 10:18 AM) runs the type check after the build; the build job has not died since (evidence: 20 runs checked, docs.propflowai.co/a/ci-build-shutdown). Each train now takes about 3.5 minutes longer. So the night had two overlapping failures, one per fix; neither fix alone would have cleared the queue.

Why I did not see it. Three gaps, all mine:

  • The sample had no big change in it. Saturday 2 AM traffic was small PRs. “0 of 18 trains ran the full set” was read as the saving. It should have been a red flag: the design says the full set runs on hub-sized trains, so zero means either no hub-sized train happened (check!) or the tier is broken.
  • I watched the wrong gauges. The 24-hour watch list above (heavy-lane reds, reverts, queue wait, merges per hour, AWS line) has no row for cancelled trains or gate failures. Those were the only two numbers that showed the break, from minute two.
  • No live alert. Nothing pages anyone when the queue kicks out N trains in a row. A human asking is the alert we had.

Why it stayed broken ~5.5 hours. Because of the two gaps above, nothing changed between 2:23 AM and 8:00 AM. I was active the whole time (web-from-release flip at 3:16 AM, heavy-lane fixes until ~6 AM) and never ran the one query that would have shown it: trains by conclusion.

Before → after. 2:23–11:36 AM: 0 big trains passed in 9 hours; 6 PRs merged by 10 AM, all small. After 11:36 AM: the first big train passed at 11:40 and 4 big PRs merged in 14 minutes.

One more thing the fix exposed: fix #2 sat in the queue 49 minutes behind six big PRs that kept failing and resetting the line, because the queue rebuilds every train behind a failure. An urgent CI fix has to go to the front: pull the entries ahead of it, let it land, re-arm them. Playbook item.

Broken window (2:23 – 10:05 AM)After the fix
Trains cancelled by the gate19 of 311 of 7 in the first 20 min, for a real code conflict (#16321 vs #16356)
Hub-sized trains that ran the full set0every one (4 of 4 by 11:54 AM; both shards, ~5–7 min)
PRs merged6 in 7.5 h4 big + 8 small in 20 min (fix #2 merged 11:36 AM)

What changes so this cannot repeat.

  • Gauges: cancelled trains and gate failures become the first two rows of any CI watch, with “two in a row” as the alert line.
  • Measurement rule: a before/after table is not written until the sample contains at least one of each shape the change treats differently (here: a small and a hub-sized train).
  • A standing CI/CD agent that watches 24/7 and acts by playbook (intervene / wait / alert). Proposal: below.
  • The three plumbing traps that cost an hour on the fix PR (spot kill withdraws the approval; cancelled duplicate run read as verdict; nobody re-fires the review) go on that agent’s playbook as “green run but not queued” checks, and each gets its own fix PR.

Second incident, 4:12 PM: a green train turned main red

A PR that lets Clara answer a renter’s texted question (#16333) went through the queue green at 3:50 PM. Eighteen minutes later the post-merge checks failed: the PR added a new Clara tool without registering it in the tool inventory, and the test that checks that registry never ran on the train.

TimeWhat happened
3:50 PM#16333 merges; its queue train ran the full test set and the full guard-pin set, all green.
4:12 PMPost-merge heavy lane fails: tool-inventory drift test, 5 assertions.
4:20 PMDriver claimed (session 002). Revert #16406 opened, marked urgent, put at the front of the queue.
5:07 PMRevert merged and promoted to production at once. 5:32 PM: post-merge checks green again.
5:50 PMRoot cause found; fix PR #16410 opened (see below).

Why the train missed it

The test fell between two lanes. The queue’s full test run skips every guard-pin test on the assumption the guard-pin phase runs them. The queue’s guard-pin phase skips the ‘bookkeeping’ pins on the assumption the post-merge run covers them. This test was classed as bookkeeping, so on a queue train it ran in neither place; only the post-merge run saw it.

What changes (#16410)

One rule, enforced by a test: the queue may skip a test in the full run only if the guard-pin phase on that same train runs it. Bookkeeping pins now stay in the full run. The tool-inventory test is promoted to a blocking pin because it reads the whole tool tree, so no code change ever ‘selects’ it. The test ships with a mutation control: putting the old exclude list back turns it red.

Two corrections to my own first write-up

I first reported a second failure (a leasing guard). It was output from a passing test. I also first blamed the affected-test selection; the train had run the full set. Lesson, same as the morning: read the train’s logs before naming a cause.

Same shape as Pinterest

Pinterest's pipeline runs a dependency graph, then targeted tests, then a thin queue, then a post-merge check. We have the same shape. Our “services” are directories in one repo.

Receipts

WhatWhereState
Safety net (post-merge full suite, revert bot, blessed release)#16314merged
Queue change (thin queue)#16325merged 2:21 AM MT
Fix #1: full-set tier decided at run time (did not fix the skip)#16396merged 10:05 AM MT
Fix #2: no success(); names the needed results#16400merged 11:36 AM MT; first big train green 11:40
Backup revert of the thin queue#16397closed 11:56 AM MT, not needed
The reversed decision (full suite back in the queue)#15178reversed
HistoryADR-0107earlier decision

Fresh levers we are not using yet (research, Oct 4)

About 55% of our machine time goes to rebuilt trains and re-run PR checks, so these levers go after that waste.

#Lever (plain words)Expected effectEffortRiskStatusSource
1Split the waste by cause: Count minutes lost to rebuilt trains, re-run PR checks, flakes and idle CPU on the big machinesNone directly; shows which later lever is worth the mostLowNoneNEWRunsOn costs
2Retry only failed test files; quarantine repeat flakes: A flaky test stops kicking a good PR out of the trainFewer rebuilt trains (root of the rebuild cascade)Low-MedLow (needs a “failed twice = real” rule)NEWTrunk
3Skip a check whose code is identical: If the train’s code tree matches one already proven green, reuse that resultCuts re-run PR checks and the first PR in a trainMedLow-Med (tree must match exactly)NEWGitHub queue docs
4Cancel superseded PR-check runs: A new push kills the older run of the same PR (PR runs only, never queue runs)Fewer wasted PR-check minutes from review-fix pushesLowLowNEW (verify we lack it)GitHub concurrency docs
5Cheap checks cancel the big test job: Run lint and typecheck alongside tests; if they fail, stop the 64-core job at onceDoomed runs stop early; no delay for good PRsLowLowNEWGitHub concurrency docs
6Cache test results by input hash: Skip a test group when its files and dependencies are unchanged since a green runLargest minute cut after 2 and 3Med-HighMedNEWRunsOn S3 cache
7Balance shards by time; size machines to real CPU use: If the 64-core machines sit idle at the tail, split by test duration or use a smaller boxFewer machine-hours at the same wall timeLow-MedLowNEWRunsOn costs
8Small jobs on small machines: Lint, typecheck and bot steps should not run on big boxesFewer machine-hoursLowVery lowNEW (verify)RunsOn costs
9Larger queue groups (2-3 PRs per train): Fewer trains for the same mergesFewer builds, but a failed group needs a bisectLow (setting)MedNEW (verify first)GitHub queue docs
10Graviton (ARM) machines: Same work on cheaper hardwareLower price per hour, not fewer minutesMedMed (native modules)NEWAWS Graviton
11Independent lanes in the queue: An unrelated PR’s failure does not rebuild everyone behind itAttacks the cascade at its rootHighMed-High (replaces the queue)NEW, lastAviator
12Savings plan: Discount on on-demand machines onlySmall unless on-demand share is largeLowLowNEW, low valueAWS
-Spot “capacity-optimized”, circuit breaker: RunsOn already defaults to itNone--ALREADY HAVERunsOn
-Predictive test selection (Launchable, Datadog): We already select affected testsNone--ALREADY HAVEMachalica 2019
-Nx / Turborepo migration: Too big a rewrite; take only the hash-cache idea (row 6)None--REJECTED—
-Warm pools, CodeBuild Lambda, Dependabot grouping, artifact retention: Trains run 13 min, so a 25-second start is noise; the rest save penniesNone--REJECTED—
-Lower queue depth: Trades velocity for costNone--REJECTED (Fede)—

Try first, in this order: measure the waste by cause; retry only failed test files and quarantine flaky ones; reuse a green result when the code is identical; cancel older runs of the same PR; let cheap checks stop the big test job early; then save test results from the 15-minute main lane for the queue to reuse. None of this lowers queue depth or slows a merge.

No dollar figures: the baselines in the raw notes were not ours.

Proposed: a standing CI/CD agent (Fede, Oct 4: “like a full-time engineer watching twenty-four-seven”)

Status: Proposed — pending Fede’s pick. Today’s 8-hour outage was watched by nobody: the session that owned CI was busy on the next task and had no alert. The job below is small, boring and permanent: look at five numbers every 15 minutes, act by playbook, page a human only for the two things a human must decide.

Where it lives: three options

A. A job inside Agent Smith (Gera’s system, Mac mini)B. A dedicated CI seat on the Mac mini (own launchd daemon, claude -p turns, like Trinity)C. GitHub-native only (workflows that page and auto-rerun; no agent)
Already hasmerge-queue watcher, deploy watchdog, red-main claimer, steward sweep, Slack seatnothing yet; the hooks exist (gh-event-forward, pr-event-wait, red-main-claim)deploy-queue-watchdog, auto-merge-all, revert bot
Can it fix forward?yes (writes PRs today)yesno: it can rerun, page and revert, not write a fix
Who merges changes to itGera only (“nothing merges into agent-smith without Gera”)us, same dayus, via ci.yml PRs
Time to first useful versiondays (through the bridge, Gera’s queue)1 day: playbook + 5 gauges + Slack alerthalf a day
CostGera’s time; Smith’s tokens~1 Opus turn / 15 min when something is off, Haiku otherwisenear zero
Risktwo owners of one systemanother daemon on the mini (admission control exists)pages a human for everything it cannot fix

Recommendation: C now, B this week, A later if Gera wants it. C is the part that would have caught today in minute two (two cancelled trains in a row → Slack alert) and costs nothing; it ships as ordinary ci workflow PRs. B is the part that fixes forward at 3 AM without an open session; it runs the playbook below and nothing else. A folds B into Smith once the playbook has run for a few weeks and Gera has seen it work; a draft of B through the bridge first costs days we do not have.

Playbook: signal → wait / intervene / alert

Signal (checked every 15 min)WaitIntervene (no human)Alert (Slack: #agent-trinity; Fede DM only if he asked)
Merge-queue train cancelled1 cancel: normal (a real conflict is what the queue is for)2 in a row with the same failing job: read the gate error; if it names a job that skipped → look for success() + a skipped upstream job; if a runner death → gh run rerun3 in a row, or any cancel whose failing job is in .github/: page, with the gate line and the last ci.yml merge
Green PR, armed, not in the queue for >15 min—the three plumbing traps: approval withdrawn after a spot kill → gh workflow run claude-code-review.yml -f pr_number=N; debounce-cancelled duplicate run is the newest → gh run rerun; required context missing → re-fire itstill stuck after both: page
Urgent CI fix in the queue—move it to the front: dequeue the entries ahead, let it land, re-arm them (today: 49 min lost behind the PRs it fixed)—
Red main (push CI failure)—claim the driver slot; if the red is the last merge and the fix is not obvious in 20 min → revert that one commit (revert doctrine)page on the revert, one line
Post-merge heavy lane red on a commit whose train was green—Claim driver, revert the introducing merge with [urgent] to the front of the queue, then find which lane skipped the failing test; a test skipped by one lane must be run by the other on the same train.—
Heavy lane red (post-merge full suite)1 red from a known flaky file: wait for the rerun“environmental” classification → rerun; a culprit commit → revert bot already does it2 reds in a row with different culprits: page
Release lag (main ahead of release)< 45 min: normal (heavy lane cadence)> 45 min and heavy lane green: check the advance step, rerun> 90 min: page
Queue depth< 8 entries≥ 8 for 30 min: look for the head entry’s train; if it is failing, treat as row 1≥ 15: page
After any ci.yml merge—measure the next hour with at least one train of each shape the change treats differently; a 0 in any “ran” column is a red flag, not a savingno hub-sized train in 2 hours: fire a workflow_dispatch dry run on one
Spot runner killed mid-job—attempt 2 is automatic; re-fire the review if the approval was withdrawn≥ 5 kills/hour: page (switch the pool to on-demand for the day is Fede’s call)

Rules the agent keeps: diff against the last working version before theorising (today’s first RCA was wrong because I did not); every intervention is one line in ci-decisions.log and one line in the Slack thread; it never merges, closes or splits another lane’s PR, only re-arms a green one; it never touches a customer; and a human is paged with one plain line and a recommendation, never a wall.

What it is not: a reviewer, a builder of features, or a second owner of Smith’s lanes. It watches, nudges, fixes CI plumbing, and wakes the right owner.

The big moves

We test the same change about seven times. Compute is mostly avoidable (82–88%, estimate; the measured sample above puts guards at about a third of first failures, not most); the tests that catch real bugs mostly are not.

  1. Test each change once, in groups.Kills: 5.1 queue runs per merged PR; half of Oct 2's compute.Pick C: thin queue, one full run per batch after merge, auto-revert, canary.
  2. Stop gating on tests about CI itself.Kills: 383 tests, 46 placeholder jobs, ~1,100 guards off the merge path.Pick B + C: nightly guards, keep ~20 with a real incident behind them.
  3. Never rerun what did not change.Kills: reruns of code that already passed.Pick A this week, B next month: skip a passed tree, then cache per file.
  4. Cap rounds; check locally first.Kills: 1,423 review-triggered re-runs a day.Pick C: two CI rounds, local checks before push, no re-fire on verdicts.Also: a PR the line kicked out is not re-queued until its code changes.
  5. Spend safety where incidents come from.Kills: pre-merge checks that miss configuration and provider failures.Pick C: canary, instant rollback, revert bot, heavier checks on risky paths.
Diagrams, tables and decisions

Sequencing, agreed with Smith (Oct 3, 02:00Z)

  1. Prerequisite for move 1: deploys ship from the last batch that passed the full test set, not the tip of main. Today a merge is the production deploy, so testing after merge would ship untested code first. Move 1C waits for this and for move 5.
  2. Quick wins this week: stop re-running CI on every review verdict (1,423 runs on Oct 2); delete the 46 placeholder jobs; skip a run when the same code already passed (3A); two CI rounds per PR, with typecheck and affected tests run locally before each push (4).
  3. Move 2 is a filter, not a purge: guards fail only on NEW violations (2C); about 20 incident-backed guards stay hard gates; the 383 pipeline-shape tests get a keep/delete check one at a time. On Oct 2 a guard (property-owner-id-retired) stopped a PR from reading a credential by a retired field.
  4. Measure before rebuilding the queue: sample 48 hours of queue failures by cause (guard, flaky full set, real). If guards dominate, ratchets may remove most cancelled runs and the rebuild shrinks. First sample (Oct 1–3, our ledger of queue runs): about 10–12% of queue entries pass; of unique first failures, guards 29% (bookkeeping guards 24%), flaky tests 29%, machine problems 22% (runner disk full, fixed Oct 2), real bugs 20%. 88% of failed-job events are echoes of a group that was already doomed, so raw failure counts overstate guards.
  5. No pre-merge guard is deleted until the post-merge batch run, bisect and auto-revert are live and have caught a real failure (Trinity's caveat).

Diagrams 1 and 2: one change, today and after

Diagram 1: One change, seven test runs today

PR laneevery push~2.2 pushesper mergeReview boton each push(in the 1.3% bucket,no own count)Re-runsfire on everyreview verdict:1,423 on Oct 2Queue entrya run per queued PR:5.1 permerged PRSibling cancelsone fail drops allruns behind it:1,511 on Oct 2Full set~5 times permerged PR, ~3.4cancelledPost-mergedeploy on pushto main (1.3%);tested againBasic checks ran ~7 times per merged PR; guard shards ~7 times each (42 shard jobs)About 60% of all compute repeated a run that already happened or never finished
Runs per merged change: ~7 today (Oct 2 counts).

Diagram 2: The same flow after the five moves

PR lane (thin)type check +affected tests,~20 key guardsThin queuecheck againstfresh main,one runFull set, onceper batch (of 10),after mergeBisect + revertsplit failing batch,auto-revert,daily capCanarygradual rollout,instant rollback~1.1 pushes per PR, and one full run shared by a batch of 10
Runs per merged change: from ~7 to one thin run per push plus a shared full run.

Diagram 3: Six months of cost

$0$3,000$6,000$9,000$12,000050100150200$136Apr$1,002May$621Jun$1,166Jul$2,659Aug$10,904~$1,280Sep1738317245190Spend per month, before creditsMerged PRs per dayGitHub-hosted (gross)AWS-hosted CIMerged PRs per day (right axis)
AWS cost more than planned because the plan used August's 45 merges a day while merges grew 6–13×, and credits hid both bills. September GitHub was $10,904 gross but $2,239 charged.

The AWS line, Sat Oct 4 (Cost Explorer, gross before credits)

DayGrossCI machines (EC2)Product (DynamoDB, Lambda, logs, API, rest)Note
Sep 24–28 (avg)$58$0$58CI still on GitHub-hosted runners
Sep 29$347——CI moves to AWS runners
Sep 30$450——credits run out at month end
Oct 1–3 (avg)$531 (estimate: $1,592 usage ÷ 3; Oct 1 and 2 individually not pulled)——first days the page shows real dollars
Oct 3$429$324 (76%)~$105 (estimate: ~$45 of it is CI-induced, by subtraction from the $58 pre-CI baseline; public IPs $13 are measured)no cuts in it: the thin queue went live Oct 4, 2:21 AM MT

September usage was $4,012, covered in full by credits (so the spending page read ~$0). The CI bill did not grow on Sep 29; it moved from GitHub’s invoice to AWS and lost the credits. Of Oct 3’s CI machines, the two 64-core spot machines that ran the full suite on every train (c7g/c8g.16xlarge) were $102; the thin queue removes them from every train that is not hub-sized, a realistic $60–100/day. CI then sits near $250/day. A “dramatic” cut (CI at ~$65/day, $2k/month) only comes from running each change fewer times (today ~7 test runs per change: every push, every bot verdict, the queue, post-merge), which is the point of the five moves above. Dollar per move: next section to fill, with the first clean day (Oct 5) as the baseline.

Source: aws ce get-cost-and-usage, account 194925971150, Oct 4 12:05 PM MT. Oct 3 may still grow a few percent as Cost Explorer settles.

Diagram 4: PR size, CI cost and review quality

01530450%20%40%60%≤100101–300301–600601–1,000>1,00041.112.48.16.73.066.5%40.7%22.5%10.0%5.8%CI runs per 1,000 linesClean first reviewLines changed per PR (7,557 PRs merged, Aug 3 to Oct 1)CI runs per 1,000 linesFirst AI review clean (%)
Verdict: small PRs review far better but cost more CI per line; fix the cost with batching, keep the 600- and 1,000-line gates, aim for 100–600 lines.

What we delete, what we keep

ItemVerdict
Cancelled queue runs (50% of compute)Delete check against fresh main first
Full set once per queued PR (25%)Change once per batch, after merge
Guard shards in the queue (~5%)Delete from the queue
Guard shards on PRs (~5%)Shrink to ~20 incident-backed guards, rest nightly
383 tests about CI's own shape (~2%)Delete as gates
46 verdict and placeholder jobs (<1%)Delete
Re-runs on every review verdict (1,423/day)Delete the trigger
Product unit tests (~6,800 files)Keep affected subset on PR, full once per batch
Eval and replay testsKeep and grow
Authorization-scope testsKeep as a merge gate

What our sessions do locally, and what changes

The PR lane is about 9% of daily compute, so local habits are a few percent of cost. But they decide how many times a PR enters the line and whether it fails there.

Pulled by the two seats from their last 60–79 merged PRs (2026-10-03). Counts are real; interpretation is labeled where it is an estimate.

Agent Smith's seat (Gera's lane)Trinity's seat
Pushes per PR before greenMedian 2, average 2.3; worst 10% at 4+. 29 of 60 landed on one commit. Only 5 of 60 pushed in a merge of main.Average 2.66 extra pushes per merged PR, median 1. 34.5% of PRs get 3 or more, and those make 83% of extra pushes (79 PRs).
What drives the extra pushesSmith's read, not measured: mostly a review-bot 🟡/🔴 finding, then a red check, rebases last.Measured: pushes after a review verdict (15 of 23 after a 🟡, 8 of 25 after a 🟢, despite the rule), then rebases onto main (about 24% of extra commits).
What runs locally before a pushnpm run review:self, typecheck, touched test files only — never a full vitest run.typecheck and review:self about once per push. Guard tests: never.
Hand re-triggers of review/CI1 hand @claude comment across all 60 PRs; re-reviews mostly fire on their own on a new commit. Hand re-runs of CI jobs not measured.Not measured; re-reviews fire on their own on each new commit.
Habit that wastes runsRe-queueing a PR the merge line kicked out with no change (#15346, tonight); queueing several PRs from one session at once (three at 3:47 PM CT). Both happen because a PR's own CI run doesn't include the guard tests the queue runs, so it looks green when it isn't.Updating the branch from main mid-PR (the pre-push hook text tells sessions to rebase), and pushing after a 🟡 verdict.
Habit worth copyingRun the queue's guard tests locally against the touched files before marking a PR ready — would have caught all four of tonight's kick-outs on the laptop instead of in the line.A local push hook (live Oct 3) caps a PR at two CI rounds and blocks pushes after a verdict; run the line's guard set before the first push.

What changes

Habit todayChangeSaves (estimate)
Branches get updated from main mid-PR (about 24% of extra commits; the pre-push hook tells sessions to rebase)Stop. The line re-tests on fresh main anyway.About 80 runs a day
2.66 extra pushes per merged PR on average. 34.5% of PRs get 3 or more, and those make 83% of extra pushes.Hook-enforced two-round capUp to half of extra pushes
Pushes after a yellow verdict (15 of 23) or a green one (8 of 25), despite the ruleHook blocks the push; the finding becomes a follow-up issueFewer review rounds
Guard tests are not run locallyRun the always-run guard set (about 60 s) before the first pushFewer line failures (unsized)

Already working, keep: typecheck and review:self run about once per push. Docs-index, lockfile and PR-body fix-up commits are now zero in the sample. --no-verify is about 1 per two weeks.

Caveats: 79-PR sample (plus or minus 10 points). "Pushes" is an upper bound on runs. Estimates overlap, so do not add them.

What is still missing (Oct 3, 15:00Z)

After the overnight build. Ordered by impact. Owner in brackets.

  1. Deploys still ride the tip of main. The release branch now advances only after a green full run, but Vercel still deploys main. Until the deploy target moves, the full set cannot move to after-merge. [Fede: Vercel target]
  2. The revert bot has never caught anything. Bisect, revert and the 5/day cap are merged and unproven. No guard is deleted until it has caught a real red; a controlled proof on a throwaway branch would shorten the wait. [002]
  3. The line still runs the full set per group. Today's PRs trim it (bookkeeping guards out, unchanged code skipped); the thin-line shape is designed, not started. Gated on 1, 2 and a 48-hour by-cause measurement. [002 + Smith]
  4. Review re-runs CI about 1,400 times a day. Kill the re-run on every bot verdict; delete the 46 placeholder jobs. No PR yet. [Smith, item H]
  5. Guards assert strings, not properties. One label change on Oct 3 broke five drift tests that pinned the old text. Rule and sweep needed; nobody owns the keep/delete pass over the 383 pipeline-shape tests. [unowned]
  6. Flaky loop is half-closed. Detector comments only; auto-quarantine exists. Close detector → issue → quarantine with no human step; weekly flaky share. [002]
  7. Spot reclaims now eject groups (~3% of runs). Confirm the re-run path re-queues the group; add a reclaim bucket to the ledger; measure 48 h after the cheap-machines PR lands. [002]
  8. Rules live on one Mac. Two-round cap, no push after 🟡, guards before first push, no re-queue without a code change are local hooks. Repo-level versions (shipping rules in CLAUDE.md; the bot refusing round 3) are missing. [002 + Smith]
  9. One CI file, two swarms. Ten pipeline PRs serialize on one 7,000-line ci.yml; ~1 h of work costs ~10 h of waiting. Split ci.yml into per-job workflow files with lane ownership. [unowned; proposed next]
  10. No $ per merge. Machines + GitHub minutes per merged PR, weekly, beside throughput. [Smith's dashboard + 002 ledger]
  11. Not started: canary + instant rollback (move 5A); per-file test caching (move 3B, next month).

What "agents run CI" means: the operator is a swarm, so every rule has to be mechanical and every decision written where the other swarm reads it. Oct 2–3 worked because both swarms agreed one merge order in writing; it was slow because one file forced it. Item 9 removes the need, not item 8.

Decisions for Fede

MoveABCRecommendation
0 Deploy from last green batchKeep deploying main tipVercel deploys a release branch advanced by the batch runSame as B plus canary %B now, C with move 5
1 Test once, in groupsTune GitHub's queueBuy a batching queueThin queue, full set once per batch after mergeC now, B as fallback
2 Demote the guardsDelete the 1,155 guard filesRun them nightlyFail only on new violationsB + C; delete the 383 and 46
3 Never rerun unchangedSkip a passed code treeCache per test fileMove to Nx or TurborepoA now, B next month
4 Cap roundsTwo CI rounds per PRAgents check locally firstBoth, and stop re-firing on verdictsC
5 Safety where incidents come fromCanary and instant rollbackRevert bot with daily capA and B, plus heavier checks on risky pathsC

What the talks, podcasts and books add

  • Google, Future of Google Testing: no merge queue; 90% of test targets never broke after merge, so most tests can wait for the batch.
  • Google, Micco 2012 talk: testing every change hit a cost wall that grows with the square of volume. Same arithmetic as our queue.
  • Shopify, Universe 2019: tested every ordering of waiting PRs, dropped it within days, kept a failure tolerance before ejecting.
  • Uber, QCon-style talk: ordering by predicted success cut CI resources 53%.
  • Stripe, Minions talks and blog: two CI pushes, then a human (the talk describes about 10 retries); lint loops locally first; cost is the reason; 65% of PRs merge with no human edits.
  • Trunk, talk: agents treat a red build as their own fault and delete or sleep around flaky tests; look up known flakes before any retry.
  • DORA/Accelerate: a ten-minute feedback budget, locally and in CI.
  • Fowler, Continuous Delivery: revert the faulty commit from main first; review before merge adds friction.
  • Google SWE book: large tests run after submit; tests lose value near 1% flakiness (Google's own is ~0.15%).
  • Tension: Stripe runs 5M+ tests per PR on ~25,000 cores and invests in speed, not batching.

Caveats

  • Move 2 needs a filter, not a count. Building Evolutionary Architectures (via summaries) and Google's Beyoncé rule say keep a cheap, objective check with a named property or a real downstream consumer. The 383 go through that filter before deletion.
  • CI cost is not the only brake on AI PRs. LinearB data: AI-authored PRs wait ~1,000 minutes for review pickup vs ~200 for others, and merge 32.7% vs 84.5%. This brief is about CI cost and throughput; review wait is a separate problem.

Sources

Research notes: 13 files under ~/agents/002/research_notes/Agentic era CI at scale/ (web sources, talk transcripts, podcasts and books, local practices).

396 written sources, 24 talk transcripts, 8 podcast episodes (show notes), 4 books via free chapters and summaries, plus our own repo, bill and session data.

Research notes: internal audit, six months of CI cost and speed, PR size economics, merge-queue designs, post-merge testing and progressive delivery, test selection and flaky tests, which CI gates prevent defects, CI cost engineering, CI vendors and landing platforms, agentic-era CI practices, pure-agentic delivery and 10× volume.

Full analysis (long)

PropFlow's CI bill comes mostly from running the same checks on the same code again and again and throwing most of the results away. Machine prices are not the problem, and neither are the tests that catch real bugs. On October 2 the pipeline used about 1.27 million core-minutes (≈$360). Half of that went into merge-queue runs that were cancelled. Another quarter went into re-running the full ~8,000-file test set once for every PR waiting to merge (research note: internal audit). In a 48-hour sample of merge-queue failures, 77% (86 of 111) came from the 14% of test files that police the repo's shape (pinned counts, drift checks, tests about CI itself). Those failures were not product bugs. Nine of ten September production incidents were the kind none of the 8,000 tests could catch (same source).

What leading teams do, and what it means for us

Leading teams converge on one design: a thin, fast check before merge, one full run per batch after or at merge, automatic culprit-finding and revert, and a canary with rollback. No team that publishes its pipeline has dropped automated CI, even with AI writing most of the code. What they dropped is repetition, unbounded retry loops, and human waiting. Applied here, that design plausibly cuts compute 82–88% and lifts the merge line's throughput ceiling 5–10×, both estimates. The move to small PRs was worth it for review, and its CI penalty disappears once CI stops testing every PR separately. GitHub's machines are not the bottleneck. Its merge queue design and its API limits are. Renting AWS machines directly was the right call on price. The move went wrong because the plan was priced at August volume and startup credits hid the real bills on both sides.

The one-page answer

Five radical moves, as choices

# Move Option A Option B Option C Recommendation
1 Test each change once, in groups Keep GitHub's merge queue and tune depth and batch size Buy a queue that runs one test for a batch of PRs, finds the bad PR by splitting the batch, and retries a flaky failure before evicting (Trunk, Mergify, Aviator, Graphite) Keep a thin merge queue (type check plus affected tests only). Run the full set once per batch after merge, then bisect (split a failing batch in half and retest until the culprit is found), revert it automatically, and deploy through a canary C now, B as fallback. A cannot beat the (1−p)/p ceiling below. GitHub's native queue still starts one run per queued PR even when a group merges together, so "once per group" needs B or C (not verified; first open question below)
2 Demote the guard layer Delete the 1,155 guard files Run them nightly. They open one issue per failing family and never block a merge Turn exact-count pins into "fail only on new violations" ratchets B + C for guards; delete outright the 383 tests about CI's own shape and the 46 jobs that only report other jobs' results. Keep about 20 guards with a real incident behind them as merge gates
3 Never run a test whose inputs did not change Skip a run when the exact same code tree already passed Cache each test file's result, keyed on a hash of the file plus everything it imports, stored in S3 Migrate to Nx or Turborepo packages A this week, B next month. C pays off little in a monolith (Mercari)
4 Cap rounds and verify before pushing Hard cap of two CI rounds per PR, then escalate Agents run type check and affected tests locally before every push Both, and stop re-firing CI on every review verdict C. Stripe caps agents at two CI rounds (Stripe). We average about 2.2 pushes per merge and had 1,423 review-triggered re-runs on Oct 2
5 Spend the safety budget where incidents come from Canary plus instant rollback on Vercel and Lambda Bot that bisects post-merge failures and opens reverts, with a daily cap A and B, plus heavier pre-merge checks only for risky paths (auth, permissions, money, outbound messages) C. Our incidents came from configuration, providers and end-to-end behaviour, and canaries catch that class

Headline numbers from our own data

Why the merge line cannot keep up. In a merge queue where each PR is tested on top of the ones ahead of it, one failure throws away every run behind it. Say each queued PR fails with probability p. Then merges per round can never exceed (1−p)/p, however deep the queue (research note: merge queue designs). Uber measured exactly this ceiling (EuroSys'19). Our per-entry p has not been measured directly. Queue runs failed 28% of the time in the Sept 30–Oct 2 window (research note: six month history), which suggests p of roughly 20–30% (estimate). The merge line passed 24–42% of its runs (the figure in the brief). On Oct 2 it was 702 of 2,214, or 32% (research note: internal audit).

Per-entry failure rate p Merges per round, queue depth 10 Ceiling at any depth
30% (our likely range, estimate) 2.3 2.3
20% 3.6 4
10% 5.9 9
5% 7.6 19
2% 9.0 49

With ~10-minute rounds, a ceiling of 3 means about 18 merges per hour against peak arrivals of 30–50 per hour (estimate). That gap is why the queue grew to 26 entries at 46 arrivals per hour on Oct 2 (research note: six month history). The lever is p, not depth.

Where Oct 2's compute went (≈1.27M core-minutes ≈ $360, ±25%) (research note: internal audit):

Bucket Share
Merge-queue runs cancelled and thrown away (1,511 runs) 50%
Merge-queue runs that passed (702), of which the 64-core full test set alone was 25% of the day 34%
PR checks (1,475 runs) 9%
A measurement-only shadow job (now off) 4%
Deploys on push to main 1.3%
Review bot, fences, Lambda deploys, review-triggered re-runs 1.3%

Per merged PR, the basic checks ran about 7 times, the six guard shards about 7 times each (42 shard jobs), and the full set about 5 times, about 3.4 of them cancelled. About 60% of all compute repeated a run that had already happened or was never allowed to finish (same source).

14% of tests produce 77% of queue failures. Of 7,995 test files, 1,155 (14%) are guards. They read source files as text, scan the repo, pin exact counts, or police CI. In a 48-hour sample they caused 86 of 111 named merge-queue failures, and 50% of the failing runs had no other cause. Each guard passes alone and fails when a sibling PR moves main underneath it. In 150 sampled failures, zero were the type or build clashes a merge queue exists to catch. In the PR lane, about 41 of ~70 failure signals were the pipeline failing on itself (same source).

Six months of cost. Credits hid both bills (research note: six month history):

Month Merged PRs/day GitHub Actions (org-wide) AWS CI (before credits) Median open→merge
Apr 17 $136 $0 ~0.2 h
May 38 $1,002 $0 ~0.45 h
Jun 31 $621 $0 ~0.4 h
Jul 72 $1,166 gross / $864 net (Jul 14–31 only) $0 ~0.5 h
Aug 45 $2,659 gross / $800 net ($10k startup credit from Aug 17) $0 ~0.5 h
Sep 190 $10,904 gross / $2,239 actually charged (credit ran out Sep 25) ~$1,280 CI + ~$1,860 one-off AWS Config/Security Hub (covered by AWS credit) 0.4 → 0.9 h
Oct 1–2 ~510 ~$21–25/day ~$280–440/day (estimate; AWS credit runs out ~Oct 3) ~2.6 h
Period $/day Merges/day $ per merged PR
GitHub, Aug 1–17 $65 45 ~$1.45
GitHub, Sep 22–28 $708 282 ~$2.50
AWS, Oct 1 (estimate) ~$383–438 585 ~$0.70
Target ($65/day at ~500 merges) $65 500 ~$0.13

PR size (all 7,557 merged PRs, Aug 3–Oct 1, no sampling; CI runs counted on the PR side only) (research note: pr size economics):

Lines changed Merged Median time to merge First AI review clean Pushes after review Review threads PR runs per merged PR PR runs per 1,000 lines Reverts per 1,000 PRs
≤100 1,509 0.24 h 66.5% 0.54 0.39 1.79 41.1 2.7
101–300 2,120 0.51 h 40.7% 1.31 1.07 2.43 12.4 3.3
301–600 1,826 0.80 h 22.5% 2.29 1.97 3.50 8.1 5.5
601–1,000 1,065 1.25 h 10.0% 3.89 3.68 5.23 6.7 5.6
>1,000 1,037 2.04 h 5.8% 6.36 7.06 7.99 3.0 5.8
Period PRs merged/day PR runs per 1,000 merged lines First AI review clean Median time to merge Share of lines from 1,000+ line PRs
Before the small-PR rule (to Sep 9) 50 3.7 24% 0.64 h 80%
Small-PR rule (Sep 10–28) 227 7.3 32% 0.63 h 41%
Size gate (Sep 29–Oct 1, 3 days) 444 9.0 43% 1.28 h 19%

Leaders run a thin gate, then batch, then revert

The largest published systems do not run the whole suite on every change before merge. At Google, presubmit runs only fast, reliable tests, with an average wait of about 11 minutes. A change that passes has a 95%+ chance of passing everything else. The slow tests run after submit, in batches. A failing batch is automatically split to find the culprit, and rolling back is the default fix (SWE at Google ch. 23). Google gave up testing every commit separately because compute grew with submissions times tests (Memon et al. 2017). Meta runs fewer than a third of the dependent tests and still catches over 99.9% of faulty changes. Everything runs on main every few hours, and that run catches the rest (Machalica et al.). Shopify ran about 60% of tests per build with 99.94% recall. Only five failing tests escaped in 8,360 merged commits, because the full suite runs on every deploy (Shopify).

Merge queues at volume batch PRs and retry before evicting. Shopify tests batches of 8 PRs, runs 3 batches at once, and evicts a PR only after repeated failures (Shopify). Its P90 merge time fell from 693 to about 41 minutes after it split the queue into independent lanes (Graphite case study). Uber's queue took its iOS main branch from green 52% of the time to always green (EuroSys'19). It later found 40–65% of its speculative builds were aborted. Running only the builds likely to matter cut CI resources 53% (arXiv 2501.03440). The research agrees. Batching with bisection used 72% fewer test runs than testing each commit, and only 42% fewer once flaky tests entered (Najafi et al.). On 285M test results, batching saved 88% of machines and missed zero failures. Test selection alone missed up to 55% (Fallahzadeh et al.).

Agent-first teams add three habits. Stripe merges 1,000+ agent-written PRs a week. Lint runs locally in under 5 seconds, CI selects from 3M+ tests, and the cap is two CI rounds before a human steps in (Stripe). OpenAI's harness team kept builds under a minute and kept blocking gates minimal ("corrections are cheap while waiting becomes expensive"). Flaky tests get follow-up runs instead of blocking (mirror of OpenAI post). Meta's RADAR lands 25K+ low-risk diffs a day with no human review. They get reverted at one-third the rate and cause incidents at one-fiftieth the rate of other diffs, because a model trained on past reverts decides which diffs qualify (arXiv 2605.30208). Across 153k merges, 94% still go through queues one at a time. AI-assisted PRs broke main 1.9% of the time against 4.4% for others, and teams of 2–5 broke it 0.77% of the time (Mergify 2026). Team size predicts breakage far better than AI authorship does.

What we are missing is structure, not settings

We have no "test once" principle. Every published system tests a given code tree once, or once per batch. We test it on the PR, again on each queue rebuild (5.1 queue runs per merged PR on Oct 2), again after review verdicts, and until recently again on main (research note: internal audit). Small open-source repos that skip runs whose exact code tree already passed went from 37 to about 4 machine-minutes per merge (hydrozoa #767). GitHub's queue reuses nothing from the PR run (GitHub Docs), so the reuse has to be ours.

We have no admission bar for gates. Google lets a check block a commit only if it reports correctness issues and has essentially zero "effective false positives", meaning findings developers ignore or work around. To appear at review at all, a check needs under 10% (SWE at Google ch. 20). A guard whose usual fix is "bump the number" fails that bar by definition. Google calls these change-detector tests and calls them harmful (Testing Blog). Agents make it worse. A 2026 study found agent-written tests lean toward print statements and exact-value checks, and they barely change task outcomes (arXiv 2602.07900). Our test code is now 113 MB against 82 MB of product code (research note: internal audit).

CI feeds on itself. The main workflow file is 7,201 lines. Workflow files took 1,096 edits in 14 days. 23% of commits on main touched no product code, and 22% touched CI or test infrastructure. The loop runs like this. A guard breaks a sibling. Someone adds a control to prove the guard can fail. A manifest pins the guards. A test checks the manifest. Then the manifest disagrees with itself, which caused 9 of 40 sampled PR failures (same source). Each layer is reasonable on its own, and together they are the largest single source of red.

The queue has no answer for flakes. GitHub's queue evicts on the first failing status and rebuilds everything behind it. It has no bisection, no retry-before-evict, and no independent lanes (research note: merge queue designs). Every flake therefore costs a full round for every PR behind it. Google measured flaky results on about 1.5% of test runs and spent 2–16% of compute re-running them (Micco). Slack cut its test-job failure rate from 57% to under 5% by automatically disabling flaky tests and assigning each one to an owner (Slack).

The safety net sits in the wrong place. September's incidents were these: a setting switched on by mistake, email gating logic, a cross-company data leak, an AppFolio site change, a voice provider outage, the CI spend itself, voice dead-air, a voicemail detector made default without end-to-end evaluation, and home-matching logic (research note: internal audit). Only the data leak is a class that a pre-merge test (an authorization-scope test) is built to catch. The rest need canaries, replays on real traffic, and fast rollback.

Nobody owns cost per merged PR. Every AWS projection used August's 45 merges a day. Credits masked both bills, and nobody tracked the metric that matters: dollars per merged PR (research note: six month history).

The 80% bet holds on compute and over-holds on jobs

Fede bet that 80% of the compute and jobs are unnecessary. The data supports that for compute, and for jobs it is conservative. Of 56 jobs in the main workflow, 10 do work. The other 46 turn other jobs' results into check names, or post "(not applicable)" so required-check lists stay satisfied. A typical PR shows ~170 check names, 141 of them skipped (research note: internal audit). The audit's redesign lands at 12–18% of today's compute, about $45–65 a day (estimate). An earlier internal cost model put the recommended package at ~$176 a day, with a $116 stretch. It said $65 needs test selection by changed files, ~1.1 pushes per PR, and ~1% bad PRs (research note: six month history). The two estimates differ on one question: does the design remove queue repetition structurally, or tune it? Only structural removal reaches $65.

Item Share of compute Real defects caught Verdict What covers the risk instead
Cancelled queue runs 50% 0 by definition Delete. Check against fresh main before queueing; eject a failing PR in one round Nothing needed
Full set once per queued PR 25% The same 3 real bugs a once-per-batch run would catch Once per batch, after merge (Move 1C) Post-merge run, bisect, revert, canary
Guard shards (slices of the guard tests, each on its own machine) in the queue ~5% Sibling collisions only Delete from the queue Run once on the PR against fresh main
Guard shards on PRs ~5% ~3 of 40 sampled (route inventory, protected classes, smoke password) Shrink to ~20 incident-backed guards, one 2-minute shard; the other ~1,100 run nightly Nightly issue, review bot
383 tests about CI's own shape ~2% 0 product defects, 41% of queue reds Delete as gates; keep ≤10 weekly One owner for the workflow file
46 verdict and placeholder jobs <1% 0, but 23 of 40 PR reds Delete Three required checks: checks, tests, queue verdict
Re-runs on every review verdict (1,423/day) ~0.5% 0 Delete the trigger Bot already posts its verdict
Product unit tests (~6,800 files) inside the full set The real bugs Keep: affected subset on PR, full once per batch —
Eval and replay tests small 5 of ~14 real PR catches Keep and grow: highest yield per minute —
Authorization-scope tests small The class behind the Sep 8 data leak Keep as a merge gate —

The deletions carry a risk. Architecture drift may take a day to show up instead of minutes. The evidence says that trade is safe: none of September's incidents was drift of that kind, and Meta found more post-merge faults from PRs landing in parallel than from tests it skipped (Machalica et al.).

Small PRs paid off for review, and batching removes their CI cost

Yes, the move was worth it. Above 1,000 lines, the first AI review comes back clean only 5.8% of the time, against 66.5% at 100 lines or fewer. Pushes after review run 12× higher (6.36 vs 0.54), review threads 18× higher, and median merge time is 8.5× longer (2.04 vs 0.24 hours) (research note: pr size economics). Reverts rise steadily with size, from 2.7 to about 5.5–5.8 per 1,000 PRs above 300 lines, though 33 reverts is too few to be firm. Graphite's industry data has the same shape: PRs over 500 lines get updated about 5 times after opening (Graphite). Over the three periods, the share of lines from 1,000+ line PRs fell from 80% to 41% to 19%, and the first review came back clean 24%, then 32%, then 43% of the time. Under the 1,000-line limit (Sep 29–Oct 1, 3 days), merges per day reached 444, with 43% clean first reviews and 9.0 CI runs per 1,000 lines. PRs over 1,000 lines are still 13.7% of merged PRs but 57% of merged lines (same source).

The CI side has a real cost. Merged PRs per day went 50 → 227 → 444, while merged lines per day only went 58k → 113k → 162k, so PR-side runs per 1,000 merged lines rose 3.7 → 7.3 → 9.0. Tiny PRs are the worst per line: 41.1 runs per 1,000 lines, and 7.9% of them close unmerged (same source). Median merge time doubled to 1.28 hours under the size gate, but that period is only three days and coincides with the merge queue coming back (26% of merges went through it, and 13.8% of those were kicked out at least once), so the slowdown cannot be pinned on PR size. The CI penalty exists because every PR gets its own queue run and its own main-branch run. With batching, CI cost per merged PR falls almost N-fold for a batch of N (Mergify). Group-testing math puts the best batch near 1/√p: about 7 PRs at a 2% failure rate, about 3 at 10% (estimate) (research note: merge queue designs). Verdict: keep the 600- and 1,000-line gates. Aim for 100–600 lines. Fold non-urgent sub-100-line follow-ups into the next PR in the same lane. Fix the CI cost through batching, not bigger PRs. One caveat: the retro's claim that 1,000+ line PRs were "7% of PRs but 38% of blocking findings" does not match this data (13.7% over 60 days, 3.8% under the gate), and blocking findings by size were not pulled. Treat it as unverified.

GitHub limits the queue, not the machines; vendors sell speed, not savings

What went wrong with the move from GitHub to AWS

The per-minute promise held. AWS ran about $0.0014 per machine-minute on Sep 30, against GitHub's $0.005–0.014 (research note: six month history). Four things broke the plan. First, it froze August volume (45 merges a day) into every projection ($300, $680 and $1,100 a month), while merges grew 6–13× and machine-minutes 6–18×. Second, credits hid both bills. The "$10k GitHub month" was gross, and the card was charged $2,239. GitHub's credit ended Sep 25 and AWS's runs out around Oct 3 (estimate), so both became real cash within about a week of each other. Third, there were side costs and switch mistakes. AWS Config and Security Hub recorded every short-lived runner, costing ~$1,860 in 5 days. The AWS runners were switched off during an AWS outage on Sep 21 and left off ~7 days at peak volume, costing $1,909 in real GitHub cash over Sep 25–28. 1.26M of September's minutes ran on GitHub-hosted jobs that never moved. A full quota for spot machines (AWS spare capacity sold at a discount that AWS can take back) pushed ~72% of AWS machine dollars onto on-demand machines on Sep 30, with 66k–134k launch failures a day. Fourth, the merge queue came back on Sep 30 without batching. It threw away half its runs, and median open-to-merge rose from ~0.5 to ~2.6 hours. One session claimed AWS cost "$5–6/day" and the true figure was ~$310 (same source). The most accurate line in the record is from Sep 29: "the savings are in minutes, not the vendor."

Price per run by platform

The table prices a 64-core, 15-minute run, close to our full set (64 cores × 10.9 minutes) (research note: vendors and platforms):

Platform $/run $/month at 500 runs/day $/month at 2,000 runs/day
AWS spot via RunsOn (what we run) $0.18 $2,633 $10,530
AWS on-demand, no spot $0.58 $8,700 $34,800
Namespace 64-core, prepaid $1.20 $18,000 $72,000
GitHub-hosted Arm 64-core $1.47 $22,050 $88,200
Blacksmith, 2 × 32-core shards $1.92 $28,800 $115,200
GitHub-hosted x64 64-core $2.43 $36,450 $145,800
Depot 64-core (docs price) $2.88 $43,200 $172,800
Buildkite hosted $3.84 $57,600 $230,400

Self-hosted spot is 8–14× cheaper per run than any hosted option (RunsOn pricing; GitHub pricing). Vendors earn their markup with faster consumer CPUs (about 1.5–2× single-thread speed), sub-3-second boots, and warm disks that stay mounted between jobs (research note: vendors and platforms). Even if the faster CPUs halve wall time, Namespace still costs 3.5–4.5× spot (estimate). The price gap and the minutes gap multiply, and the minutes gap (5–10× from test-once) is ours to close. Verdict: stay on AWS spot via RunsOn. Do not go back to GitHub-hosted. Consider a fast-CPU vendor only for latency-critical jobs. One watch item: GitHub postponed, not cancelled, a $0.002/min fee on self-hosted runners (GitHub Changelog). That fee taxes many small jobs most, which is another reason to delete the 46 placeholder jobs.

Is GitHub itself the bottleneck?

GitHub's machines are not the bottleneck: 2,000 15-minute runs a day average about 21 concurrent machines against a limit of 1,000 (GitHub limits). Three parts of GitHub do bind. The merge queue starts one run per queued PR, with no bisection, flake grace or lanes, and jumping the queue rebuilds everything (GitHub Docs). On Apr 23, 2026 it produced wrong merge commits that reverted code in 2,092 PRs, and its monitoring did not notice (GitHub blog). The API limits cap content creation (PRs, comments, statuses) at 500 per hour per identity, and the built-in Actions token at 1,000 requests per hour per repo, or 15,000 on Enterprise Cloud (GitHub rate limits). At 10× our volume, a single bot identity will hit the content cap at peak. Capacity is the third: GitHub itself says agent load has it planning for 10–30× growth (same blog). The answer is not to leave GitHub. Make the queue thin (Move 1C), spread bot work across several App identities, and use webhooks instead of polling. If post-merge reds stay high, buy a third-party queue. Check first whether those vendors count bots as paid seats.

A 12-month plan for 10× volume

The plan has four phases. Each phase ends with a measured number, not a date. The exit numbers are targets, not forecasts.

Phase What changes Exit number
Weeks 0–2: stop the waste Delete the 46 placeholder jobs, the 383 tests about CI's shape, the review re-trigger and the shadow job. Demote ~1,100 guards to nightly. Check each PR against fresh main when it is armed. Skip runs whose code tree already passed. One owner for the workflow file, with one measured change per day Queue failure rate p under 5%. Cost per merged PR under $0.30
Weeks 2–8: move the full run after merge The queue runs only type check and affected tests. The full set runs once per batch on main. A bisect bot runs in shadow for a week, then opens and merges reverts for repeat-confirmed failures, capped per day. Vercel canary with instant rollback (Vercel Rolling Releases). Per-test-file result cache $65/day at ~500 merges. Median open→merge back under 0.5 h
Months 2–6: ration by risk Auth, permissions, money and outbound-message paths get the heavier pre-merge set and deep AI review; everything else gets the thin gate. Two-round cap per PR. Agents run type check and affected tests before pushing. Bot work spread across several identities. Cost per merged PR on a dashboard with a budget alarm ≤1.1 pushes per merge. Revert rate tracked by risk tier
Months 6–12: choose the landing model If post-merge reds or revert attribution get worse, buy a batching queue with lanes, or build a small landing service. Otherwise keep the thin queue Ceiling ≥10× today's peak arrivals

The cost arithmetic is an estimate, built from inputs measured on our pipeline: $0.033 per affected-only PR run, $0.20 per full-set run, and ~$35 a day of fixed side costs (research note: six month history). At today's ~500 merges, 1.1 pushes each, and batches of 10, the total is about 500×1.1×$0.033 + 50×$0.20 + $35 ≈ $63 a day. At 5,000 merges it is ≈ $315 a day, about $0.06 per merged PR. Cost then grows with batches, not with PRs. Throughput gets a similar estimate. Deleting the guard collisions removes ~77% of named queue failures. Checking against fresh main and allowing one flake retry should push p from ~25% toward 2–5%. That lifts the per-round ceiling from about 3 to 19–49. With ~10-minute rounds, that is roughly 115–290 merges per hour against today's 30–50 peak. Full 10× at peak may still need two or three independent lanes.

What stays true when agents write and review everything

Traditional CI still makes sense, but its job changes. It stops being a bug net run on every push. It becomes a tamper-proof referee plus fast recovery. No organization with a published pipeline has dropped automated checks, even at 75–100% AI-written code. What they cut was human waiting, retry loops and repetition (research note: pure agentic future). Three safeguards should survive any cut.

A central CI the authoring agent cannot edit. On deliberately impossible tasks, frontier models gamed the tests up to 76% of the time, and cheating fell to near zero when the tests were hidden or read-only (ImpossibleBench). GitHub tells reviewers to check CI changes first, because agents skip tests and weaken thresholds (GitHub blog). It also does not run Actions on its own coding agent's pushes until a human approves (GitHub Docs). Our workflow files took 1,096 agent edits in two weeks, which is exactly the exposure this safeguard exists for. Protect the workflow files and the list of gating tests with an owner rule.

Authorization tests at the route boundary. The Sep 8 cross-company data leak was the one September incident a pre-merge test is built to catch (research note: internal audit). Those tests stay as merge gates however thin the rest gets.

Canary plus rollback, and careful auto-revert. Waze estimates canaries could prevent a quarter of its incidents (Google Cloud). Google's 2025 outage report ordered every critical change behind a default-off flag with gradual rollout (Google Cloud incident). Auto-revert needs limits. Google's SafeRevert reverts only 56% of true culprits so that false reverts stay at 0.5%, because finding the culprit is right only 77% of the time per change (SafeRevert). Bots can also break things more than people do: one automated release tool at Google broke the build on 39 of its 42 commits (Memon et al.). AI review is precise but its recall is unproven: vendor benchmarks put bug catch rates anywhere from 18% to 82% (research note: pure agentic future). It informs the gate. It does not replace it.

Open questions and the cheapest test for each

Question Why it matters Cheapest experiment
Does GitHub's "merge the group if its last PR passes" setting cut the number of runs, or only relax the verdict? Decides whether Move 1 can stay on GitHub's native queue Sandbox repo, 3 PRs queued together; count the triggered runs (one hour)
What is our real per-entry failure rate p, flakes included? Sets the throughput ceiling and the batch size Sum one day of queue events by outcome and cause; re-run each failure once
How many real bugs does the full set catch that the affected subset misses? Justifies moving the full set after merge Run the full set on main after each merge for a week; count reds the PR lane passed
Does demoting the guards let drift reach customers? The main risk of Move 2 Two weeks of nightly guard issues; trace any that would have caused an incident
What hit rate would a per-file result cache get? Sizes Move 3B Replay the last 500 merges offline, computing file-plus-imports hashes
Will the auto-revert bot blame the wrong PR? False reverts erode trust Shadow mode for a week: open revert PRs without merging; score them by hand
How much does a cancelled queue run actually cost? The 50% figure is modelled Add start and end times to the CI collector; sum machine-seconds by outcome for one day
Did the size gate shift the PR mix? Detail data stops at Sep 22 Re-run the existing PR analysis script once the pull finishes
Do third-party queues charge for bot authors? Decides Move 1B's price One email to each of Trunk, Mergify, Aviator and Graphite

Conclusion

The pipeline became a product the agents build for themselves. A fifth of commits and a large share of red runs now serve CI's own rules. Machine prices are the wrong place to look: the vendor table spans 20× from cheapest to dearest, yet our biggest overspend comes from repeating runs we already bought. Two numbers deserve to become the operating metrics. The first is cost per merged PR, which fell from $2.50 to ~$0.70 and needs to reach ~$0.13. The second is the per-entry false-red rate p, which sets how many PRs can merge per round, whatever queue depth or vendor we choose.

Agents shift where verification belongs, not how much of it matters. Writing code got cheap, so a slow gate now costs more than a bad merge that a canary catches and a revert undoes in minutes. Gates that agents can edit or game are worth less than ever. The lasting design is a thin pre-merge gate the agent cannot edit, one honest full run per batch, and fast, rate-limited recovery. It is also the only one of the options here whose cost grows with batches rather than with PR count.

Sources

The page draws on thirteen research notes written for this review (not published; the last two cover talk transcripts, podcasts and books, and local practices). What each covers:

  • Internal audit: our CI from first principles. Every job, test class and run on Oct 2: where the compute went, how often the same suite repeats, the CI-about-CI loop, and the verdict on the 80% bet.
  • Six months of CI cost and speed. Month-by-month merges, GitHub and AWS bills, credits, inflection points, and what sessions claimed versus what happened in the move to AWS.
  • PR size economics. All 7,557 PRs merged Aug 3–Oct 1 by size: review loops, merge time, CI runs per line, reverts, and the three rule periods.
  • Merge-queue designs at very high volume. How Uber, Google, Meta, Shopify, Zuul, GitHub and others batch and handle failures, plus the throughput ceiling model.
  • Post-merge testing and progressive delivery. What runs before versus after merge at Google, Meta, Chromium and Mozilla; culprit finding, auto-revert, canaries and rollback.
  • Test selection, result caching and flaky tests. Meta and Google test selection, content-addressed caching, TypeScript monorepo tools, and quarantine versus retry for flakes.
  • Which CI gates actually prevent defects. Defect yield per test type, when drift guards and ratchets help or turn brittle, Google's presubmit rules, and AI-written tests inflating CI.
  • CI cost engineering. Hosted versus self-hosted price and performance, sharding economics, waste from cancelled and re-run jobs, caching, and cost per merge benchmarks.
  • CI vendors and landing platforms. Per-run and per-month prices for a 64-core run across vendors, what they sell beyond machines, and where GitHub breaks at thousands of PRs a day.
  • Agentic-era CI practices. How teams with agent-written code changed PR volume, pipelines, pre-push checks and landing, from published sources.
  • Pure-agentic delivery and 10× volume. Organizations near 100% AI-written code, AI review as a gate, auto-revert risks, and what to build for 10× change volume.
PropFlow Docs