Summary
Every expensive CI job except the two scaffold tiers runs unconditionally on every pull request.
check-test (~7 min) and quality (~2 min) have no job condition at all, so an RFC or a
one-file documentation change pays the same toolchain bill as a change to packages/service.
The fix is not more workflow YAML. .github/scripts/ci-classify-changes.ts already exists, is
pure, unit-tested, and conservative by design — "an unrecognised path forces the gate to run. It
NEVER skips because it failed to classify." It just answers only two questions, for one workflow.
Promote it to a capability vector that every expensive job reads.
Measured — last 20 closed PRs
| Job |
Gate today |
Total |
Ran on |
check-test |
none — always runs |
~138 min |
20/20 |
scaffold-runtime (aspire + docker + postgres) |
classifier |
~128 min |
15/20 |
scaffold-static (deno-only) |
classifier |
~46 min |
15/20 |
desktop-native-linux |
declared, not honored (tracked separately) |
~41 min |
20/20 |
quality |
none — always runs |
~38 min |
20/20 |
≈390 min / 20 PRs ≈ 19.5 min per PR.
Five of the twenty touched no shipping code: #1123 (RFC), #1086, #1063, #1053, and #1055 —
which changed one file under .agents/. The classifier correctly skipped both scaffold tiers on
all five. Each still burned ~10 minutes on check-test + quality + desktop-native-linux,
because those jobs never consult it.
That is the RFC gap precisely located: the docs-only skip does fire for RFCs — just not on the
jobs that cost the most.
Design — one classifier, many consumers
Emit a capability vector once per PR, consumed by ci.yml and e2e-cli.yml alike:
| Output |
Consumers |
Path signal (starting set) |
needs_deno |
check-test, quality |
packages/**, plugins/**, apps/**, deno.json*, deno.lock |
needs_docker |
docker tier of scaffold-runtime |
database/queue/cache packages, Aspire helper generation, Prisma schema, plugins/** |
needs_desktop |
desktop-native-linux |
deploy / desktop / packaging paths |
needs_docs |
docs accuracy, link integrity |
docs/** |
needs_surface |
surface-diff |
already path-filtered; fold in for one decision point |
Invert the docker default
scaffold-static (deno-only) and scaffold-runtime (aspire + docker + postgres) already form two
tiers. What is missing is that any packages/** change escalates to the docker tier. Docker
should be the exception, reached on needs_docker, with a deno-only runtime tier as the default —
this is the single largest line in the table.
Paths are the mechanism; labels are the override — ratified 2026-08-03
Keep exactly the three labels that exist: ci:full, ci:skip-scaffold, ci:skip-e2e.
Add no per-technology labels (postgres, prisma, redis, docker, …). Deriving routing from
labels moves the decision into GitHub's label UI, where it cannot be unit-tested and every author
must remember to apply it — and the label set grows without bound until it is itself unmaintainable.
The classifier is tested code; labels are untested configuration. Prefer the tested one. A human who
knows better reaches for ci:full; nobody has to remember to tag postgres.
Two risks to hold
- A skipped job must still report, or it stops being required.
scaffold-static already
solves this — it starts and reports success with a skipped-by-policy step. Reuse that pattern for
check-test and quality; do not invent a second one, and do not simply drop the jobs.
needs_docker's path set is the dangerous one. Too narrow and a wiring regression ships.
Start it deliberately wide and tighten only against observed green history — the same
"additive until observed green" discipline e2e-cli.yml already documents for the runtime lane.
Acceptance
Provenance
Owner observation 2026-08-03: the Actions queue grows with parallelised agent PRs and most PRs run
CI unrelated to their scope. Measured over the last 20 closed PRs. The
desktop-native-linux wiring defect found in the same audit is tracked separately as a discrete
fix that can land ahead of this redesign.
Summary
Every expensive CI job except the two scaffold tiers runs unconditionally on every pull request.
check-test(~7 min) andquality(~2 min) have no job condition at all, so an RFC or aone-file documentation change pays the same toolchain bill as a change to
packages/service.The fix is not more workflow YAML.
.github/scripts/ci-classify-changes.tsalready exists, ispure, unit-tested, and conservative by design — "an unrecognised path forces the gate to run. It
NEVER skips because it failed to classify." It just answers only two questions, for one workflow.
Promote it to a capability vector that every expensive job reads.
Measured — last 20 closed PRs
check-testscaffold-runtime(aspire + docker + postgres)scaffold-static(deno-only)desktop-native-linuxquality≈390 min / 20 PRs ≈ 19.5 min per PR.
Five of the twenty touched no shipping code: #1123 (RFC), #1086, #1063, #1053, and #1055 —
which changed one file under
.agents/. The classifier correctly skipped both scaffold tiers onall five. Each still burned ~10 minutes on
check-test+quality+desktop-native-linux,because those jobs never consult it.
That is the RFC gap precisely located: the docs-only skip does fire for RFCs — just not on the
jobs that cost the most.
Design — one classifier, many consumers
Emit a capability vector once per PR, consumed by
ci.ymlande2e-cli.ymlalike:needs_denocheck-test,qualitypackages/**,plugins/**,apps/**,deno.json*,deno.lockneeds_dockerscaffold-runtimeplugins/**needs_desktopdesktop-native-linuxneeds_docsdocs/**needs_surfacesurface-diffInvert the docker default
scaffold-static(deno-only) andscaffold-runtime(aspire + docker + postgres) already form twotiers. What is missing is that any
packages/**change escalates to the docker tier. Dockershould be the exception, reached on
needs_docker, with a deno-only runtime tier as the default —this is the single largest line in the table.
Paths are the mechanism; labels are the override — ratified 2026-08-03
Keep exactly the three labels that exist:
ci:full,ci:skip-scaffold,ci:skip-e2e.Add no per-technology labels (
postgres,prisma,redis,docker, …). Deriving routing fromlabels moves the decision into GitHub's label UI, where it cannot be unit-tested and every author
must remember to apply it — and the label set grows without bound until it is itself unmaintainable.
The classifier is tested code; labels are untested configuration. Prefer the tested one. A human who
knows better reaches for
ci:full; nobody has to remember to tagpostgres.Two risks to hold
scaffold-staticalreadysolves this — it starts and reports success with a skipped-by-policy step. Reuse that pattern for
check-testandquality; do not invent a second one, and do not simply drop the jobs.needs_docker's path set is the dangerous one. Too narrow and a wiring regression ships.Start it deliberately wide and tighten only against observed green history — the same
"additive until observed green" discipline
e2e-cli.ymlalready documents for the runtime lane.Acceptance
ci-classify-changes.tsemits the capability vector above, unit-tested per outputcheck-testandqualityconsultneeds_denoand report skipped-by-policy when it is falsescaffold-runtimesplits into a deno-only default tier and a docker tier gated onneeds_dockerci:full,ci:skip-scaffold,ci:skip-e2e, and no othersProvenance
Owner observation 2026-08-03: the Actions queue grows with parallelised agent PRs and most PRs run
CI unrelated to their scope. Measured over the last 20 closed PRs. The
desktop-native-linuxwiring defect found in the same audit is tracked separately as a discretefix that can land ahead of this redesign.