You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Following #4461, I measured every lane that runs on a pull request, not just test. This is a survey with numbers attached, not a proposal — each item below states what it would cost us in coverage, and whether it buys queue time, runner slots, or minutes inside one slot. Those are three different budgets and only one of them turned out to be the binding constraint.
Headline: the binding constraint is not how much work we run. It is which runner label we ask for.
Method
gh api repos/apache/maka/actions/... over every run in a complete ~19h snapshot (1000 runs, 1756 jobs, 2026-08-31T19:37Z → 2026-09-01T14:32Z), plus per-workflow histories reaching back to 2026-08-13.
Queue time is job.started_at - job.created_at, restricted to jobs that concluded success/failure (cancelled jobs report unusable timestamps).
Path-filter and planner hit rates are replays of planTests() and of each workflow's paths: globs over the last 300 --first-parent commits.
Nothing was dispatched. No repo-wide build or test suite was run locally.
Where a claim is inferred from reading code rather than measured, it says so.
The slot budget, measured
19h snapshot, summing completed_at - started_at across all 1756 jobs:
runner class
slot-minutes
share
median queue (UTC 07–14)
ubuntu-latest
3306
45%
15.8–33.4 min
windows-*
2895
40%
0.1–0.3 min
ubuntu-24.04* (pinned)
796
11%
0.3 min
macos-*
331
5%
0.2 min
40% of everything we spend is on Windows runners that we never wait for. Cutting Windows work saves the ASF bill; it does not save a single minute of anyone's wall clock.
1. ubuntu-latest is the entire queue problem. Nothing else queues. — queue time
Median queue by UTC hour, first-layer jobs only (no needs:), same repo, same snapshot:
UTC hour
ubuntu-latest
ubuntu-24.04*
windows-*
macos-*
00–06
0.0–0.1
0.0–0.1
0.0–0.1
0.1
07
4.3
0.4
0.1
0.1
08
18.6
0.8
0.2
0.6
09
19.7
0.3
0.1
0.1
10
8.3
0.1
0.1
0.2
11
13.1
0.7
0.6
0.6
12
22.0
1.5
0.1
0.8
13
22.6
0.2
0.1
0.2
14
18.3
0.2
0.1
0.2
Controls, in order of strength:
Same run, same instant, three OSes.gitoxide-helper-admission fans out to ubuntu-latest / macos-latest / windows-latest from one run, so all three jobs share a created_at. Median queue over 8 runs: 15.0 / 0.1 / 0.1 min. In 4 of 8 runs ubuntu-latest was more than 5 minutes slower than both siblings. macos-latest and windows-latest are -latest aliases too, so this is not about the alias.
Paired ±15-minute windows, cross-workflow. 426 pairings of an ubuntu-latest first-layer job against ubuntu-24.04* first-layer jobs created within 15 minutes: median 11.4 vs 0.2 min; ubuntu-latest slower by >1 min in 285/426, faster in 22.
Single-job control, congested window only.dependency-audit is one ubuntu-latest job with no needs:. UTC 07–14, run.created_at → job.started_at: 15.8 min. cli-package-validation's first-layer Build direct-peer addon on pinned labels, same window: 0.3 min.
Not a concurrency pending artifact. Median run.run_started_at - run.created_at is 0.0 for every workflow measured, cli-package-validation included. The clock we are reading is runner allocation, not queueing behind our own concurrency groups.
Not a self-hosted split.runner_group_name is GitHub Actions for both classes.
What this means. Every controllable confounder is excluded, and the residual explanation that fits is that ASF-wide demand saturates the shared ubuntu-latest pool during European/American working hours while pinned images sit idle. The observed 18-queued-1-running snapshot and the 37-minute wait on run 33511334930 both land inside 07–14 UTC.
Not verified: that changing the label actually lands us in the idle pool. That is a causal claim and the data above is correlational, however tightly controlled. Cheapest way to settle it: pin one lane — dependency-audit, one job, p99 0.9 min, 22% of PRs — from ubuntu-latest to ubuntu-24.04 and read its queue distribution for a week. Zero coverage risk, one line.
Gives up: automatic OS image upgrades. A pinned label has to be bumped by hand when GitHub moves latest, and going stale is a real (if slow) maintenance cost. Worth noting cli-package-validation already pins every one of its runners, so the repo has both conventions today.
Budget: queue time only. It removes zero work and zero slots. If it holds, it is worth more than every other item on this list combined — CI is one job now, so it is ~15 min of wall clock per PR push during working hours.
2. native/, patches/, .asf.yaml, deny.toml fall through the planner into --full — slots + minutes
full fires on 57/300 commits (19.0%). Attribution:
commits
trigger
22
unknownCode fallback — no FULL_SUITE_FILES entry involved
16
package.json
14
.github/workflows/ci.yml
11
package-lock.json
11
scripts/ci-test-plan.mjs
1
scripts/run-workspace-tests-parallel.mjs
(rows overlap; a commit can hit several of the named files)
The single largest source of full-suite runs is not package-lock.json. It is the classifier loop in planTests() reaching code = true; unknownCode = true and recursing with forceFull: true. Broken out by bucket:
7.3% of commits run the entire suite because the planner has no opinion about them.patches/ is the sharpest case: isCliPackagePath() and isAsfSourcePath() both handle it explicitly, but the main loop never gets there, so the --full return short-circuits both. .asf.yaml is a branch-protection config file with no code reachability at all. And native/** already has three dedicated lanes (runtime-host-peer-admission, gitoxide-helper-admission, the Rust build inside cli-package-validation) — a Rust change currently pays for those and for appIcons, asfSource, storybook, e2e, and all 10 workspaces.
Gives up: for whatever gets classified, the blanket guarantee that an unrecognized path is maximally validated. That fail-safe should stay for genuinely unknown paths — the fix is to classify these four, not to weaken the fallback.
Flagging this as the one item that is very small and very valuable. Someone should decide whether it warrants its own PR. Related to #4473 (same file, same authority), so it may belong there.
3. cli-package-validation has a concurrency group but no cancel-in-progress — slots
Measured over the 19h window: it holds more runner slots than any other workflow — 736 jobs against CI's 523 — from only 60 PR runs (it is second by slot-minutes: 1672 to CI's 3126, because its jobs are short and numerous), because it fans out to 13.6 jobs per run (4-way peer-native matrix → build → 4-way smoke matrix → state-root-qualification → eval).
14 of 60 runs (23%) were still executing when a newer push landed on the same branch.
275 of 1672 slot-minutes (16%) were spent on work that was already obsolete.
Only 5 runs were cancelled, versus 76 for CI, which does set cancel-in-progress.
Without cancel-in-progress, a superseded run is not cancelled; it runs to completion while the newer one queues behind it in the same group.
Gives up: nothing on pull requests. workflow_call from the release path and workflow_dispatch land on different refs and would need to keep the current behaviour, exactly the way windows-sandbox-w0.yml already scopes its group (${{ github.head_ref || github.run_id }}).
Budget: pure slots (mostly pinned-Linux/Windows/macOS, so no queue relief). ~275 slot-minutes per 19h at current PR volume.
4. gitoxide-helper-admission is triggered by a manifest that cannot affect it — slots
Path-filter replay over 300 commits: 16 hits, and the top driver is packages/runtime/package.json at 12 of 16 (75%).
Inspecting what actually changes in workspace manifests over those 300 commits: "version" (41 occurrences), "test:dist" (36), "test" (35), "pretest" (27), "clean" (27), "typecheck" (18), "build" (16). Version bumps and npm script edits. Each one currently spends three runners (ubuntu-latest + macos-latest + windows-latest, ~1.5 min each) building and testing a Rust helper whose inputs did not move — and the ubuntu-latest one of those three is the job that produced the 15.0-minute median queue in §1.
The same shape, less severe, in runtime-host-owner-platform.yml: 29 hits, of which package.json drives 12 and package-lock.json 7 — 19 of 29 from root manifests rather than from the owner-platform sources the lane exists to check.
Gives up: coverage of the case where a dependency range in packages/runtime/package.json changes what the helper links against. Narrowing to packages/runtime/package.jsonand an actual dependency-section diff, or dropping the manifest and keeping native/gitoxide-helper/** + Cargo.lock, would need someone who knows whether that link is real. Unverified either way — I did not trace the build.
5. Release Windows check: the download failures were already fixed in #4461, and my sample was skewed — corrected
This item was wrong when first filed. Keeping it visible rather than deleting it.
The lane is real and expensive: 20.5 min median, p95 25.3, 1711 slot-minutes / 19h, 129 PR runs (36% of PR branches, 4.2 runs per branch). But the diagnosis was not.
What I originally reported: 14 of 20 sampled failures were Download and verify the pinned Windows upgrade baseline, and ci.yml retries its equivalent download while this lane does not.
Both halves are wrong:
It is already fixed.ci: allocate a runner only when its inputs changed #4461 added a Restore the pinned Windows upgrade baseline cache step keyed on hashFiles('scripts/windows-upgrade-baseline.json'), immediately before the download. Its comment records the same observation from a wider window than mine: "14 of 36 across 300 runs, always on branches that could not have caused it."
My sample was skewed. All 60 sampled runs came from the 19h snapshot, i.e. 2026-09-01, which was the day the baseline fetch broke at scale. Failures by day, pull_request only:
day
failures
dominant failing step (sampled)
08-24
5
08-25
9
08-26
12
Package the Windows installer and ZIP (9/12)
08-27
3
08-28
7
08-29
9
Package the Windows installer and ZIP (7/9)
08-30
4
08-31
10
09-01
36
Download and verify the pinned Windows upgrade baseline (11/12)
Across nine days the failures are mixed, and outside 09-01 the dominant step is a different one entirely.
What survives: Package the Windows installer and ZIP is the other repeat offender — 9 failures on 08-26 and 7 on 08-29, on a 20-minute lane. I could not determine the cause; job logs from those days have aged out of retention. Unverified, flagged for whoever can catch it live. If it is an electron-builder artifact download, it has the same shape as the baseline fetch and the same fix applies.
6. The CLI tarball is packed and smoke-tested twice on the same commit — slots + minutes
ci.yml runs 8 steps behind cli_package == 'true', ending in release:cli:pack (45s) + release:cli:smoke (81s). Planner hit rate: 70.0% of commits.
cli-package-validation.yml builds the addon on 4 platforms, packs an immutable tarball, and runs the same scripts/smoke-release-cli-package.mjs across 4 runners × 2 Node versions. Path-filter hit rate: 21.3%.
Replayed together over 300 commits: cli-package-validation fires on 64, and all 64 also run CI's cli_package steps. Zero the other way. Its trigger set is a strict subset. On those 21.3% of commits we pack and install-smoke a CLI tarball on Linux x64 twice.
Gives up: if CI's steps were gated on "and cli-package-validation is not also running", the required test check would stop being the CLI packaging gate on 21.3% of commits, and cli-package-validation is not in .asf.yaml's required_status_checks.contexts. That trade is bad as stated — the merge gate must not get weaker. Any fix here has to move the contexts, not just the steps, which is a bigger conversation than a CI-cost issue should start.
Recording it as measured duplication, not as a recommendation.
6b. Two of the three State Root transitions cannot change on a pull request
cli-package-validation's state-root-qualification job (155 slot-minutes / 19h, runs unconditionally on all 64 triggers) qualifies three transitions. Its own comment says it:
Two of these transitions are between tarballs that were published and frozen, so nothing in a pull request can change their outcome except the qualifier itself
Measured: the qualifier and its fixture (scripts/qualify-released-cli-state-root.mjs, released-cli-state-root-fixture.mjs, and the test) change on 3 of 300 commits (1.0%). So ~⅔ of that job's work has a PR-controllable input on 1% of commits and runs on 21.3%.
ci.yml independently qualifies durable state against the published baseline (33s) on state_root_compat, which fires on 28.7% of commits — overlapping cli-package-validation on 14.3%.
Gives up: the published→published transitions would stop being re-proven on every unrelated CLI change. They would still run on the 1.0% that can move them, on main, and on the release path. The risk is drift in something outside the repo (a republished tarball) going unnoticed longer — the nightly lane is the natural place to keep catching that.
7. The planner's high-hit surfaces are high for a structural reason
Replay over 300 first-parent commits (reproduces the numbers in #4461's discussion):
surface
hit rate
code
94.7%
releaseContract
71.7%
cliPackage
70.0%
runtimeSandbox
69.0%
runtimeHost
62.7%
e2e
57.7%
storybook
52.0%
astryxSurface
48.0%
stateRootCompat
28.7%
appIcons
21.3%
asfSource
19.7%
full
19.0%
storageStress
5.7%
These three are not independently wide — they are one predicate wearing three hats:
isCliPackagePath() matches any path under 7 of the 10 workspaces (cli, core, eval, mcp, runtime, runtime-host, storage). It is effectively "did you touch the backend", which is why it reads 70%.
releaseContract = cliPackage || isReleaseContractPath(...) is a strict superset by construction: 71.7% vs 70.0% means the release-contract file list adds 1.7 points over "backend changed".
runtimeSandbox = workspaces.includes('packages/cli'), and packages/cli is the top of the reverse-dependency closure, so it is reachable from core, storage, mcp, runtime, runtime-host, and eval. 69% is the closure's shape, not a predicate that can be tightened in place.
No recommendation attached. Narrowing any of these means deciding which backend workspaces genuinely cannot affect CLI packaging, and I have no evidence for that — it is a design question, not a measurement one. Recording the structure so nobody spends time "tuning" a predicate whose width comes from somewhere else.
8. The dependency closure sends 36% of commits to all 10 workspaces — minutes
Workspace-count histogram over 300 commits (avg 5.36 of 10 selected):
workspaces selected
commits
0
32
1
55
2
23
3
2
4
41
5
2
6
15
7
21
10
109
109/300 (36.3%) select everything. Only 57 of those are full. The other 52 (17.3%) get there purely through reverseDependencyClosure, and every one of them touches packages/core:
57 commits touch packages/core at all; the 52 non-full ones select 10.00 workspaces on average — no exceptions. Top files: package.json (11), events.ts (10), session.ts (6), foreign-session.ts (5), agent-run.ts (5).
The closure is correct — core really is imported everywhere. The question is whether it needs to be manifest-level. A change to packages/core/src/model-catalog.ts pulls packages/computer-use into the run because computer-use depends on @maka/core, not because it imports that module.
Gives up: a module-level import graph would be a real authority to build and keep honest, and it can be wrong in a way a manifest graph cannot (dynamic imports, re-exports, type-only edges). This is the most expensive item on the list to implement and the one I am least confident about. Same family as #4478, which does exactly this reasoning for e2e specs — if that lands and the mechanism generalizes, this is the follow-on.
9. dependency-audit runs on PRs that change no dependency — slots
paths: includes apps/*/package.json and packages/*/package.json. Replay: 50/300 commits hit, and 37 of those come from workspace manifests versus 23 from root package.json/package-lock.json. Per §4, workspace manifest diffs are dominated by "version" and npm scripts edits.
The job is genuinely cheap — 0.7 min median, p99 0.9 min — but it is ubuntu-latest, so during 07–14 UTC it occupies a congested-pool slot for a measured median of 15.8 minutes of queue to do 42 seconds of work. It also runs nightly at 03:17 UTC, in the idle window, where it has been green 24/25.
Gives up: the PR-time signal that a dependency change introduces a disallowed license or an unshipped transitive. Given the nightly lane, the loss is detection latency (hours), not detection. Alternatively keep the trigger and just pin the label per §1, which costs nothing at all.
10. timeout-minutes: 45 is uniform and nothing comes close to it — slots, preventive
Every job in every workflow uses 45. Measured durations:
workflow
p50
p95
p99
max
headroom
Dependency audit
0.7
0.8
0.9
0.9
50×
Runtime Host owner platforms
1.1
1.7
1.9
1.9
24×
Runtime Host peer admission
1.5
2.2
2.2
2.2
20×
Gitoxide helper admission
1.5
2.7
3.0
3.0
15×
Windows recovery
2.8
3.7
4.4
4.5
10×
CLI package validation
1.7
6.1
7.6
11.5
4×
Release Windows check
20.5
25.3
29.9
29.9
1.5×
CI
0.3
25.5
26.7
30.9
1.5×
CI and Release Windows check are sized correctly. A hung dependency-audit holds a congested-pool slot for 45 minutes instead of 3.
I observed no hung job in this window, so this is preventive, and it is last on the list for that reason. It also has a real downside: a too-tight timeout turns a slow runner into a red required check.
Places I looked and found nothing to cut
windows-recovery — 57% of PR branches, which looked high, but 170 runs cost only 452 slot-minutes (2.8 min median) on the uncongested Windows pool, and its seven steps (NTFS alternate data streams, root-initialization replacement race, Local IPC trust boundary, SQLite crash recovery, Runtime continuation recovery, owner-death recovery) are things no Linux lane can prove. 176 green / 6 red. Cheap, in the right pool, and the only evidence for what it checks. Leave it.
concurrency groups — correct everywhere except §3. CI, release-windows-check, windows-recovery, runtime-host-owner-platform, runtime-host-peer-admission, gitoxide-helper-admission, dependency-audit all set cancel-in-progress, and windows-sandbox-w0 correctly gives scheduled and manual runs their own group so they cannot discard each other.
Cron schedules — six of them, all offset off the hour, all in the 03:00–07:30 or 18:00 UTC idle window, none overlapping. windows-baseline 22/22 green, windows-sandbox-w0 12/12, dependency-audit 24/25. Green cron is not automatically waste, and these are cheap and out of the way. No change.
windows-baseline on PRs — it did run on 675 pull requests between 2026-08-16 and 2026-08-20, and was removed on 08-20. Already fixed; noting it so nobody re-discovers it in the run history.
take.yml — 578 runs on issue_comment, but it uses ubuntu-slim, medians 3s queue / 5s duration, and 50 of 53 sampled jobs skip immediately. Negligible, and it is already the only lane using the cheap label.
CI matrix dimensions — CI is one job with no matrix after ci: allocate a runner only when its inputs changed #4461. cli-package-validation's two 4-way matrices map to four genuinely different build targets and four different install environments; no dimension is redundant.
Two hit-rate methodologies, and they disagree for one lane. Squash-replay over merged commits gives windows-recovery 21.0%; the live snapshot gives 57% of PR branches. Squash-replay measures the final diff of merged work; the snapshot measures currently-open branches, where a long-lived PR that once touched a wide path keeps re-triggering that lane on every subsequent push. Both are in this issue and each item says which one it uses. cli-package-validation agrees across both (21.3% vs 21%), which is a decent check that the replay itself is sound.
The 19h snapshot is one weekday. The hourly table is consistent across it, but a single day cannot rule out that 2026-09-01 was unusually busy ASF-wide.
Following #4461, I measured every lane that runs on a pull request, not just
test. This is a survey with numbers attached, not a proposal — each item below states what it would cost us in coverage, and whether it buys queue time, runner slots, or minutes inside one slot. Those are three different budgets and only one of them turned out to be the binding constraint.Headline: the binding constraint is not how much work we run. It is which runner label we ask for.
Method
gh api repos/apache/maka/actions/...over every run in a complete ~19h snapshot (1000 runs, 1756 jobs, 2026-08-31T19:37Z → 2026-09-01T14:32Z), plus per-workflow histories reaching back to 2026-08-13.job.started_at - job.created_at, restricted to jobs that concludedsuccess/failure(cancelled jobs report unusable timestamps).planTests()and of each workflow'spaths:globs over the last 300--first-parentcommits.Where a claim is inferred from reading code rather than measured, it says so.
The slot budget, measured
19h snapshot, summing
completed_at - started_atacross all 1756 jobs:ubuntu-latestwindows-*ubuntu-24.04*(pinned)macos-*40% of everything we spend is on Windows runners that we never wait for. Cutting Windows work saves the ASF bill; it does not save a single minute of anyone's wall clock.
1.
ubuntu-latestis the entire queue problem. Nothing else queues. — queue timeMedian queue by UTC hour, first-layer jobs only (no
needs:), same repo, same snapshot:ubuntu-latestubuntu-24.04*windows-*macos-*Controls, in order of strength:
gitoxide-helper-admissionfans out toubuntu-latest/macos-latest/windows-latestfrom one run, so all three jobs share acreated_at. Median queue over 8 runs: 15.0 / 0.1 / 0.1 min. In 4 of 8 runsubuntu-latestwas more than 5 minutes slower than both siblings.macos-latestandwindows-latestare-latestaliases too, so this is not about the alias.ubuntu-latestfirst-layer job againstubuntu-24.04*first-layer jobs created within 15 minutes: median 11.4 vs 0.2 min;ubuntu-latestslower by >1 min in 285/426, faster in 22.dependency-auditis oneubuntu-latestjob with noneeds:. UTC 07–14,run.created_at → job.started_at: 15.8 min.cli-package-validation's first-layerBuild direct-peer addonon pinned labels, same window: 0.3 min.concurrencypending artifact. Medianrun.run_started_at - run.created_atis 0.0 for every workflow measured,cli-package-validationincluded. The clock we are reading is runner allocation, not queueing behind our ownconcurrencygroups.runner_group_nameisGitHub Actionsfor both classes.What this means. Every controllable confounder is excluded, and the residual explanation that fits is that ASF-wide demand saturates the shared
ubuntu-latestpool during European/American working hours while pinned images sit idle. The observed 18-queued-1-running snapshot and the 37-minute wait on run33511334930both land inside 07–14 UTC.Not verified: that changing the label actually lands us in the idle pool. That is a causal claim and the data above is correlational, however tightly controlled. Cheapest way to settle it: pin one lane —
dependency-audit, one job,p990.9 min, 22% of PRs — fromubuntu-latesttoubuntu-24.04and read its queue distribution for a week. Zero coverage risk, one line.Gives up: automatic OS image upgrades. A pinned label has to be bumped by hand when GitHub moves
latest, and going stale is a real (if slow) maintenance cost. Worth notingcli-package-validationalready pins every one of its runners, so the repo has both conventions today.Budget: queue time only. It removes zero work and zero slots. If it holds, it is worth more than every other item on this list combined — CI is one job now, so it is ~15 min of wall clock per PR push during working hours.
2.
native/,patches/,.asf.yaml,deny.tomlfall through the planner into--full— slots + minutesfullfires on 57/300 commits (19.0%). Attribution:unknownCodefallback — noFULL_SUITE_FILESentry involvedpackage.json.github/workflows/ci.ymlpackage-lock.jsonscripts/ci-test-plan.mjsscripts/run-workspace-tests-parallel.mjs(rows overlap; a commit can hit several of the named files)
The single largest source of full-suite runs is not
package-lock.json. It is the classifier loop inplanTests()reachingcode = true; unknownCode = trueand recursing withforceFull: true. Broken out by bucket:native/**—runtime-host-peer/src/engine.rs(12),bindings.rs(11),gitoxide-helper/**patches/**.asf.yamldeny.toml7.3% of commits run the entire suite because the planner has no opinion about them.
patches/is the sharpest case:isCliPackagePath()andisAsfSourcePath()both handle it explicitly, but the main loop never gets there, so the--fullreturn short-circuits both..asf.yamlis a branch-protection config file with no code reachability at all. Andnative/**already has three dedicated lanes (runtime-host-peer-admission,gitoxide-helper-admission, the Rust build insidecli-package-validation) — a Rust change currently pays for those and forappIcons,asfSource,storybook, e2e, and all 10 workspaces.Gives up: for whatever gets classified, the blanket guarantee that an unrecognized path is maximally validated. That fail-safe should stay for genuinely unknown paths — the fix is to classify these four, not to weaken the fallback.
Flagging this as the one item that is very small and very valuable. Someone should decide whether it warrants its own PR. Related to #4473 (same file, same authority), so it may belong there.
3.
cli-package-validationhas aconcurrencygroup but nocancel-in-progress— slotsMeasured over the 19h window: it holds more runner slots than any other workflow — 736 jobs against CI's 523 — from only 60 PR runs (it is second by slot-minutes: 1672 to CI's 3126, because its jobs are short and numerous), because it fans out to 13.6 jobs per run (4-way
peer-nativematrix →build→ 4-waysmokematrix →state-root-qualification→eval).cancel-in-progress.Without
cancel-in-progress, a superseded run is not cancelled; it runs to completion while the newer one queues behind it in the same group.Gives up: nothing on pull requests.
workflow_callfrom the release path andworkflow_dispatchland on different refs and would need to keep the current behaviour, exactly the waywindows-sandbox-w0.ymlalready scopes its group (${{ github.head_ref || github.run_id }}).Budget: pure slots (mostly pinned-Linux/Windows/macOS, so no queue relief). ~275 slot-minutes per 19h at current PR volume.
4.
gitoxide-helper-admissionis triggered by a manifest that cannot affect it — slotsPath-filter replay over 300 commits: 16 hits, and the top driver is
packages/runtime/package.jsonat 12 of 16 (75%).Inspecting what actually changes in workspace manifests over those 300 commits:
"version"(41 occurrences),"test:dist"(36),"test"(35),"pretest"(27),"clean"(27),"typecheck"(18),"build"(16). Version bumps and npm script edits. Each one currently spends three runners (ubuntu-latest+macos-latest+windows-latest, ~1.5 min each) building and testing a Rust helper whose inputs did not move — and theubuntu-latestone of those three is the job that produced the 15.0-minute median queue in §1.The same shape, less severe, in
runtime-host-owner-platform.yml: 29 hits, of whichpackage.jsondrives 12 andpackage-lock.json7 — 19 of 29 from root manifests rather than from the owner-platform sources the lane exists to check.Gives up: coverage of the case where a dependency range in
packages/runtime/package.jsonchanges what the helper links against. Narrowing topackages/runtime/package.jsonand an actual dependency-section diff, or dropping the manifest and keepingnative/gitoxide-helper/**+Cargo.lock, would need someone who knows whether that link is real. Unverified either way — I did not trace the build.5.
Release Windows check: the download failures were already fixed in #4461, and my sample was skewed — correctedThis item was wrong when first filed. Keeping it visible rather than deleting it.
The lane is real and expensive: 20.5 min median, p95 25.3, 1711 slot-minutes / 19h, 129 PR runs (36% of PR branches, 4.2 runs per branch). But the diagnosis was not.
What I originally reported: 14 of 20 sampled failures were
Download and verify the pinned Windows upgrade baseline, andci.ymlretries its equivalent download while this lane does not.Both halves are wrong:
Restore the pinned Windows upgrade baselinecache step keyed onhashFiles('scripts/windows-upgrade-baseline.json'), immediately before the download. Its comment records the same observation from a wider window than mine: "14 of 36 across 300 runs, always on branches that could not have caused it."pull_requestonly:Package the Windows installer and ZIP(9/12)Package the Windows installer and ZIP(7/9)Download and verify the pinned Windows upgrade baseline(11/12)Across nine days the failures are mixed, and outside 09-01 the dominant step is a different one entirely.
What survives:
Package the Windows installer and ZIPis the other repeat offender — 9 failures on 08-26 and 7 on 08-29, on a 20-minute lane. I could not determine the cause; job logs from those days have aged out of retention. Unverified, flagged for whoever can catch it live. If it is anelectron-builderartifact download, it has the same shape as the baseline fetch and the same fix applies.Same lane as #4470.
6. The CLI tarball is packed and smoke-tested twice on the same commit — slots + minutes
ci.ymlruns 8 steps behindcli_package == 'true', ending inrelease:cli:pack(45s) +release:cli:smoke(81s). Planner hit rate: 70.0% of commits.cli-package-validation.ymlbuilds the addon on 4 platforms, packs an immutable tarball, and runs the samescripts/smoke-release-cli-package.mjsacross 4 runners × 2 Node versions. Path-filter hit rate: 21.3%.Replayed together over 300 commits:
cli-package-validationfires on 64, and all 64 also run CI'scli_packagesteps. Zero the other way. Its trigger set is a strict subset. On those 21.3% of commits we pack and install-smoke a CLI tarball on Linux x64 twice.Gives up: if CI's steps were gated on "and
cli-package-validationis not also running", the requiredtestcheck would stop being the CLI packaging gate on 21.3% of commits, andcli-package-validationis not in.asf.yaml'srequired_status_checks.contexts. That trade is bad as stated — the merge gate must not get weaker. Any fix here has to move the contexts, not just the steps, which is a bigger conversation than a CI-cost issue should start.Recording it as measured duplication, not as a recommendation.
6b. Two of the three State Root transitions cannot change on a pull request
cli-package-validation'sstate-root-qualificationjob (155 slot-minutes / 19h, runs unconditionally on all 64 triggers) qualifies three transitions. Its own comment says it:Measured: the qualifier and its fixture (
scripts/qualify-released-cli-state-root.mjs,released-cli-state-root-fixture.mjs, and the test) change on 3 of 300 commits (1.0%). So ~⅔ of that job's work has a PR-controllable input on 1% of commits and runs on 21.3%.ci.ymlindependently qualifies durable state against the published baseline (33s) onstate_root_compat, which fires on 28.7% of commits — overlappingcli-package-validationon 14.3%.Gives up: the published→published transitions would stop being re-proven on every unrelated CLI change. They would still run on the 1.0% that can move them, on
main, and on the release path. The risk is drift in something outside the repo (a republished tarball) going unnoticed longer — the nightly lane is the natural place to keep catching that.7. The planner's high-hit surfaces are high for a structural reason
Replay over 300 first-parent commits (reproduces the numbers in #4461's discussion):
codereleaseContractcliPackageruntimeSandboxruntimeHoste2estorybookastryxSurfacestateRootCompatappIconsasfSourcefullstorageStressThese three are not independently wide — they are one predicate wearing three hats:
isCliPackagePath()matches any path under 7 of the 10 workspaces (cli,core,eval,mcp,runtime,runtime-host,storage). It is effectively "did you touch the backend", which is why it reads 70%.releaseContract = cliPackage || isReleaseContractPath(...)is a strict superset by construction: 71.7% vs 70.0% means the release-contract file list adds 1.7 points over "backend changed".runtimeSandbox = workspaces.includes('packages/cli'), andpackages/cliis the top of the reverse-dependency closure, so it is reachable fromcore,storage,mcp,runtime,runtime-host, andeval. 69% is the closure's shape, not a predicate that can be tightened in place.No recommendation attached. Narrowing any of these means deciding which backend workspaces genuinely cannot affect CLI packaging, and I have no evidence for that — it is a design question, not a measurement one. Recording the structure so nobody spends time "tuning" a predicate whose width comes from somewhere else.
8. The dependency closure sends 36% of commits to all 10 workspaces — minutes
Workspace-count histogram over 300 commits (avg 5.36 of 10 selected):
109/300 (36.3%) select everything. Only 57 of those are
full. The other 52 (17.3%) get there purely throughreverseDependencyClosure, and every one of them touchespackages/core:57 commits touch
packages/coreat all; the 52 non-fullones select 10.00 workspaces on average — no exceptions. Top files:package.json(11),events.ts(10),session.ts(6),foreign-session.ts(5),agent-run.ts(5).The closure is correct —
corereally is imported everywhere. The question is whether it needs to be manifest-level. A change topackages/core/src/model-catalog.tspullspackages/computer-useinto the run becausecomputer-usedepends on@maka/core, not because it imports that module.Gives up: a module-level import graph would be a real authority to build and keep honest, and it can be wrong in a way a manifest graph cannot (dynamic imports, re-exports, type-only edges). This is the most expensive item on the list to implement and the one I am least confident about. Same family as #4478, which does exactly this reasoning for e2e specs — if that lands and the mechanism generalizes, this is the follow-on.
9.
dependency-auditruns on PRs that change no dependency — slotspaths:includesapps/*/package.jsonandpackages/*/package.json. Replay: 50/300 commits hit, and 37 of those come from workspace manifests versus 23 from rootpackage.json/package-lock.json. Per §4, workspace manifest diffs are dominated by"version"and npmscriptsedits.The job is genuinely cheap — 0.7 min median, p99 0.9 min — but it is
ubuntu-latest, so during 07–14 UTC it occupies a congested-pool slot for a measured median of 15.8 minutes of queue to do 42 seconds of work. It also runs nightly at 03:17 UTC, in the idle window, where it has been green 24/25.Gives up: the PR-time signal that a dependency change introduces a disallowed license or an unshipped transitive. Given the nightly lane, the loss is detection latency (hours), not detection. Alternatively keep the trigger and just pin the label per §1, which costs nothing at all.
10.
timeout-minutes: 45is uniform and nothing comes close to it — slots, preventiveEvery job in every workflow uses 45. Measured durations:
CI and Release Windows check are sized correctly. A hung
dependency-auditholds a congested-pool slot for 45 minutes instead of 3.I observed no hung job in this window, so this is preventive, and it is last on the list for that reason. It also has a real downside: a too-tight timeout turns a slow runner into a red required check.
Places I looked and found nothing to cut
windows-recovery— 57% of PR branches, which looked high, but 170 runs cost only 452 slot-minutes (2.8 min median) on the uncongested Windows pool, and its seven steps (NTFS alternate data streams, root-initialization replacement race, Local IPC trust boundary, SQLite crash recovery, Runtime continuation recovery, owner-death recovery) are things no Linux lane can prove. 176 green / 6 red. Cheap, in the right pool, and the only evidence for what it checks. Leave it.concurrencygroups — correct everywhere except §3. CI,release-windows-check,windows-recovery,runtime-host-owner-platform,runtime-host-peer-admission,gitoxide-helper-admission,dependency-auditall setcancel-in-progress, andwindows-sandbox-w0correctly gives scheduled and manual runs their own group so they cannot discard each other.windows-baseline22/22 green,windows-sandbox-w012/12,dependency-audit24/25. Green cron is not automatically waste, and these are cheap and out of the way. No change.windows-baselineon PRs — it did run on 675 pull requests between 2026-08-16 and 2026-08-20, and was removed on 08-20. Already fixed; noting it so nobody re-discovers it in the run history.take.yml— 578 runs onissue_comment, but it usesubuntu-slim, medians 3s queue / 5s duration, and 50 of 53 sampled jobs skip immediately. Negligible, and it is already the only lane using the cheap label.npm ci17s,Build19s,Typecheck9s on a 1528s job. 2.9%. Nothing there.cli-package-validation's two 4-way matrices map to four genuinely different build targets and four different install environments; no dimension is redundant.Measurement caveats
plan→heavy→testshape, which queued three times per PR (measured example, run33490755623: 81.5 min wall clock, 55.1 min of it queue,planrunning 22s andtestrunning 4s). That is exactly what ci: allocate a runner only when its inputs changed #4461 fixed, and the numbers in §1 are the reason the fix mattered more than the job count suggested. Step timings in this issue come from run33511334930, which is the post-ci: allocate a runner only when its inputs changed #4461 single-job shape: 37 min queue, 25.5 min run.windows-recovery21.0%; the live snapshot gives 57% of PR branches. Squash-replay measures the final diff of merged work; the snapshot measures currently-open branches, where a long-lived PR that once touched a wide path keeps re-triggering that lane on every subsequent push. Both are in this issue and each item says which one it uses.cli-package-validationagrees across both (21.3% vs 21%), which is a decent check that the replay itself is sound.Cross-references
Package the Windows installer and ZIPfailures.Nothing in this issue would remove
testfrom.asf.yaml'srequired_status_checks.contexts, rename it, or put it behind apaths:filter.