CI/CD: where the two hours go, and the big swings

Every PR merged since we moved CI to our own machines (10,766 of them, Sep 15 → Oct 29): what happened to speed and cost, an honest accounting of splitting the app, and the moves that reach the targets.

Approved 2026-10-29 (Fede): C plus dev tools out. All merged by Oct 30 14:42 UTC; 48-hour watch to Oct 31: rollout log. Tests-at-open reverted Oct 30. #23884 and #23886 live, two arm-run cuts in build; marketing-site repo built and pixel-identical, cutover pending Fede (Oct 30 23:10Z). · Proposed 2026-10-29 · a decision page under the CI velocity plan · targets from today's team call: open→live 60 min, merge→live 30 min, an emergency lane · recomputed from GitHub and the AWS bill with the factory's own formulas (the factory page was not readable from the session)

We got cheaper, not faster. A PR takes about 2 hours from open to live (was 1 hour on GitHub's machines) and 54 minutes from merge to live (was 14–22). Most is configured waiting: a hidden 30-minute deploy window, a 12-minute release spacing, tests that wait for arming, and a merge queue that re-tests every PR alone: 2,372 runs in two weeks, 54 reds, none of them needing a code change. Splitting the app saves 1–3 minutes per merge; it is not the lever. Deleting waits and double work is.

125
min open → live, median, this week
target 60 · was 64–73
54
min merge → live, median
target 30 · was 14–22
2%
of PRs live within an hour
was 39–48%
$75
CI per day on AWS, Oct 16–29
target $65 · GitHub era $738
$0.48
per merged PR, Oct 16–29
about half a dollar since Oct 5 · GitHub era about $3
Rollout log 48-hour watch from Oct 30, 04:14 UTC

Order chosen by how easily each change is undone, not by size of win. Baseline for every before/after: week of Oct 26, open→live 125 min, merge→live 54, 2% live within an hour, about $75 of CI a day. Reports every 8 hours in #spending, three lines each.

When (UTC)ChangeHowUndoStatus
Oct 30 04:14Production deploy window 30 → 5 minrepo variableone line, in the ledgerlive; Smith no objection; reverses Gera's Oct 11 call on Fede's go; keep/30 decided at hour 48 on the Vercel delta
Oct 30 04:52Batch-train parking window 30 → 5 min (trains live since Oct 20 park armed green PRs to share one queue run; the page had missed it)repo variableone line, in the ledgerlive
Oct 30 05:25Batch trains drained (CI_BATCH_TRAINS=drain, variables deleted after one tick)repo variableset it back to livelive; judge: 0 of 33 PRs rode a batch since Oct 28, +18 min median per PR
Oct 30 04:53Checkpoint T0 (baseline, last 8 h before anything took effect): open→live 146 min, merge→live 59, 0% live within an hour, 62 PRs merged; runs PR 280 / queue 78 / main 53; fails 17%, cancels 30%report—PRs 23656 (dev tools) and 23657 (review bot) open
Oct 30Unit tests at PR open (undoes only the test deferral from #20840; the rest of the Oct 17 cost program stays); always-run list sharded in a second PRPRRevert buttonmerged Oct 30 06:20 (#23661, judge SHIP); sharding PR #23673 merged 06:33 (judge SHIP); cost counted: ~1.5× test runs per commit for ~17 min per PR
Oct 30 06:40Release gate: a docs-only main tip no longer freezes releases (pre-existing defect found by the watch: 7 holds, 111 min in 48 h)PR #23684Revert buttonmerged, judge SHIP
Oct 30Delete the 12-min release spacing; delete the 3-min main debounce2 PRsRevert buttondebounce merged 06:45 (#23686, judge SHIP); spacing merged 07:30 (#23708, judge SHIP; watch worker rollouts/h, Temporal time-outs, Lambda cold starts)
Oct 30Review bot: 🔴 only with file, line, reproducing input and why tests miss itPRRevert buttonmerged 06:52 (#23657, judge SHIP); watch: 🟡 findings that later turn into reverts
Oct 30Stale dev tools out of the production build (.local.tsx); hub hides them in productionPRRevert buttonmerged 07:00 (#23656, judge SHIP): 14 of 830 routes out; in-use tools stay; QA on production 07:05: hub shows 16 tools, the five stale ones gone, removed route 404, Status page loads (screenshot)
Oct 30Express lane v1: [urgent] + express label → thin PR run, immediate promote, max 3/day, guard on paths, incident link and a daily cap (who applied the label is not a guard: every session acts as Fede)2–3 PRsRevert buttonjudge SHIP on all three branches; #23718 (deploy dispatch keeps the red-sweep hold) merged 08:35; release side #23671 merged 10:46 (ships a merged express PR at once, promote-now takes a PR number, red-sweep check now sees push-triggered sweeps); PR side #23752 merged 11:40 (thin run on express label + [urgent] + ## Incident, guarded by the dangerous-diff list and a rolling 24-h cap of 3). Express lane v1 live. no queue jump yet (Smith: a jump restarts every train; needs the red-main claim); floor ~28 min until the queue is thin
Oct 30 12:53Checkpoint T1 (hour 8, 04:53–12:53 UTC): open→live 91 min (T0 146, week 125), merge→live 18 (59 / 54), 31% live within an hour (0% / 2%), 81 PRs merged; runs PR 275 / 15 min, queue 93 / 10, main 61 / 5; fails 12%, cancels 24%. Most changes landed in the second half of the window, so the next checkpoint is the first clean read.report—all plan PRs merged except the thin queue (#23767, one fix pending)
Oct 30 06:26Red sweep: one flaky test (turnover work-order ids drawn in parallel since June; 4–5 of 30 runs fail under load). Releases held 24 min; re-test green 06:50. Not a rollout change. Fix-forward PR on the test.incident—resolved; driver 009
Oct 30Sweep per merge batch (30-min coalescing, ~41 sweeps/day, +$5–7/day)PR #23699Revert buttonjudge SHIP (twice); one bot catch fixed: the urgent-merge check could not see push-triggered sweeps, now every sweep reader is event-agnostic with a scanner; merged 11:28
Oct 30Sweep retries failed test files once (judge's precondition before the queue drops its test re-run: at 41 sweeps/day, flakes alone would freeze releases 1–2 times a day)PR #23715Revert buttonmerged 08:10 (judge SHIP); would have saved 4 of 6 red sweeps last week; first live proof is the next red shard
after ackMerge queue keeps typecheck, lint, build and guard pins; drops only the unit-test re-run (saves ~$6–9/day; +2–4 min merge→live on normal merges because push:main now tests)PR #23767Revert buttonjudge SHIP (gap stated in his words); Smith acked with one condition, built: a red sweep files the red-main claim at once (driver: Smith by his choice) and opens the issue; the #alerts page is held out because Fede's 2026-10-01 ruling keeps CI health off Slack except the 8-hourly #spending report: Fede's call: allow one #alerts line per red sweep, or keep the ruling. Smith: claim + issue meets his condition. PR #23767 re-armed after the judge's and the bot's findings (skipped-test block gone, mutation control added, claim-posted flag into the issue); judge SHIP; disarmed because Smith's listener drops a claim posted by his own bot user: the claim must post as Trinity, which needs a new repo secret (TRINITY_SLACK_BOT_TOKEN, the bot token already in the Keychain). Fede said finish: the secret is in; merged 14:42. Every change in the plan is live; the 48-hour watch continues to Oct 31 22:40 CT.
deferredRevert-not-freeze on a red sweep; queue jump for express——not in the 48 h; the driver reverts by hand
Oct 30 16:00–18:15 UTCFleet overload——1,640 runner jobs in the hour vs 593 the day before; PR runs 74 vs 29; AWS spot out of stock; GitHub Actions degraded. Tests-at-open made each PR run about 25% heavier. Main stayed green; production stayed current.
Oct 30 20:55 UTCReverted: tests at PR open (#23661, #23693)——Fede: repeats the Oct 17/19 finding (2.1 runs per PR vs 1.3 at arm). Tests and guard pins run once a PR is armed again (#23845, #23854). Expect open→live to lose about 10 minutes until the review-rounds work lands.
Oct 30 19:40 UTCOpus 5.5 max full pass, every change vs history——One change repeated a known failure (tests at open). Fifteen keeps. The 5-minute deploy window and the removed release spacing stay only while Vercel spend is near its $6.67/day budget; Vercel added to the watch.
Oct 30 20:30 UTCTried and dropped: tests start once on the bot's green——Built (#23873), measured about 2 minutes saved per PR against one extra type-check/lint/build run per PR. Closed, branch kept.
Oct 30 21:00 UTCReview-rounds read, 397 PRs——Pushes after the first verdict: 48% fix a red check, 23% fix a real bug, 18% merge main, 10% new work. Five changes worth 13–18 min per PR. First two opened: no merging main to pick up a fix (#23884); five cheap hygiene checks in the local self-review (#23886).
Oct 30 20:54 UTCCheckpoint, hour 16——Last 8 hours: open→live 73 min (week before 125), merge→live 21 (54), 34% live within an hour (2%), 80 PRs merged. Runs: PR 295 at 16 min, queue 92 at 9, main 61 at 8. Deploys on pace for ~66/day (was 42). No red main, no red sweep.
Oct 30 21:30ZWhere the hour goes, measured on 88 PRs merged todayper PR, median minutes: open → first bot verdict 2; first verdict → last push 8 (p90 109); last push → armed 6; armed → entered the queue 19 (p90 50); queue → merged 10. First verdicts: 41 green, 30 yellow, 16 red.—read; the 19-minute “armed → queue” block is the largest piece left
Oct 30 21:40ZWhy PR runs go red (60 failed runs today)26 real test breaks by the author, 8 policy gates, 4 type errors, 3 flaky, 0 machine failures, 12 double-counted re-runs—read; reds are real, so the lever is catching them on the laptop, not CI flake
Oct 30 21:50ZDropped: skip the repeated type-check/lint/build job at arm timeOpus audit (never tried; #20840 measured it as waste but it was never built). Over 39 arm-time runs it saves 0.0 min of wall time; only ~$3/day of machine cost—not a speed lever; parked as a cost item
Oct 30 22:05ZFound: test shards start 8 min into the arm-time runthey wait on the “affected” job: 66 s deep checkout, ~2.5 min full dependency walk (vitest list --changed, no cache), ~2 min running the selected tests serially, 41 s always-run list; each shard then does its own 60 s deep checkout—two no-behavior-change cuts in build: shallow checkout in both test jobs (~1.5 min), send the selected tests to the shards (~2 min)
Oct 30 22:00ZReview-rounds change 1 merged: never merge main to pick up a fixPR #23884 merged 48742e697; the pre-push guard loses its override, the manual and the review bot say sorevert #23884live
Oct 30 22:03ZReview-rounds change 5 armed: fast self-review runs five cheap CI checks before the first pushPR #23886 armed at 5204221165 after three red rounds on its own test file (git env shape, then two type errors); five judge suggestions filed as #23904–#23908, bot nit #23910revert #23886live (merged 5d6e82095)
Oct 31 01:20ZShallow checkout on the test and shard jobs mergedPR #23931 merged a45c267f5; each checkout 62–63 s → 20 s, about 1.5 min per PR; a spot-reclaim exit inside a sub-step was misreported as a lint failure on the way (issue #23961)revert #23931live
Oct 31 02:22ZAffected selection runs on the shardsPR #23950 merged 391d3741b; planted failing test went red on one shard, the aggregate and the required check (run 41154171774); unit-test job 433 → 307 s, first shard start 600 → 395 s, arm-time run 931 → 792 srevert #23950live
Oct 31 03:50ZTwo push-hook false positives fixed#24051 (deleted-file check no longer halts on a comment path that climbs past the repo), #24055 (symbol sweep ignores nine ordinary words); found when the marketing-dashboard cleanup could not pushrevert eitherlive
Oct 31 04:53 UTCCheckpoint, hour 24——Last 8 hours: open→live 111 min (week before 125), merge→live 26 (54), 10% live within an hour (2%), 203 PRs merged. Runs: PR 651 at 13 min, queue 234 at 9, main 151 at 7; fails 14%, cancels 18%. Vercel production deploys: Oct 29 42, Oct 30 67. No red main, no red queue run, no red sweep. Open→merge is the whole gap (79 min median); merge→live is done.
Oct 31 12:53 UTCCheckpoint, hour 32——Last 8 hours (overnight): open→live 114 min (week before 125), merge→live 15 (54), 25% live within an hour (2%), 20 PRs merged. Runs: PR 44 at 13 min, queue 20 at 9, main 20 at 7; PR-run fails 36% (author test breaks), queue and main 0%. Vercel production deploys: Oct 31 30 by 12:53Z. Nothing new went live; no red main, no red queue run, no red sweep.
Oct 31 15:11 UTC#24082 merged: marketing dashboard code out of the app——Closes the marketing move: the staff dashboard is served from its own repo and Vercel project behind Google sign-in (live since 01:44Z); the app no longer builds or routes it (34 files, −2,984 lines). Two false reds on the way from the arm-run verdict adopting a cancelled sibling run (issue #24115); a full re-run of the label run cleared it. Rollback: Revert #24082, DNS unproxied, domain back to the app project.
The trend, week by week median minutes per merged PR
0 30 60 90 120 150 180 target 60 (open→live) target 30 (merge→live) Sep 29: our AWS machines · Oct 1: merge queue · Oct 4: release train Open → live, week of Sep 14: 64 min median over 1184 PRs 64 Open → live, week of Sep 21: 73 min median over 1824 PRs Open → live, week of Sep 28: 155 min median over 2785 PRs Open → live, week of Oct 5: 111 min median over 1867 PRs Open → live, week of Oct 12: 115 min median over 1407 PRs Open → live, week of Oct 19: 119 min median over 881 PRs Open → live, week of Oct 26: 125 min median over 818 PRs 125 Merge → live, week of Sep 14: 13 min median over 1184 PRs 13 Merge → live, week of Sep 21: 22 min median over 1824 PRs Merge → live, week of Sep 28: 51 min median over 2785 PRs Merge → live, week of Oct 5: 25 min median over 1867 PRs Merge → live, week of Oct 12: 60 min median over 1407 PRs Merge → live, week of Oct 19: 52 min median over 881 PRs Merge → live, week of Oct 26: 54 min median over 818 PRs 54 Sep 141184 PRs Sep 211824 PRs Sep 282785 PRs Oct 51867 PRs Oct 121407 PRs Oct 19881 PRs Oct 26818 PRs Open → live (median)Merge → live (median)
Dashed green lines are the targets. The jump at Sep 29 is not the machines: the same weeks added the merge queue (Oct 1), the release train (Oct 4), the 30-minute deploy window back on (Oct 12) and tests-on-arm plus a 2-hourly sweep (Oct 17). Hover a point for the week's PR count.
Where a typical PR's 119 minutes go Oct 16–29, the PRs between the 40th and 60th percentile, n=436
Waiting to be armed (tests do not start until then): 17 min 17 Armed test run, review, fix pushes: 29 min 29 Merge queue re-test: 12 min 12 Merge → release moves (main check ~8, then 12-min spacing): 22 min 22 Release → live (30-min window, wait behind last deploy, 6-min build): 39 min 39 Before merge: 58 minAfter merge: 61 min, of which ~40 are the window and the spacing Waiting to be armed (tests do not start until then): 17 min Armed test run, review, fix pushes: 29 min Merge queue re-test: 12 min Merge → release moves (main check ~8, then 12-min spacing): 22 min Release → live (30-min window, wait behind last deploy, 6-min build): 39 min
Blue is before merge, red after. The real work after merge is about 14 minutes (main-branch check ~8, Vercel build ~6). The urgent path that skips the window and spacing already does merge→live in 20 minutes. The slowest tenth of PRs (385 min) spend 157 minutes before they are even armed: parked work, not CI.
The decision pick one; "hold" keeps the 30-minute window and the per-PR re-test
OptionWhat it doesOpen→liveMerge→liveCI $/day (now ~$75)EffortHits 60 / 30 / $65?
A Stop waitingDelete the 30-min window, the 12-min spacing and the 3-min main debounce; delete dead and advisory work~80–85~15–20about −$2 (and +$6–10 on Vercel)~3 daysno / yes / no
B A + test onceTests start at open; the queue only type-checks and builds the merged tree; a red sweep reverts its culprit instead of freezing every release~45–55~15–20−$35 to −$50~10 daysyes / yes / yes
C B + express lane recommendedFix-to-production lane in ~15 min with a guard; a 🔴 needs a failing input or test, otherwise it is a 🟡 issue~55–60 in the 48 h, ~50–55 once the queue is thin (judge's numbers, Oct 30)~16–21; emergencies ~28+$30 to +$45 during the 48 h; +$10 to +$25 after; cost comes down only with the fix-push narrowing~15 daysyes / yes / not yet
D C + first splitDev tools out of the production build; marketing site as its own app1–3 lower1–3 lowerabout the same~22–26 daysyes / yes / yes

Recommendation: C, in this order. The window and spacing today (one setting and a small PR), then tests at open, then the thin queue with revert-not-freeze, then the express lane and the evidence rule for reds. Dev-tool deletions ride inside the cleanup; the rest of the split waits. Everything in A–C ships without switches, as fixes to live behaviour.

What the data corrects six things we believed that the numbers do not support
  1. Machines are not the bottleneck. A spot machine arrives in 20 seconds; building and type-checking are ~2 min of PR time and ~8 min after merge.
  2. Volume is not either. We merge 146 PRs a day now, against 212 a day in mid-September when open→live was 60.
  3. The AWS move did not slow us down. Cost per merged PR has been about half a dollar since Oct 5 while the waits were added around it. Four natural experiments say the window decides merge→live:
    PeriodDeploy windowMerge→live med / p90Open→live medDeploys/day
    Sep 15–24, GitHub machinesnone14 / 2060140
    Sep 24–28, GitHub machines30 min48 / 728558
    Oct 7–11, AWS + release train5 min20 / 287381
    Oct 12–17, AWS30 min (repo variable)61 / 8011641
    Oct 17–29, plus tests-on-arm and the 2-h sweep30 min53 / 7712036
  4. The merge queue re-test catches little, not nothing corrected Oct 30. The first count (54 failures, all spot reclaims, all merged unchanged) looked only at runs the queue let through; a rejected run ends as cancelled. Recount by the Opus judge: about 50 rejections in 36 hours across 40 PRs, 11 of which needed a fix before merging, and at least 5 were breaks that appear only against current main (3 caught by guard pins, 2 by type-check or build). So the thin queue keeps guard pins plus type-check, lint and build, and drops only the test re-run. The 7-day recount (Oct 30): 65 real rejections, 35 by guard pins, 27 by type-check or build, 3 unit-only (a flake, a spot reclaim, a closed PR), so a queue that keeps guard pins would have missed none. Its saving is about $6–9 a day, not $20–26.
  5. Rework multiplies everything. The median PR gets 3 CI runs; 35% of runs are cancelled by a newer push; 35% of PRs have a red run. Each push after open restarts the ~21-min armed run: open→merge is 41 min with no pushes, 57 with one, 78 with two. Even a clean PR (38% of PRs) takes 101 min open→live, so the fixed waits have to shrink.
  6. Gera and Fede see the same pipeline. Last two weeks: same size, same review rounds, same red rate, same queue and deploy times; 125 vs 120 min open→live. Gera's PRs wait 27 min to arm; Fede's arm at open and sit 45 min in the armed phase, about 18 of them parked by batch trains (live since Oct 20, though the census called them dark; 0 of 33 of Fede's PRs since Oct 28 rode a batch). Draining the trains fixes Fede's side; tests at open fixes Gera's.
The big swings, measured deletions and skipped work first; minutes and dollars each removes
#MoveOpen→liveMerge→live$/dayEng-daysRisk / what could breakReverses
1Stop waiting on purpose: delete the 30-min window variable (the docs already say 5), the 12-min release spacing and the 3-min main debounce−35 to −4055 → 15–20Vercel +$5.5 to +$9.7; AWS −20.5–1Deploys go from 36 to 70–105 a day: more Lambda and worker ships, more cold starts (warm-up exists). Smaller batches roll back more easilyOct 12 window; Oct 6 pacing
2Queue proves only the merge: type-check and build the merged tree; drop its test re-run and the 2-min wait−6 to −80−20 to −26, net about 0 to −6 once the sweep runs per batch1–2A test break that only shows on the merged tree reaches main; guard pins stay in the queue and the per-batch sweep catches the rest (needs #6 first)Sep 30 queue reinstatement
3Test once, early: unit tests start at open alongside the review; fix pushes re-run only the tests the fix touches; the 11-min always-run step runs only when its inputs change, or is split across machines−20 to −250+$10 to +$20 as built (about 1.5× test runs per commit); the narrowing of fix-push re-tests, later, is what makes it net negative3–4A wrong narrow test pick lets a break slip; the queue type-check and the sweep catch itthe Oct 17 deferral
4Delete dead and advisory work: 13 dead workflow files (14% of the YAML); day0-replay, the pre-queue type check and the eval dispatch loop off the PR path; 4 red report-only nightlies; 8 sub-minute crons folded into one; the deploy wait off the runner0–20−5 to −72Low: a few reports stop—
5🔴 only with evidence: a blocking finding names a failing input or test, otherwise it becomes a 🟡 issue−6 to −8 (mean)0−5 to −6 + tokens1Real bugs could slip; watch the revert rate. 27% of PRs get a 🔴 and it doubles time to merge (49 → 110 min), mostly from the restarted test run, so #3 shrinks every round—
6A red sweep reverts its culprit and ships the rest (no global freeze)slowest 1%slowest 1%01–2Medium. Freezes added 66 h of deploy gaps since Oct 12 but touched only 20 of the 250 slowest PRs—
7Express lane (below)emergenciesemergencies~03–4Becoming a shortcut; see the guard—
8First split: dev tools out of the production build; marketing site as its own app−1 to −3−1 to −3small6–11Login and middleware are shared—

Before (this week, medians)

open→live 125 min · merge→live 54 · 2% of PRs live within an hour · ~$75/day

After C (judge's estimate, Oct 30; measured every 8 h below)

open→live ~55–60 min · merge→live ~16–21 · a clean PR ~35–40 · cost up $30–45 a day during the 48 h until the fix-push narrowing lands

On #1, the Vercel doc from Oct 11 prices the window at $5.55–$9.72 a day; that puts Vercel at $15–19/day against a $6.67 budget. If we want Vercel flat later, build the production bundle once on our machines and only upload it (also removes the 6-min build), at the price of production secrets on CI machines: a security call. Faster machines are on no list: the whole checks job is under 4 minutes.

The emergency express lane a fix commit in production in about 15 minutes

Today there is no way past the merge queue: the ruleset has no bypass list, editing it empties the queue, and GitHub's "jump to the front" option exists but is unused. [urgent] only skips the spacing and the window after merge (measured 8 cases: 41.5 min to merge, 20 to live). The manual promote failed 3 of its last 6 runs on a short commit id.

StepToday ([urgent])Express
PR checks~21 (armed run)~6 (type-check, lint, affected tests, split across machines)
Review bot5–6, in parallel5–6, in parallel
Merge10–12 (queue)~4 (jump to the front; type-check and build only)
After merge~8 main check, then ~6 build~6 (Vercel build starts at merge, promote in seconds)
Total~60~14–16

Guard, so it never becomes the normal path: an express label plus an [urgent] title and an incident link (who applied the label is not a guard: every session acts as Fede); no change under auth, middleware, permissions, secrets or the merge guard; at most 3 a day, each posting one line to #alerts; the full sweep runs on the shipped commit and a red one reverts it automatically; a weekly count on the Decisions page; fix the manual promote to accept a short id or a PR number.

Splitting the app: the accounting what each piece weighs and what pulling it out saves

The repo was inventoried unit by unit: files, lines, whether the unit is in the web build, the type check and the test set, and which workflow deploys it. Compile and type-check are about 2 minutes of a PR run and about 8 minutes after merge (queue build 1.8 min, Vercel build 5.5 min). Those are the only minutes a split can touch. None of the splits touches the review, queue or deploy waits that make up the two hours.

UnitSizeWhere it costs timeWhat pulling it out savesWhat ties it inVerdict
Dev tools + staff admin + admin/dev APIs301 of 1,001 routes (30%); 893 files / 194k lines only they use (11% of the web build)queue build, Vercel build0.5–1.5 min per merge, 0 per PR; build memory (a build died of memory on Oct 16)shared login, data layer, UI kitDo it, no split needed rename unused routes to .local.ts, the way 10 dev routes already stay out of production; 1–3 days
Marketing site (public)100 files, 25k lines, ~3% of the buildbuildseconds for product CI; marketing edits ship in ~5 min instead of queuing behind the appsame host, middleware, sitemap, shared componentsGo — Fede, Oct 30 evening 5–8 days; plan below
Lambdas + renewal worker215 files, 63k lines, ~1% of type check and testsnothing on a PRnothing; they already deploy on their own but wait for the whole web CIimport src/libWorkflow change, not a split
Clara agent loop agents/888 files, 288k lines; 314 files compiled into the web app6% of type check, 4.5% of tests1–2 min216 files import src/lib; 195 src/lib files import agents back; an import alias falls through to itArchitecture project weeks; undo the two-way imports first
scripts/ + evals/7,500 files, 1.66M lines, 1,070 test files (10% of the test set)tests, scripts type check (13 s)seconds per PRevery file imports src/libOwn packages, later
Tests2.56M of the 4.64M type-checked lines (55%)type check 36 s——Type check is already small

Fede asked for this on Oct 29; it was parked as Later behind option C. Tonight two one-line homepage copy PRs each ran the full app CI, so it is a go.

Also found: mock-pms-service, testing-harness and tools are type-checked for no reason; two stale pages ((public)/test, api/mock) can go; 8 packages have no importer. Seconds each, zero risk.

Moving the marketing app (marketing.propflowai.co) out Live — Oct 31

Decision (Fede, Oct 31 00:1xZ): option A, with login, no MFA, "like how we did factory." Cut over the same night: the dashboard now lives in its own repo and Vercel project (propflow-marketing), and marketing.propflowai.co sits behind a Cloudflare Access app with Google sign-in for any @propflowai.co address (same shape as factory.propflowai.co; the app itself refuses any request without a valid Access token). The visits number reads the main app's Vercel Analytics project, so it matches what the dashboard showed before.

StepBeforeAfterProof
Where the code livesMain app repo, every change = a full app CI runOwn repo, own 1-minute CI, own deployrepo propflow-marketing, main a3f690d
Who can open itThree platform admins after app sign-in + MFAAnyone with a @propflowai.co Google account, one sign-in, 30-day sessionAccess app 69df77f1, policy email_domain=propflowai.co
marketing.propflowai.coApp project, 307 to /login302 to propflowai.cloudflareaccess.com (Google), then the dashboardcurl 01:44Z; origin reached through Cloudflare (cf-ray + x-vercel-id on a throwaway probe host, deleted)
Dashboard code in the app~2,500 linesRemoved in PR #24082 (merged Oct 31 15:11Z, a12be253c)#24082; two push-hook false positives fixed on the way (#24051, #24055)

Still open: one real Google sign-in on the new host by a person confirms the six views (every automated step is proven: edge redirect, origin reachable, token audience matched, six views on the preview build); if it fails, rollback is Revert #24082 + DNS unproxied + domain back to the app project. Also open: whether the dashboard keeps the copied read/write AWS key or gets its own read-only user (Fede's call); rollback = set the DNS record back to unproxied and move the domain back to the app project (two API calls, under a minute). Scoping that preceded the decision is kept below.

The scoping (Oct 31, 00:00Z)

What it is. One staff dashboard at marketing.propflowai.co (Overview, Traffic, Channels, Campaigns, Leads, Connections), about 1,000 lines of page code plus 1,500 of data and LinkedIn code, built Oct 24, open only to the three platform admins after sign-in and MFA. It reads visit events and campaign counts from the traffic table, demo requests and the LinkedIn token from the main table, and the Vercel Analytics and LinkedIn APIs. No cron feeds it.

What cannot move. The visit-recording endpoint the public pages post to, the /go campaign links and their counters, the code that writes traffic rows, and sign-in. Only the dashboard (the read side) can move; the one shared contract is the traffic key layout.

OptionShapeEffortMain risk
AOwn repo and Vercel project on marketing.propflowai.co, reading the same tables8–12 daysSign-in: passkeys, MFA and the admin roster live in the app; A must route sign-in through the app or verify the session against it
BSame repo, own Vercel project, a marketing-only CI class so a dashboard PR skips the app's tests3–5 daysA path-filtered required check can wait forever (seen before in this repo)
CLeave it0.5 dayNone new

Change volume: 13 commits in 60 days, 8 of them on the day it was built; nothing since. A move buys ownership isolation, not publish speed. Recommendation: C; B if Fede wants it out for ownership; A only if a non-engineer must ship dashboard changes alone. Unverified: whether the app's session check accepts a forwarded cookie from another server; the traffic table's access role.

Marketing site as its own repo: the plan Proposed — pending review

Tonight's two copy changes each paid the whole app pipeline: type check, lint, build, test shards, paid bot review, merge queue. One needed a 19-minute run after two cancelled ones; the other went red for 13 minutes and re-ran. The case is speed to publish and keeping copy edits away from the app, not CI spend: only 3 of 998 PRs merged Oct 16–30 were marketing-only (~42 machine-minutes in 14 days).

TODAY: ONE REPO App repothe app and the public site, together Full app CI, for every changetype check, lint, build, test shards, bot review, queue One Vercel projectone build serves both addresses propflowai.co app.propflowai.co AFTER OPTION A: TWO REPOS Site repo (new)public pages only Light site checksbuild, screenshot, copy lint Site Vercel projectpreview link per change propflowai.co App repothe product, unchanged Full app CIas today App Vercel projectas today app.propflowai.co
Two shapes. Today a homepage copy edit waits on the same pipeline as a product change. With A, it builds and previews on its own; the app does not move.
OptionWhat it isTrade-offPick
A New repoSite repo with its own Vercel project on propflowai.co; the app keeps app.propflowai.co. Light checks. The UI pieces the site uses (18 UI-kit and 7 app-library modules, counted) and the brand colors and fonts copied once.Two copies of a few components. Every app page served under propflowai.co today (sign-in, legal pages, callbacks) must be listed and moved before the cutover.Recommended the only option where a copy edit never touches app CI
B Same repo, own packageSite as a separate package; a path filter runs only site checks on site-only PRs.Still shares the merge queue and bot review. A path filter is a new switch, and ours have crept.Second
C Copy-only fast laneText-file-only PRs skip the app pipeline.Smallest, but only copy; layout and component edits still ride full CI.Stopgap
TodayWith A
Copy change to live60–75 min: one app CI cycle plus the queue (open→live median 73 min today, 125 last week)Minutes: a site build and preview. To measure (the accounting above estimates ~5 min)

What makes A work. 37 site files import the app's UI kit, 28 import app library code, 2 touch login-like paths. Those are what get copied or cut.

Step 1 inventory, measured Oct 30
Routes on the public host21 site, 24 app, 13 shared or unclear (legal pages, sitemap, robots, og images, contact form)
API routes the site calls5, all app routes; /api/contact is called by the contact form but has no handler on main
Shared pieces to copy or package18 UI-kit modules, 7 app-library modules
Env vars read2
Tests importing the marketing tree39
Analyticsin the root layout (Vercel Analytics plus a first-party beacon); none in the marketing tree
Preview-token mechanismnone in the marketing tree

Rollout, dark and small.

  1. List every route served under propflowai.co today; mark each site or app.
  2. Create the repo and Vercel project on a preview address; copy the public pages and the UI pieces they need; make it build.
  3. Screenshot every public page against production at 1440 px, headless, and diff.
  4. Cut propflowai.co over at a quiet hour, with an instant swap back. Fede's go; turning the root domain on is his call.
  5. Delete the public pages from the app repo in one PR.

Risks to carry through: identical URLs for search ranking, analytics tags (they live in the app shell today, not in the site code), the [preview] build flow for unpublished designs, and who owns the site repo (Fede's sessions or Gera's).

Footprint: src/app/(public) 100 files / 24,874 lines; src/components/marketing/page 77 / 21,314; marketing/components 12 / 10,741; marketing/home 29 / 1,865; 21 tests / 2,222 lines; 37 assets under public/marketing and public/videos. (standalone)/marketing is an internal analytics dashboard and stays. No site repo exists today; camellia-site is the nearest shape. Tonight's copy PRs: #23897, #23899.

Gera's side (Agent Smith, Oct 30 22:15Z): agrees the end state is a separate repo with its own Vercel project, but recommends two steps. Now: a CI filter so a PR that only touches marketing paths runs lint, a build and the bot review, then goes live without the full test suite. Then: the separate repo. His coupling read matches ours: the marketing tree pulls brand tokens, the home content module, shared UI and composition pieces and the logo animation; about 12 tests and the middleware (which decides what the root domain serves) depend on it, so the root route and the preview links move with the site. He found no earlier written decision either. He asked that the two auth-like imports be named on this page.

Stopped, Oct 31 00:35Z: Fede: Gera is already driving the landing page refactor, so this lane ends here. What exists is handed to Gera through Smith: the repo, the idle Vercel project, the merged dark app change (inert while the setting is off), and the cutover issue. Nothing customer-facing changed.

Built, Oct 30 23:10Z. Steps 1–3 done: route list (21 site, 24 app, 13 shared), repo propflow-site with its own Vercel project on a preview host, 27 pages pixel-identical to production (one animated icon differs by 0.002%). Shape changed to multi-zone: the app keeps propflowai.co and every app path; only the 27 marketing paths get sent to the site, and only when a setting is on (off = today's behavior). Found and fixed before any traffic: cache header parity, "don't index" on the preview host, assets served from the site's own host, and the security header that lists allowed asset hosts (both repos). The app-side change is PR #23963, merged dark Oct 31 00:0xZ: nothing changes until the setting is on (cutover issue #23957). A throwaway probe proved a Vercel rewrite stamps the forwarding domain on the request, so the live pages will stay indexable. Cutover ask is with Fede. One slip: the traffic beacon was live on the preview host during the first screenshot runs; being counted, nothing deleted without Fede.

Decided: A — Fede, Oct 30 22:30Z ("go, you have 4 hours"). Build window to about 02:30Z Oct 31: new repo, own Vercel project on a preview host, screenshot match of every public page against production. Constraints: every domain accounted for, nothing breaks, analytics identical. The root-domain cutover is a separate go from Fede.

Smith's two-step order (B now, then A) is on the table as a fourth choice; the trade is a faster copy lane within days against building a path filter that is thrown away when the repo moves.

Workflow cleanup 92 files, 45,670 lines of YAML; what to delete
  • Dead, delete: the old auto-merge logic (3,522 lines, off since Oct 1), the review re-check and rule-9 re-check (off), four shadow measurement lanes, branch preview domains, two runner canaries, two never-run A/B lanes. 13 files, 6,351 lines.
  • Red and nobody reads them: the database-escape sweep (6% green), the weekly stress run (0%), the nightly renewal canary (52%), the renewal scoping evals (33%). Their only output is an 8-hour report whose schedule is commented out. Fix with an owner, or delete.
  • Spend with no consumer: day0-replay runs ~650 times a day writing an artifact; the pre-queue type check runs ~379 times a day and nothing required reads it; the behavior-eval pack was dispatched 75 times in one day by one actor, 84% red.
  • Machines started only to wait: 8 crons at 10–30 min intervals start ~600 machines a day for sub-minute jobs; the deploy job holds a machine ~30 min per deploy while Vercel builds (~1,450 machine-min/day).
  • Review plumbing: each review verdict starts four more workflows (~83,000 runs in 30 days); six workflows can write the required review check.
Cost GitHub era vs our machines, per day and per PR
$0 $200 $400 $600 target $65/day GitHub-hosted era: $738 a weekday (Sep 22–26) 2026-09-29: $294 of CI (AWS gross minus the $53/day the product costs) Sep 29 2026-09-30: $397 of CI (AWS gross minus the $53/day the product costs) 2026-10-01: $542 of CI (AWS gross minus the $53/day the product costs) 2026-10-02: $712 of CI (AWS gross minus the $53/day the product costs) 2026-10-03: $376 of CI (AWS gross minus the $53/day the product costs) 2026-10-04: $101 of CI (AWS gross minus the $53/day the product costs) Oct 4 2026-10-05: $285 of CI (AWS gross minus the $53/day the product costs) 2026-10-06: $274 of CI (AWS gross minus the $53/day the product costs) 2026-10-07: $118 of CI (AWS gross minus the $53/day the product costs) 2026-10-08: $158 of CI (AWS gross minus the $53/day the product costs) 2026-10-09: $164 of CI (AWS gross minus the $53/day the product costs) Oct 9 2026-10-10: $38 of CI (AWS gross minus the $53/day the product costs) 2026-10-11: $50 of CI (AWS gross minus the $53/day the product costs) 2026-10-12: $78 of CI (AWS gross minus the $53/day the product costs) 2026-10-13: $58 of CI (AWS gross minus the $53/day the product costs) 2026-10-14: $149 of CI (AWS gross minus the $53/day the product costs) Oct 14 2026-10-15: $150 of CI (AWS gross minus the $53/day the product costs) 2026-10-16: $122 of CI (AWS gross minus the $53/day the product costs) 2026-10-17: $85 of CI (AWS gross minus the $53/day the product costs) 2026-10-18: $0 of CI (AWS gross minus the $53/day the product costs) 2026-10-19: $59 of CI (AWS gross minus the $53/day the product costs) Oct 19 2026-10-20: $102 of CI (AWS gross minus the $53/day the product costs) 2026-10-21: $18 of CI (AWS gross minus the $53/day the product costs) 2026-10-22: $99 of CI (AWS gross minus the $53/day the product costs) 2026-10-23: $64 of CI (AWS gross minus the $53/day the product costs) 2026-10-24: $39 of CI (AWS gross minus the $53/day the product costs) Oct 24 2026-10-25: $44 of CI (AWS gross minus the $53/day the product costs) 2026-10-26: $71 of CI (AWS gross minus the $53/day the product costs) 2026-10-27: $150 of CI (AWS gross minus the $53/day the product costs) 2026-10-28: $155 of CI (AWS gross minus the $53/day the product costs) 2026-10-29: $40 of CI (AWS gross minus the $53/day the product costs) Oct 29
CI dollars per day = AWS gross minus the $53/day the product costs with no CI on AWS (Sep 22–28 median). GitHub-hosted weekdays cost $738 (Sep 22–26, from the Oct 4 cost study). The spike is the first week on AWS and the merge queue; the thin queue (Oct 4) and the deferral of tests (Oct 17) brought it down.
Week ofCI $/dayMerged PRs/day$ per merged PR
Sep 22 (GitHub-hosted)738 (weekday)261~2.8
Sep 283483980.87
Oct 51552670.58
Oct 12922010.46
Oct 19611260.48
Oct 26 (4 days)1042040.51

Where the machine minutes go on a day like Oct 28 (1,495 CI runs): PR checks and tests about half, the merge queue's per-PR re-test about a third, main-branch runs and the 2-hourly sweep the rest; scheduled jobs add ~1,640 machine-minutes. The queue cut (#2) saves $20–26 a day, tests-once (#3) $9–18, the cleanup (#4) $5–7: together $35–50 a day, from ~$75 to under the $65 target. (Dollar deltas are shares of the AWS bill, ±30%.) Per-run price is already 6× below GitHub's; what is left is runs per PR, not price per minute.

Method and caveats what was measured and how
  • Live = the first production deploy whose commit is at or after the PR's merge commit on main (Vercel records the GitHub deployment at success; 32 of 32 sampled within 3 s). Same definition as the factory app.
  • Weeks are by merge date, Monday start. GitHub-hosted runners until Sep 28; our machines Sep 29; merge queue Oct 1; release train Oct 4; deploy window back to 30 min on Oct 11, 9:25 PM CT (repo variable PROMOTE_WINDOW_MINUTES; the script default and the runbook still say 5); tests deferred to arming Oct 17.
  • The 119-minute breakdown uses the timelines of 3,259 PRs (arm, queue, merge events) joined to deploy runs; minute budgets inside jobs come from ~60 CI runs, 25 queue runs, 15 sweeps and 15 deploys (Oct 19–30). Medians with those sample sizes.
  • Dollars per move are the AWS bill split by machine-time share (±30%), not metered readings. The review bot's token cost is not in them. The "CI on AWS" line is EC2, EC2-other, CloudWatch and VPC above the Sep 22–28 baseline.
  • Review rounds and pushes move together in the data, so they are confounded. Red-sweep freezes affect the worst 1% of PRs, not the median.
  • Pulls: every merged PR (per day, to avoid the 1,000-row cap), 48,478 CI runs, 3,712 sweep runs, 3,113 production deployments, daily AWS Cost Explorer. Raw files and scripts stay in the session scratchpad; a memory note records the recipe.
  • The factory app (factory.propflowai.co) sits behind a login wall and its database refused the session's tokens; nothing here was read from it.
Week ofPRsOpen→merge med / p90Merge→live med / p90Open→live med / p90Live ≤ 60 minCI runs per PRPRs with a red run
Sep 141,18446 / 42813 / 2064 / 46048%340%
Sep 211,82434 / 27322 / 6673 / 31139%235%
Sep 282,78597 / 69251 / 97155 / 7628%224%
Oct 51,86768 / 34925 / 87111 / 44321%231%
Oct 121,40752 / 22860 / 80115 / 2914%229%
Oct 1988163 / 23452 / 77119 / 2984%337%
Oct 2681867 / 30454 / 77125 / 3572%338%

CI runs per PR is 3 since Oct 17 because arming now starts a second run by design.

PropFlow Docs