# Gates between "PR opened" and "merged" — inventory (2026-09-30)
Data: FACT = fetched today via REST (about 90 calls, fedechapa token). EST = labelled with basis.
Sources: 25 latest distinct pushes to fede-propflow PRs (runs listed per head SHA, 551 runs); job timings from 5 runs today plus the Sep 29 cache `ci-audit/jobs` (449 runs, n per row below); 11 failure logs read.

## 1. What actually gates a merge (FACT)
| Layer | Gate |
|---|---|
| Classic branch protection on main (REST `/branches/main`; `/protection` 404s for this token) | 6 required contexts: Build, Type Check, Unit Tests, Regression Gate - Leasing (required), Lint, review |
| Ruleset "Require CI" (id 14925316, updated 2026-09-13) | 1 required context: **Nested Gate** (from ci-nested.yml); 0 approving reviews required; no deletion, no force-push |
| Auto-merge guard (auto-merge-all.yml, 3,516 lines) | Not a GitHub check. Its own rules 0-9 decide whether to arm auto-merge: verdict clean at head SHA, dangerous-diff hold, base moved, red-main freeze, evidence, quiescence |
So 7 required contexts, produced by 3 workflows (CI, Claude Code Review, Nested Package CI). Everything else is an extra run. Seven workflows are already disabled manually (Terraform Security Scan, Voice base-prompt drift, Turnover harness, Lambda bundle check, Semgrep, WS prototype, PR file collisions).

## 2. Runs per push (FACT, 25 pushes, 551 runs = **22.0 runs per push**)
| Workflow | Runs/push | Outcome mix (all 25 pushes) | Wall min (median, completed) |
|---|---|---|---|
| Auto-merge on green | 3.88 | 41 skip, 32 ok, 24 cancelled | 1.2 |
| Claude Code (@claude) | 2.56 | 64 of 64 skipped | 0 |
| **CI** | 2.28 | 21 cancelled, 13 ok, 18 failed, 5 pending | 17.6 |
| Claude Review Check Override | 1.76 | 33 skip, 11 ok | 1.4 |
| Review execution re-pin | 1.76 | 33 skip, 11 ok | 1.6 |
| Branch Preview Domain | 1.24 | 29 ok | 1.1 |
| CI Dispatch Guard | 1.12 | 26 ok | 0.9 |
| Claude Code Review | 1.08 | 20 ok, 5 failed, 1 skip | 8.2 |
| Nested Package CI | 1.04 | 25 ok | 1.3 |
| Claude Review Recheck | 0.76 | 16 ok | 1.0 |
| Docs-only fence | 0.52 | 13 of 13 skipped | 0 |
| Dependabot Review Gate | 0.44 | 11 of 11 skipped | 0 |
| NO-OP review debounce (all titles) | about 0.9 | mostly cancelled or skipped | 0-4 |
| Quality Gate POC / PR gates / Semgrep / Self-review / PR body gate | 0.36 / 0.32 / 0.28 / 0.20 / 0.20 | older pushes; still firing on some SHAs | 1.9-2.8 |
| Path-filtered harnesses and evals (each under 0.15) | about 1.2 total | | 0.2-4.7 |
Seven satellites fire on essentially every push and do zero or near-zero work: Auto-merge, Claude Code, Override, Re-pin, Dispatch Guard, Docs-only fence, Dependabot gate (about 12 runs of the 22).

## 3. Machine-minutes and wall per run (FACT, job timings summed)
| Workflow | n | Machine-min median | Wall median | Jobs |
|---|---|---|---|---|
| CI (PR event) | 23 (Sep 29) + 4 (today: 37.4, 36.1, 39.1, 6.2) | **43.4** | 11.2 | 31 (21-46) |
| Claude Code Review | 14 + 3 today (3.9, 7.8, 12.4) | 4.1 (today 7.8) | 4.2 (today 7.8) | 2 |
| Self-review | 26 | 2.9 | 2.9 | 1 |
| Quality Gate POC | 14 | 3.5 | 1.7 | 3 |
| Semgrep | 14 | 2.0 | 2.0 | 1 |
| Turnover harness | 12 | 1.7 | 1.7 | 1 |
| Lambda bundle | 12 | 1.5 | 1.5 | 1 |
| PR body gate | 26 | 0.8 | 0.8 | 1 |
| Nested Package CI | 14 | 0.5 | 0.5 | 1 |
| PR size tier / file collisions / Dispatch Guard | 14 each | 0.4 / 0.2 / 0.1 | same | 1 |
| Auto-merge, Override, Recheck, Re-pin, Branch Preview | 7-20 | 0.1-0.3 | 0.1-0.3 | 1-3 |
| Path-filtered evals and harnesses (Renewal, voice-comprehensive, Sync EL, AppFolio loop, identity) | 1-5 | 2.0-6.7 | same | 1-4 |
EST per push: 22 runs x the medians above gives about 55 machine-min (matches the 55 measured in the small-PR analysis, n=15). CI is about 75-80% of it. Billing note: each job also pays a 1-minute floor on GitHub-hosted billing; on RunsOn it is boot time (EST 30-90 s per machine, no measurement).
Wall time to first verdict (from the 80-PR lifecycle sample): open to first review verdict 0.10 h median; open to merged 0.65 h median; merged to production 1.13 h median (30-min promote batching plus push CI on main).

## 4. Blocking failures: real defect or not? (today, sampled)
Of 57 CI runs, 18 failed (32%). Logs read for 11 of the failed jobs:
| Class | Count | Examples | Real defect? |
|---|---|---|---|
| Real test or eval failure | 3 | `marketing-home-page.test.tsx` FAIL in Unit Tests (affected); Renewal scoping eval assertion; Guard pins shards 2-4 (same SHA failed on both) | Yes (Guard pins and evals: likely; test failure: confirmed) |
| Runner never launched: `VcpuLimitExceeded`, limit 320 vCPU | 3 jobs on 3 runs (Semgrep, Unit Tests affected, Unit Tests full 2/4), all 15:51-16:09Z | "failed to create fleet ... vCPU limit of 320" | **No. Infra.** Fallback runner also failed |
| Restated or derived failure | 3 | `Type Check` job re-states a failed `CI Checks` ("Missing/cancelled evidence is not a code-failure verdict"); label-only run re-stating another run's failure; PR Verdict | No, duplicates of the source failure |
| Unread | 2 | | unknown |
Claude Code Review: 5 of 5 failed runs failed at "Encode verdict in check status", which is how a blocking verdict becomes a red check. So these are review verdicts, not infra. Earlier 30-day sample (prior audit): 🔴 verdicts 30 of 290, 29 followed by a push; 43% of 🔴 were real bugs (Fede's 09-29 inspection).
Take: red CI on a PR is mostly a true signal or a duplicate of one; the one big false-red source today is the 320-vCPU cap (EST: 1 in 6 failed runs). Not sampled: the other 27 required-check-less workflows' 30-day failure history (API budget; the 30-day sample is in `audit30/`).

## 5. Per-workflow classification (all 62 files)
Legend: Req = required context producer. Trig = main triggers. Keep / Fold (into CI) / Sched (make scheduled or advisory) / Delete.
| File | Trig | Req | Protects (one line) | Class | Reason |
|---|---|---|---|---|---|
| ci.yml | PR, push main, review, cron, dispatch | Yes (Build, Type Check, Unit Tests, Regression, Lint) | Code compiles, tests pass, leasing regression | Keep, simplify | 50 jobs, about 43 machine-min/push. Drop `pull_request_review` trigger; the one place everything else folds into |
| claude-code-review.yml | PR, dispatch | Yes (review) | AI review verdict | Keep | Only defect-finding review; 4 to 8 min |
| ci-nested.yml | PR | Yes (Nested Gate) | Nested npm manifests install/build | Keep, fold into CI as a lane | Needed only for ruleset name; 0.5 min |
| auto-merge-all.yml | PR, review, workflow_run, dispatch | No (the guard itself) | Arms merge only when verdict is clean | Keep, slim | 3.9 runs/push, 60% skipped/cancelled; one wake per verdict |
| claude-review-check-override.yml | review | No | Re-writes the review check after a review | Fold into claude-code-review | 1.76/push, 75% skipped |
| claude-review-recheck.yml | PR | No | Re-checks verdict after a push | Fold into claude-code-review | 0.76/push |
| review-execution-repin.yml | workflow_run, review | No | Re-pins review to the current SHA | Fold into claude-code-review | 1.76/push, 75% skipped |
| rerun-review-on-infra-failure.yml | workflow_run | No | Retries review on infra error | Fold; move off ubuntu-latest | GitHub-hosted |
| rerun-on-runner-shutdown.yml | workflow_run | No | Retries jobs killed by spot reclaim | Fold; move off ubuntu-latest | 89% skipped; GitHub-hosted |
| review-verdict-watchdog.yml | hourly cron | No | Un-sticks PRs with no verdict | Keep until review is one workflow, then delete | 24 runs/day, exists because machinery is fragile |
| ci-dispatch-guard.yml | pull_request_target | No | Guards dispatch and Dependabot PRs | Fold to one `pull_request_target` job with actor filter | 1.12/push, 2 jobs |
| branch-preview-domain.yml | PR, cron | No | Vercel preview alias | Make `opened` only | 1.24/push, not a merge gate |
| claude.yml | comments, issues, review | No | @claude on demand | Narrow trigger to `@claude` mentions | 2.56/push, 100% skipped |
| docs-only-fence.yml | reusable | No | Blocks docs-only PRs touching code | Delete (path filter in `plan`) | 100% skipped |
| pr-size-tier.yml ("PR gates") | reusable | No | PR body, size, collisions | Fold into one job | Self-review already covers body and size |
| semgrep.yml | reusable, weekly cron | No | SAST | Sched (nightly + security paths) | Already disabled manually; 2 min |
| quality-gate-poc.yml | reusable | No | POC quality checks | Delete or nightly | "POC" on every push; 41% minutes cancelled earlier |
| require-eval-diff.yml | reusable | No | Prompt PRs carry eval diff | Fold into CI `plan` lane | Path-filtered; 0.5 min |
| closer-eval-freshness.yml | reusable | No | Closer eval not stale | Fold (path-filtered) | 1.6 min |
| claude-type-sync.yml | reusable | No | Claude types in sync | Fold (path-filtered) | 1.6 min |
| phone-address-dump-drift.yml | reusable, daily cron | No | Phone map drift | Sched only | Daily cron already exists |
| schedule-drift.yml | reusable, weekly cron | No | Schedule drift | Sched only | Weekly cron exists |
| sync-specialists / sync-phone-numbers / sync-turnover-intake | reusable, push main, cron | No | Sync ElevenLabs agents to config | Keep on push main; drop PR lane | These write prod config after merge, not PR gates |
| provision-western-slope-proto.yml | reusable, push | No | WS prototype provisioning | Delete | Prototype; already disabled |
| intent-evals / renewal-evals (+sms, scoping, replay) / clara-behavior-evals / pipeline-lab-evals / voice-comprehensive | reusable, PR, cron, dispatch | No (but can turn PR Verdict red) | AI behavior regressions | Keep path-filtered (prompt/voice/renewal dirs); cron for the rest | 2-7 machine-min each; scoping eval was one real red today |
| harnesses.yml | push main, dispatch | No | 12 harness jobs | Sched (nightly) plus path-filtered in CI | 12 jobs, mostly 2.5 min |
| wo-creation-e2e.yml | PR, dispatch | No | Work-order E2E | Path-filter at `on:` | 99% skipped earlier |
| ci-docs.yml | push | No | Docs build | Keep | 1.5 min |
| deploy-lambdas.yml | PR, workflow_run, dispatch | No | Ships lambdas | Keep; trigger on lambda paths only | 24 jobs |
| deploy-renewal-worker.yml | workflow_run, dispatch | No | Ships renewal worker | Trigger on worker paths only | 1,686 runs/30d earlier |
| deploy-announce.yml | workflow_run | No | Slack deploy note | Fold into promote job | 38% skipped |
| promote-now.yml | dispatch | No | Emergency promote | Keep | Manual only |
| main-lane-red-alert.yml | workflow_run | No | Alerts on red main | Keep (ci.yml already has `alert-main-red`; remove one) | Duplicate |
| prod-freshness-alarm.yml | */30 cron | No | Prod not stale | Keep | 48 runs/day |
| cache-janitor, audit-runtime-token-parity, dlq-alarms-refresh, lambda-runtime-eol-check, adr-lens-sync, voice-prompt-drift, voice-replay, nightly-* (d8, el-config-drift, renewal-harness, suppression-canary), nightly.yml, goldmine-live-scorecard, prod-cookie-preflight, workflow-pr-digest | cron/dispatch | No | Ops hygiene and nightly checks | Keep as scheduled | Not in PR path |
| audit-runtime-token-parity, dlq-alarms-refresh | also push | No | Infra parity | Path-filter the push trigger | |
| effort-sweep-real-replays, sonnet5-vs-current-ab, el-agent-retire, renewal-evals-sms etc. (dispatch-only) | dispatch | No | One-off experiments | Delete the done experiments (sonnet5 A/B, effort sweep) | Dead weight |
| claude-code-review reusable jobs, unit-tests-* inside ci.yml | - | - | 10 extra verdict/attribution jobs inside CI (`unit-tests-check`, `-verdict`, `-pending`, `-red-attribution`, `-name`) | Fold/simplify | 5 jobs exist to encode one result |
Count: Keep 18, Fold 14, Sched 13, Delete or narrow 17 (EST, from the table rows).

## 6. RunsOn machine spec duplication (FACT, grep of `runner=`)
| Shape | Copies in `runs-on:` | Files |
|---|---|---|
| 2cpu-linux-arm64, cpu=2+4, ram=8+16, 12 families, spot price-capacity-optimized | 147 (156 `runner=2cpu` incl. variants) | 58 files |
| 2cpu-arm64 non-spot, 7 families, ram=8 | 7 | |
| 2cpu-arm64 variant with 12 other families | 2 | |
| 8cpu-linux-arm64, 32+64 GB | 3 lines (5 `runner=8cpu` mentions) | ci.yml |
| 2cpu-linux-x64, spot=lowest-price | 3 | claude-code-review, semgrep, ci |
| 1cpu-linux-arm64 | 1 | ci.yml |
Each line repeats the same `vars.CI_RUNS_ON == '1' && format(...) || fallback` ternary: 163 uses of `vars.CI_RUNS_ON`. Fallbacks are GitHub-hosted (`ubuntu-24.04-arm`, `ubuntu-latest`, `propflow-arm64-8core`). Four files still name hosted runners directly: harnesses.yml, rerun-on-runner-shutdown.yml, rerun-review-on-infra-failure.yml, voice-replay.yml. Two CI runs today showed `on-demand m7g.2xlarge` for an 8-core job despite `spot=price-capacity-optimized`.
Fix: one RunsOn `runs-on.yml` runner config (named runners, e.g. `ci-2cpu`, `ci-8cpu`), referenced by label name; drop the ternary once GitHub-hosted fallback is banned.

## 7. Top mistakes and opportunities (from this data)
Mistakes: (1) 22 runs per push, 12 of them near-empty satellites; (2) review machinery split across 6 workflows plus a watchdog; (3) 50-job CI with 5 jobs that only encode a verdict; (4) spec copied into 147 places; (5) 320-vCPU quota produces false-red runs at burst time; (6) the required list is split across branch protection (6) and a ruleset (1).
Opportunities: fold satellites into CI `plan` and one review workflow (EST 22 to about 8 runs per push); one runner config file; raise the vCPU quota or cap concurrency; path-filter harnesses; make evals/Semgrep scheduled.
Not measured: queue time per job; 30-day failure history per workflow (partial in `audit30/`).
