Problem
Two lanes account for 93% of the non-Linux runner time a pull request costs. Measured over the 300 most recent first-parent commits on main (2026-08-20 .. 2026-08-26), replaying each lane's paths filter against each commit's diff:
| lane |
hit rate |
median |
expected cost per PR |
release-windows-check |
21% |
18.7 min |
179s |
windows_recovery |
54% |
3.3 min |
113s |
runtime-host-owner-platform (win+mac) |
10% |
1.0 min |
13s |
gitoxide-helper-admission (win+mac) |
4% |
1.6 min |
9s |
windows-sandbox-w0 |
3% |
2.8 min |
5s |
runtime-host-peer-admission |
0.3% |
3.6 min |
~0 |
The four narrow lanes are already close to free. The two expensive ones are wide because their paths lists name whole source trees rather than what the lane executes.
windows_recovery runs ten recovery test files. Their static import closure is 435 source files across packages/{core,storage,runtime,runtime-host}. Its filter currently admits all 1,347 files under those four src/** trees. Replayed against the same 300 commits, the current list matches 49% and the closure matches 33%.
release-windows-check has the same shape. It hangs on apps/desktop/src/main/*.ts and apps/desktop/package.json, both high-churn, while what it actually exercises is the installer, the update path, and the rollback path.
Desired outcome
Each lane's paths list is derived from the closure of what that lane runs rather than hand-maintained. scripts/ci-test-plan.mjs already owns the workspace graph and reverseDependencyClosure; these lists should be projections of it, locked by a contract test the way the postinstall launcher chain is in #3914.
Expected saving is roughly 100s of Windows and macOS runner time per pull request, about half an hour of scarce capacity per day at the current merge rate. That is a real but moderate win. The larger reason is drift: eight workflows each keep their own hand-copied path list, and a moved file silently drops a lane from the pull request with nothing to catch it.
A separate commit in the same pull request should set timeout-minutes: 45 across the pull-request lanes. Current limits are 120 for ci.yml against a 19.8-minute worst observed successful run and 90 for release-windows-check against 19.7 minutes, so a hung job holds a scarce runner slot far longer than any real run needs. 45 is roughly 2.5x the p95 of the slowest lane, which leaves the flake-retry and cold-cache headroom that a 30-minute limit would not.
Alternatives or workarounds
Leaving the lists hand-maintained keeps the drift risk, which is worse than the runner cost.
Static import analysis can miss a dynamic import or a runtime-only dependency, so a derived list is still a pre-filter, not proof of the lane's real input. That is already the standing position for windows_recovery: the unfiltered push: [main] run and the nightly run are what make a missed match recoverable, and both must stay unfiltered.
Per-lane timeout tiers were considered and rejected. The six cheap lanes currently sit at 30 minutes, but that value was inherited rather than chosen, and their combined hit rate is low enough that raising their worst case to 45 costs far less than the two expensive lanes save.
windows-sandbox-w0 was examined and needs no change. Its 3% hit rate reflects genuinely stable code, not a filter that is too narrow.
Claude Opus measured the hit rates, timing distribution, and import closure. A human contributor must verify the derived lists and own the change.
Problem
Two lanes account for 93% of the non-Linux runner time a pull request costs. Measured over the 300 most recent first-parent commits on
main(2026-08-20 .. 2026-08-26), replaying each lane'spathsfilter against each commit's diff:release-windows-checkwindows_recoveryruntime-host-owner-platform(win+mac)gitoxide-helper-admission(win+mac)windows-sandbox-w0runtime-host-peer-admissionThe four narrow lanes are already close to free. The two expensive ones are wide because their
pathslists name whole source trees rather than what the lane executes.windows_recoveryruns ten recovery test files. Their static import closure is 435 source files acrosspackages/{core,storage,runtime,runtime-host}. Its filter currently admits all 1,347 files under those foursrc/**trees. Replayed against the same 300 commits, the current list matches 49% and the closure matches 33%.release-windows-checkhas the same shape. It hangs onapps/desktop/src/main/*.tsandapps/desktop/package.json, both high-churn, while what it actually exercises is the installer, the update path, and the rollback path.Desired outcome
Each lane's
pathslist is derived from the closure of what that lane runs rather than hand-maintained.scripts/ci-test-plan.mjsalready owns the workspace graph andreverseDependencyClosure; these lists should be projections of it, locked by a contract test the way the postinstall launcher chain is in #3914.Expected saving is roughly 100s of Windows and macOS runner time per pull request, about half an hour of scarce capacity per day at the current merge rate. That is a real but moderate win. The larger reason is drift: eight workflows each keep their own hand-copied path list, and a moved file silently drops a lane from the pull request with nothing to catch it.
A separate commit in the same pull request should set
timeout-minutes: 45across the pull-request lanes. Current limits are 120 forci.ymlagainst a 19.8-minute worst observed successful run and 90 forrelease-windows-checkagainst 19.7 minutes, so a hung job holds a scarce runner slot far longer than any real run needs. 45 is roughly 2.5x the p95 of the slowest lane, which leaves the flake-retry and cold-cache headroom that a 30-minute limit would not.Alternatives or workarounds
Leaving the lists hand-maintained keeps the drift risk, which is worse than the runner cost.
Static import analysis can miss a dynamic import or a runtime-only dependency, so a derived list is still a pre-filter, not proof of the lane's real input. That is already the standing position for
windows_recovery: the unfilteredpush: [main]run and the nightly run are what make a missed match recoverable, and both must stay unfiltered.Per-lane timeout tiers were considered and rejected. The six cheap lanes currently sit at 30 minutes, but that value was inherited rather than chosen, and their combined hit rate is low enough that raising their worst case to 45 costs far less than the two expensive lanes save.
windows-sandbox-w0was examined and needs no change. Its 3% hit rate reflects genuinely stable code, not a filter that is too narrow.Claude Opus measured the hit rates, timing distribution, and import closure. A human contributor must verify the derived lists and own the change.