0081 — A PR runs the repo-wide tests its diff names when it opens; unit tests and guard pins still wait for the bot's green
- Status: Accepted 2026-11-01, as a ONE-WEEK TRIAL with a kill criterion (decision 5). Recorded at decision time (ADR-0004). Built in one propflowai PR, listed under Build.
- Date: 2026-11-01
- Deciders: the
cicdsession asked Fable (receipt~/.claude/jobs/fable/ff69e4139.json, verdict RESOLVED, option A, held under order reversal). Gera's standing rule routes a low-risk CI choice to Fable, not to him. Fede gets a heads-up on #engineering-bridge, because the PR-open CI cost is his lane. - Composes with: Fede's tests-on-bot-green (propflowai
a83bee783e0: a never-armed PR's unit tests and guard pins start once when the review bot passes the head, and arming adopts that run) and Fede's Oct 30 revert of unit tests at PR open (propflowai #23845 / #23854). This ADR leaves both as they are (decision 3). - Systems: propflowai
ci.yml(one new job,early-repo-wide),scripts/ci/early-repo-wide.mjs(the selector). It reusesscripts/ci/repo-wide-tests.mjs/always-run-tests.mjs(the candidates),guard-select.mjsneedlesFor(the match) andpr-selection-run.sh(the run, with its one retry).
The problem
Of the 150 propflowai PRs merged Nov 1–2, 134 review rounds came after the first one, and about 45% of them were a red CI check found after the PR opened. The check was a repo-wide test the author never runs locally: a caller-count pin (portfolio-derivation-call-sites.drift.test.ts, "expected 41 keys to deeply equal 40"), a classification inventory (clara-prompt-adoption.drift.test.ts), or a pinned call list (held-email-senders.drift.test.ts, review-card-hold-families.drift.test.ts). Under Fede's design these tests run in the Unit Tests lane, and that lane starts at the bot's green or at the arm. So the red arrives after a review round has been paid. The bot then reports it as a 🔴, the author pushes a fix, and the fix push costs another round of about 35 minutes.
Authors cannot run these tests by habit. ADR-0040 keeps the pre-push hook light, and the local guard refuses a sweep. Of the 150 PRs, 99 came from Fede's sessions, and those use different tooling. A local-only fix misses them. ADR-0030 says coverage must follow the artifact, not the producer. That rules out option B.
The decision
One early job,
early-repo-wide, on a never-armed PR's code push. It runs when preflight readsarmed == 'false'. It skips a draft, a Dependabot PR, an express run, a docs-only run, a superseded push, a label-only run, a carried run and arun-testsPR. It runs only on pushes the deferred test lane skips. That is a subset of them, not their exact complement: drafts and Dependabot PRs, which the lane defers for good, are left out. So on any one head, this job and the unit-test lane never both run. On the bot-green rerun, preflight answersarmed=true reason=bot-green, and this job skips.What it runs: the repo-wide tests the diff names, and nothing else. The candidates are the always-run repo-wide list (
always-run-tests.mjs --unfiltered): drift, ratchet, inventory, decide-*, and every test that reads a workflow or a JSON ratchet. A candidate is picked when the PR edits it, or when its source text names a changed file's path, distinctive basename or directory of depth 3 or more. That is the same needle ruleguard-select.mjsalready uses for guard pins. The run goes throughpr-selection-run.sh, so a file must fail twice to be red.What it does NOT run, by construction:
- The affected unit-test selection.
vitest --changed, the declared reads and the test-read map are not used. These are what #23845 took off PR open. - The
Guard pinsjob's selection. It stays where Fede put it, at the bot's green or the arm. - The mutation-control sweep, the F01 voice fingerprint and the operator-rename freeze fingerprint. These already run at PR open inside
CI Checks(the requiredLintcontext), since #24354 and before it. The early job does not run them a second time.
Every test the early job runs still runs again in full in the normal lane at the bot's green or the arm, so nothing moves out of that lane. The early job is advisory: it is not a required context and no verdict reads it. GitHub shows its red on the PR, and the review bot reads the failing check, so the author learns about it before the round instead of after.
- The affected unit-test selection.
A selection cap. Above it, the job falls through to the normal path. A diff that names more than 200 candidates runs nothing early. It leaves a
::noticewith the count and lets the bot-green run cover it. Measured on the 121 merged PRs before this ADR: median 67 files, p75 118, p90 185, p95 216, max 366. 112 of the 121 PRs (93%) are at or under 200. So the cap drops only the hub-sized diffs (thescripts/lib/-shaped PRs, the 940-control universe of #24354) that could bring back the Oct 30 16:00Z overload. Recall on the sample of reds is in Why 200. The job gets one 8-core spot machine, the same shape as aUnit Tests (selection)shard, and a 20-minute timeout.One week, then keep it or delete it. The check is due 7 days after the propflowai PR merges, comparing that week with the 7 days before the merge:
- Keep only if review rounds per merged PR fall by a bigger percentage than CI dollars per day rise.
- And only if peak concurrent runners (the 16:00Z shape) are no higher than in the week before, and ci.yml runs per PR are no higher (2.1 vs 1.3 was the #23845 trigger).
- Also read: the early job's own job-minutes per day, the share of PRs that run it versus fall through above the cap, and how many early reds came before the first bot verdict.
Any one of the gates failing deletes the job.
Rollback is deleting the job, plus the selector and its test. Nothing else reads its output, and no required context or verdict depends on it.
Why 200
The 25 test files named in the red verdicts of the review-rounds sample were replayed against the selector. For each, the test source came from main as it was just before the PR merged, and the diff was the PR's non-test files.
- 12 were repo-wide (the early job's scope). 11 of the 12 are picked by the needle rule. The miss is
clara-prompt-adoption.drift.test.tson #24270, an inventory of model call sites whose source names no path that PR changed. - At a cap of 200, 10 of the 12 are caught. At 150, only 8 are. #24262 selects 209 files and is lost at either cap. #24151 and #24147 select 157 and 159.
- 13 were ordinary unit tests or a guard pin. These are excluded by design (decision 3), and they keep surfacing at the bot's green.
What this cannot catch, stated
- A caller-count pin that lists its expected callers misses a brand-new caller file, when the test names neither that file nor its directory. The needle rule reads the test's source, and a new file is not in it. A directory needle catches most of these cases (#24137 was caught by
src/lib/domain/leasing). - Diffs over the cap. These get the normal path only.
- Ordinary unit tests and guard pins. These are deliberately out of scope.
Rejected
- B, run these checks in the local pre-push hook. ADR-0040 decision 3 keeps pre-push light. The local mutation-control run was dropped after 218 load-flake reruns. Also, 99 of the 150 PRs come from sessions whose tooling would not adopt it, which is the shape ADR-0030 forbids.
- C, wait for the test-result cache (propflowai #24377). The cache skips tests whose inputs did not change. The early job selects tests whose inputs did change. The two sets do not overlap, so the cache never makes this job free.
- Running the full always-run list at open. That is about 1,080 files, roughly four 8-core shards per push. It is the unit-test-at-open cost #23845 removed.
- Folding the job into
CI Checks. Its steps feed the requiredLintcontext through a pinned truth table. A trial must not change a required check's meaning, and as a separate job, rollback is a single delete.
Build
- propflowai#24407: the
early-repo-widejob inci.yml,scripts/ci/early-repo-wide.mjs, its test and its mutation control. On its own first run it caught two repo-wide pins that PR broke, before any arm. - Tracker row
early-checks(group "Cut re-runs") on docs.propflowai.co/a/ci-speed-and-cost-2026-11 holds the one-week check.