How a change actually travels from agent to production. A map, not a redesign.
Pinned to propflowai origin/main @ 864d9a57570. Live GitHub and AWS reads were made read-only between 21:50 and 22:10 UTC on 2026-10-02. Every claim is tagged V (verified: file:line or command output), I (inferred) or U (unknown).
Read this first: four things the brief assumed that the system contradicts.
cancelled, not failure. Since 2026-10-01 20:03 UTC a Queue fail-fast job cancels its own run on the first red check. On 10-02 there were 0 failed and about 1,377 cancelled queue runs, and 94% of 180 sampled cancellations contained a real failure. (V, §10)review and the Regression Gate — Leasing are pass-by-design. LLM evals never run in the queue or on main. (V, §3, §5)1 Topology2–3 PR lifecycle & merge queue4 & 11 Execution & economics5 Tests6–7 Dependency graph & hotspots8–9 Deploy & staleness10 Failures12 The constraint13 Unknowns14 System map
This section is synthesis. It combines the verified numbers in §2–§11, and every inference is marked as one. It does not recommend anything.
npm run build. The queue runs the full guard list, the 64-core full suite, and the build. So the queue is the first place many tests ever execute against a given change. In 18 of 22 large red-main incidents the culprit PR had a green Unit Tests, and its Guard pins were usually skipped.sync-tour-mock-covers-both-views failed 33 runs across 19 PRs in about 2.5 h (V).ci.yml merges between 09-30 and 10-02, including 2 reverts. Some of them changed which tests the queue runs (V).VcpuLimitExceeded (5 red-main incidents), a full runner disk (11 of 171 sampled cancellations), and spot reclaims (exit 137) (V).Yes: agent utilization is high while system throughput is set elsewhere (I). Agents produce PRs faster than the chain can absorb them at the current per-entry failure rate. Each extra queued PR deepens the chain, and a deeper chain raises the odds that something ahead fails and forces a rebuild. Beyond some depth, more PRs in flight means more scrap, not more merges. Where that depth sits is not measured (see §13).
| Candidate | Role today | Evidence |
|---|---|---|
| A. Compute capacity | Secondary: a source of variability, not the binding limit. EC2 quotas (4,096 spot vCPU in us-east-1) cap it, but they bind only in bursts. | §4.1, §4.2 latency burst, §10 VcpuLimitExceeded |
| B. CI duration | An amplifier. A 17-min median queue run times a 24-deep chain sets the price of each failure. | §3, §11 |
| C. Merge serialization | Half of the constraint. The single ALLGREEN chain means one red entry invalidates everything behind it. | §3 ruleset, §11.1 |
| D. Branch staleness | Real but indirect. PRs are tested against main as of their last push, about 28 commits behind by merge time, yet text conflicts are only 9% of exits. Staleness matters mostly because the PR run is not the integrated run. | §9.3, §9.5, §10.4 |
| E. Integration failure probability | The other half of the constraint. It is driven mainly by the PR lane not running what the queue runs, and by shared-count and list ratchets. | §5.6, §10.2, §10.5 |
| F. Dependency coupling | The root of E's shape. One TypeScript program, about 2,000 cross-tree alias imports, and 221 count/list ratchet drift tests that turn independent changes into dependent ones. | §1, §6, §7 |
| G. Deployment architecture | Not the merge constraint. It adds about 53 min median of main-to-live latency and couples all units into one gated release. | §8 |
| H. Combination | This is the answer: C × E, with F producing E and B pricing it. A and G sit downstream or off to the side. |
| Question | Why it matters | How to verify |
|---|---|---|
What enqueues PRs now that auto-merge-all.yml is disabled (since 10-01 20:48Z)? Every queued PR is enqueued as gera-propflow or fede-propflow. | The arming policy decides queue depth and ordering. CLAUDE.md mentions a "local merge-loop" (I), and this map found none in the propflowai repo. | Check the mini's LaunchAgents and the agentflow repo for the merge loop; read the actor on recent added_to_merge_queue timeline events. |
| Who did the bulk dequeue of 94 PRs on 10-02 between 19:00 and 21:00? | Manual removals are 168 of 310 non-merge exits, a large source of churn. | GraphQL RemovedFromMergeQueueEvent.actor; GitHub audit log. |
| Ruleset edit history. It was edited at 13:44 CDT on 10-02. | The queue parameters (24/16/ALLGREEN/30 min) may be hours old; any before/after comparison depends on them. | GitHub org audit log (needs admin); ruleset history API. |
| Does GitHub cancel CI runs for groups it invalidates, or let them finish? | It decides how much of the 86k exec-min/day of scrap is avoidable. | Correlate merge_group run cancel actor or queue-fail-fast log lines against queue rebuild timestamps. |
| Of the ~1,290 queue failures hidden by fail-fast on 10-02, what is the full class breakdown? | §10 classified only a skewed 171-run sample. | Batch-pull failed-job annotations for every cancelled merge_group run. |
| For each culprit PR, did the failing test run in its PR checks? | It separates "PR lane coverage gap" from "true overlap between two PRs", which is the core of the graph-vs-train question. | Join each red test file to the culprit PR's affected-selection list (in the CI log) and the guard-pin selection. |
| True per-entry failure rate in the queue, independent of position. | The chain arithmetic in §12 rests on it. | Group merge_group runs by PR and find each PR's first run where it is the head entry. |
| Real dollar cost per queue run (RunsOn instance-seconds × price). | The brief's "~$20 per failure" is unverified. | AWS Cost Explorer filtered by RunsOn stack tags; RunsOn cost reports. |
| Why the flex pool, defined on-demand, also runs 1,911 spot t4g.medium. | Capacity behaviour under bursts. | .github-private/.github/runs-on.yml and RunsOn logs (AWS console). |
| Spot circuit-breaker drift: live CFN 100/15/5 vs Terraform 20/15/10. | It changes how runners behave when spot fails. | RunsOn stack parameters in the AWS console. |
| Whether reused pool instances keep disk state between jobs. | Cache correctness and isolation. | RunsOn pool documentation; inspect a pooled instance. |
| GitHub cache is 10.68 GB against a 10 GB cap. What is the hit rate? | Evictions turn every run into a cold install. | Cache-restore step outputs (cache-hit) across a day of runs. |
Whether a skipped Nested Gate on a PR can mask a red one of the same name. | The required-check integrity of the queue. | GitHub support or a controlled test PR. |
| Vercel dashboard Ignored Build Step, and the project's git settings. | It decides whether anything but the API promote can deploy main. | Vercel project settings (needs the Vercel connector re-authorised or dashboard access). |
How infra/runs-on-flex, infra/temporal-worker and lambda/vercel-log-drain get deployed. | Units with no deploy workflow sit outside the map. | Ask the owners; check Terraform state and the AWS console. |
| Shared env/secrets across Vercel, Lambdas and ECS. | A runtime coupling the code can't show. | Vercel env, Lambda configuration, ECS task-def secrets (AWS console). |
Real eval run times, and how often test-full runs. | Completes the test-cost picture. | Job durations from gh api …/jobs over a week. |
| The number of commits that land after a PR's last PR-level CI event. | This, not base age, is the true staleness at queue entry. | Each PR's last pull_request run timestamp against the main log. |
AGENT SWARM (~200–470 PRs/day landed; worktree off fresh origin/main, enforced by hook)
│ pre-commit: regenerate + stage route manifest pre-push: symbol/docs-index/... checks, push-pacer
▼
PR opened ─────────────────────────────────────────────────────────────────────────────
│ pull_request → ci.yml on refs/pull/N/merge (= PR + main AS OF THIS PUSH, never refreshed)
│ Type Check (tsgo, whole repo) · Lint (whole repo) · Unit Tests (AFFECTED, cap 200)
│ Guard pins (SUBSET, 3 shards) · always-run drift/decide-* · Leasing gate (path-gated)
│ Build: NOT RUN (left to Vercel) · evals: path-gated, PR head only
│ claude-code-review (LLM, PR commits only; clean verdict does NOT survive a new SHA)
│ branch protection strict=false → being behind main is allowed
▼
ENQUEUE (auto-merge-all.yml DISABLED since 10-01; enqueued as gera-/fede-propflow — actor U)
▼
GITHUB MERGE QUEUE ruleset: ALLGREEN · build ≤24 · merge ≤16 · wait 2 min · timeout 30 min · SQUASH
│
│ base M ─▶ [M+A] ─▶ [M+A+B] ─▶ [M+A+B+C] ─▶ ... up to 24 deep, all running at once
│ │ │ │
│ each group runs ci.yml on merge_group (17 min median):
│ Checks (lint + scripts typecheck + npm run build) · Unit Tests (affected vs base_sha)
│ Guard pins (FULL, 6 shards) + Full set (64-core: full guards + full suite) ← suite runs 2×
│ review = PASS-BY-DESIGN · Leasing gate = PASS-BY-DESIGN · Queue fail-fast watcher
│
│ B red ─▶ fail-fast CANCELS B's run (reads "cancelled", not "failure")
│ ─▶ every group built on B is red/void ─▶ GitHub ejects B, REBUILDS C, D, ... on M+A
│ ~4.8 queue runs per merged PR · 68% of runs end cancelled · text conflict = 9% of exits
▼
MAIN (squash; ~19 commits/hr; up to 16 PRs per push)
│ push → ci.yml: skips tests whose tree the queue already validated; checks only
│ post-merge-heavy (cron :07/:37): full suite on tip → culprit-find, auto-revert, hold deploys
│ sync-specialists / sync-phone / turnover-intake / DLQ alarms ── fire on raw push:main,
│ NOT gated on CI ─▶ ElevenLabs + CloudWatch live within minutes
▼
ONE GATED RELEASE (needs push CI = success)
├─▶ vercel-promote: waits a 30-min batch window, promotes the newest green tip ─▶ propflowai.co
│ (one runs, one waits, a newer arrival replaces the waiter → ~80% of push runs "cancelled")
├─▶ deploy-lambdas: esbuild import-graph gate per lane (fail-open) ─▶ 13–15 of 15 lanes usually
│ ─▶ zip ─▶ aws lambda update + alias live (6–8 min after CI)
└─▶ deploy-renewal-worker: source hash over src/agents/... ─▶ arm64 image ─▶ ECR ─▶ ECS Fargate
(almost every merge rebuilds it; 13–34 min)
▼
PRODUCTION web lags main by median 53 min (p90 90) · one DynamoDB table shared by every unit
RUNNERS under all of it: RunsOn on EC2 (3 stacks: us-east-1 2-AZ "production", us-west-2, us-east-1 6-AZ flex),
one instance per job, spot + on-demand, GitHub Actions cache (10.68 GB of 10 GB cap), no Docker/K8s
except the worker image build.
The section reports were each gathered by an independent read-only pass. Where two passes measured the same thing over different windows, the numbers differ slightly. Examples: merge_group cancellations are 1,539 in the trailing 24 h vs 1,377 on the calendar day 10-02; required checks are 6 from classic branch protection plus Nested Gate from the ruleset.
Pinned to origin/main @ 864d9a57570. Tags: V = VERIFIED (cited), I = INFERRED, U = UNKNOWN.
Shape: a single monorepo, a single TypeScript program, a single root lockfile, and no monorepo build tool. (V) There are 22,015 tracked files (git ls-tree -r). No turbo.json, nx.json, Bazel, cdk.json, SAM template.yml or serverless.yml appears in the tree. None of those tools appears in root dependencies either; the positive control is that next 16.3.0, esbuild, vitest and @typescript/native-preview (tsgo) all do appear. Package manager: npm ≥11, node ≥24 (package.json engines). Root package.json has 482 npm scripts, 76 dependencies and 27 devDependencies.
packages/*, which contains exactly one package, @propflow/ui. (V)** 544 src files import it.agents/clara (@propflowai/agents-clara, own package-lock.json), mock-pms-service (own lockfile, vercel.json and next.config.js, so it is a separately deployable Vercel app), quality-gate-poc (own lockfile), testing-harness, tools/graphify. There are 4 lockfiles in total.infra/runs-on-flex/*.tf, which defines the CI runners), and Python in pockets (evals/fast-tier, infra/temporal-worker/preflight-secrets.py).src/ (12,748) Next.js app on Vercel: app/ (528 route.ts, 198 page.tsx), lib/, components/, __tests__/ lib/data/types.ts 22,852 lines, the canonical entity schema (no codegen; hand-written + JSDoc @canonical/@derived) agents/clara/ (655) Clara agent runtime; lib/ mirrors src/lib (parallel data/types.ts, 3,561 lines) lambda/ (200) 17 function dirs, each with build.ts (esbuild) + deploy.sh; lib/esbuild-clara-fallback.ts services/renewal-worker/ the ONLY Dockerfile; Temporal worker on ECS Fargate infra/ runs-on-flex (Terraform: CI runners) · temporal-worker (shell + IAM/task-def JSON, no IaC framework) packages/ui (36) the one workspace scripts/ (4,153) ops tools, DDB migrations/GSI backfills, CI helpers, harnesses (separate tsconfig.scripts.json) evals/ (1,732) · e2e/ (59) · tests/ (11) · evidence/ (262) · quality-gate-poc/ (210) config/ (13) phone-registry.json, elevenlabs-agents.json, automations.toml … contracts/ 1 file (change-term/scenarios.json), NOT an API-contract layer elevenlabs/ 1 file (.gitkeep) docs/ (1,343) INDEX.md generated by scripts/generate-docs-index.ts; 169 entries in docs/adr/ .github/ (84) 64 workflows; CODEOWNERS .husky/ pre-commit, pre-push, post-checkout root clutter .pr-body-*.md, PROGRESS-*.md, ITERATION-2-PLAN.md, chunk-complete-draft.md, e2e-rent-roll-test.mjs
Schemas and migrations (V): the schema is code, src/lib/data/types.ts plus a parallel agents/clara/lib/data/types.ts that describes itself as a "PARALLEL TREE" (header lines 1–12). There is no migrations directory. DynamoDB GSI changes are one-off npm scripts (gsi::add, gsi::backfill, gsi::revert, parity: in package.json).
Generated, committed files (V): src/lib/traffic/route-manifest.generated.ts and coverage.generated.ts (regenerated and staged by .husky/pre-commit), plus docs/INDEX.md (npm run docs:index).
Coupling (V): the boundary between agents and src exists only on paper.
tsconfig.json maps "@/lib/": ["./src/lib/", "./agents/clara/lib/*"], includes **/*.ts, and has no references. Only scripts, evals/fast-tier, quality-gate-poc and quality-replay are excluded. So src, agents, lambda, packages and e2e typecheck as one program. The ci.yml commit "tsc heap cap 12288 → 20480MB (#15374)" is a symptom of that.CROSS_RUNTIME_PATTERNS (eslint.config.mjs:8-30) blocks only literal src/ and agents/ paths. Its own comment says the alias fallback still bridges the two trees.@/lib/X import against the tree with the importer-first rule that lambda/lib/esbuild-clara-fallback.ts uses:agents/ resolve into src/lib (451 distinct targets).agents/clara/lib (182 targets).data/dynamo/index.ts and data/dynamo/conversation.ts), and which one is used depends on who imports it.src/ 27 files, lambda → agents/ 3, scripts → src 1,264, scripts → agents 113, evals → src/agents 219.services/renewal-worker/index.ts imports @/lib/temporal/workers/* directly.Can each area be built or deployed on its own?
mock-pms-service (own app), infra/runs-on-flex, packages/ui (has its own build.ts). (V/I)src + agents source. They deploy separately but are coupled at build time. (V)What tooling exists
| Instrument | State |
|---|---|
graphify graph.json | Gitignored (.gitignore:408-411). Pulled from s3://propflow-photos/graphify/ by scripts/pull-graph.ts, rebuilt nightly by Agent Smith (docs/runbooks/graphify.md:33). Not available in CI (.github/actions/nested-gate/action.yml:123). Advisory only. (V) |
npm run test:affected → scripts/test-affected.sh | Exists; covered by another agent. (V) |
Lambda gate scripts/lambda-deploy-gate.ts | Real esbuild import-graph check per lane. The diff starts from the head of the previous green push:main CI run. It fails open to deploy=true (deploy-lambdas.yml:193-247). The gate's LAMBDAS= list has 16 lanes; vercel-log-drain is absent, although it is one of the 17 directories. lambda/README.md lists tour-reminder, which does not exist, so the README is stale. (V) |
| Renewal-worker gate | A content hash over HEAD:src, HEAD:agents, packages, package-lock.json, config, tsconfig.json, … (deploy-renewal-worker.yml:193-215). Any change under src/ or agents/ redeploys the worker. (V) |
| CODEOWNERS | Covers only .github/reviewer-core.md, reviewer-lenses/, workflows/ and itself. Nothing in code is owned. (V) |
| Dockerfiles | One: services/renewal-worker/Dockerfile. It COPYs all of src, agents, config, packages and a quality-gate-poc/src slice. (V) |
| tsconfig project references | None. (V) |
| ESLint import rules | no-restricted-imports cross-runtime blocks (lines 407–569) catch literal paths only. (V) |
| Package manifests | Only one workspace, so no per-package dependency graph. (V) |
Four kinds of dependency
src/lib/data/types.ts (4,025 direct importers via @/lib/data/types) re-typechecks everything. Changing it also, by construction, changes the input of every esbuild bundle that reaches it.src/lib/data/dynamo-client.ts:239 and agents/clara/lib/data/dynamo-client.ts:70 read TABLE_NAME = process.env.DYNAMODB_TABLE_NAME || 'propflow-data'. Lambdas pin propflow-prod / propflow-stage (lambda/appfolio-sync/deploy.sh:75-76), and so does the ECS task definition (task-definition.template.json:28). The two type trees describe the same rows. A row-shape change in one tree compiles cleanly and breaks the other at runtime. (I)workflow_run on a green push:main CI run; the worker uses a source hash.Concrete examples
infra/runs-on-flex/vpc.tf against any src/app/**/page.tsx. (I; this is a negative and is not proven.)src/lib/data/repository.ts (IDataRepository), src/lib/data/index.ts, src/lib/data/dynamo/index.ts, src/lib/data/json-repository.ts and src/lib/data/store.ts (8,087 lines). Two PRs that each add a method touch the same five files. (V for the structure; I that every method needs all five.)src/lib/data/types.ts (419 commits in 30 days), package.json (203), .github/workflows/ci.yml (169). (V)route-manifest.generated.ts records that two PRs each changed a count line from N to N+1. Git merged the identical edits, main landed one short, and push:main went red (#9921, #9948, 2026-09-19). The file now stores no counts, and docs/INDEX.md does the same.eslint-suppressions.json (192 files, 765 suppressions): one PR removes file X's entry after fixing X, while a concurrent PR adds a new violation in X. Both merge cleanly and lint goes red..drift.test.ts files (7,931 test files in total). Of those, 221 mention allowlist, ratchet, baseline, ceiling or grandfather, and 210 walk the tree (readdirSync, glob, ls-files). 129 tests read .github/workflows. 32 drift tests parse types.ts. There are 71 allowlist/baseline/ratchet files outside docs/. These tests turn any concurrent tree-wide invariant into a semantic conflict. The CI agent should look at "PR lane always runs drift and decide- guard tests (#15617)".Throughput (V): 6,512 commits on main, 6,467 distinct (#N) PR refs, only 20 true merge commits (squash merging), and 45 commits with no PR ref. That is about 209 PRs/day; median 175/day, max 542/day. The brief's "~100/day" is I and looks low. Authors: "Gera" 3,845, "fede-propflow" 2,619.
Top serialization points
| # | File | Changes | Why independent agents must edit it |
|---|---|---|---|
| 1 | src/lib/data/types.ts | 419 | Single hand-written schema for every entity. Commits that touch it touch a median of 12 files (p90 34). (V) |
| 2 | package.json | 203 | 330 of the 334 added lines are npm-script registrations; 1 is a dependency. Every new script or harness registers a runner here. scripts-unreachable-baseline.json and check-scripts-runner-ratchet.ts push in the same direction (I on the exact enforcement). package-lock.json changed only 3 times. (V) |
| 3 | agents/clara/lib/agent/tools-leasing.ts | 173 | Leasing tool registry and schemas. (I) |
| 4 | .github/workflows/ci.yml | 169 | CI is reshaped constantly (lanes, heap, quarantine, merge queue). (V, from subjects) |
| 5 | src/lib/traffic/route-manifest.generated.ts | 168 | Regenerated and auto-staged by .husky/pre-commit on any route.ts or page.tsx change. (V) |
| 6 | agents/clara/lib/agent/conversation-manager.ts | 154 | The agent-loop hub. (I) |
| 7 | scripts/lib/user-route-inventory.ts | 152 | 326 hand-classified { route: … } rows; each scope change to a session-gated API route edits its row. (V) |
| 8–10 | src/lib/data/index.ts, json-repository.ts, dynamo/index.ts (+ store.ts 106) | 150 / 133 / 122 | The repository fan-in from example B. (V) |
| 11 | src/app/api/voice/personalization/route.ts | 139 | Single voice dynamic-variable webhook. (I) |
| 12 | src/__tests__/entity-classification.drift.test.ts | 124 | A registry of non-entity interface names with reasons. New exported interfaces in types.ts add {name, reason} rows here, so it moves in lockstep with #1. (V, from hunks) |
| 13 | src/lib/domain/portfolio/registry/keys.ts | 111 | Settings-key registry (2,058 lines). (V) |
| 14 | scripts/ci/scripts-unreachable-baseline.json | 96 | A ratchet list of unreachable scripts; adding or retiring a script edits it. (V) |
| 15 | src/__tests__/admin-scope-inventory.drift.test.ts | 77 | "A new route anywhere in the tree fails until classified" (header); covers all 150 admin routes. (V) |
Also notable (V): scripts/operator-rename/ratchet.json 45, config/phone-registry.json 30, eslint-suppressions.json 23, customer-identifier-fixture-baseline.ts 20, agents/clara/lib/data/types.ts 72 (the parallel schema), and claude-code-review.yml 60 and auto-merge-all.yml 52 (the merge machinery is itself a hotspot).
docs/INDEX.md is enforced but cool: 53 changes (V). It is enforced by the pre-push check-docs-index.ts, by the docs-index step in ci.yml and by a full rebuild in ci-docs.yml. It is lower churn than the brief implied.
Hooks that force edits to central files (V)
.husky/pre-commit regenerates and git adds the route manifest and coverage files whenever src/app/**/(route|page).ts(x) or src/lib/traffic/surface.ts changes..husky/pre-push runs check-symbol-sweep, check-mutation-control, check-deleted-file-importers, check-elevenlabs-deploy-lane, check-docs-index (diff-scoped against $BASE) and check-husky-runtime.Pattern (I): most hotspots are registries and inventories that a drift test keeps closed: the types classification, route inventories, scripts baseline, key registry and the generated manifest. Each turns "add one thing" into "edit one shared list". Lists merge cleanly in Git, but count lines and ratchet ceilings collide semantically, as in example D.
Pinned to origin/main @ 864d9a5757 (git rev-parse origin/main). Live API snapshot taken 2026-10-02 at about 21:58–22:00Z. Tags: V = verified (file:line or command output), I = inferred, I-gh = inferred from GitHub's documented semantics, U = unknown.
gh api repos/.../rulesets returns one ruleset, 14925316 "Require CI". It is active, targets ~DEFAULT_BRANCH, has bypass_actors: [], and its updated_at is 2026-10-02T13:44:15-05:00, earlier today. The parameters below are current as of the snapshot and may have changed recently.
merge_queue: merge_method SQUASH, max_entries_to_build 24, min_entries_to_merge 1,
max_entries_to_merge 16, min_entries_to_merge_wait_minutes 2,
grouping_strategy ALLGREEN, check_response_timeout_minutes 30
required_status_checks: [{context:"Nested Gate", integration_id:15368}],
strict_required_status_checks_policy:false
pull_request: required_approving_review_count 0, dismiss_stale_reviews_on_push false,
(other review params off); also deletion, non_fast_forwardGraphQL mergeQueue.configuration returns the same values: checkResponseTimeout 1800s, mergingStrategy ALLGREEN, minimumEntriesToMergeWaitTime 120s. (V)
branches/main/protection does not return 404. Classic protection is still active alongside the ruleset (V): required_status_checks.strict:false, contexts = Build, Type Check, Unit Tests, "Regression Gate — Leasing (required)", Lint, review, all app_id 15368 (GitHub Actions). enforce_admins:false, required_conversation_resolution:false. No required human review.
The effective required set is the union of seven contexts: the six classic ones plus Nested Gate. Neither layer requires the branch to be up to date (I-gh: GitHub layers rulesets and classic protection, and both apply).
merge_group (V)git grep merge_group -- .github/workflows: only ci.yml has a merge_group: types:[checks_requested] trigger (ci.yml:196). claude-code-review.yml:110 says its merge_group trigger was deleted. auto-merge-all.yml and ci-followups.yml use branches-ignore: ['gh-readonly-queue/**'] on their workflow_run legs.
Per ci.yml:189-195, only the "CORE lane" has a merge_group leg. The jobs that actually run are (V, from the if: conditions):
| Job | merge_group behaviour |
|---|---|
preflight, plan | run (plan diffs vs merge_group.base_sha, ci.yml:6015) |
checks (type/lint/DDB/build) | runs, on-demand rather than spot (ci.yml:765), with queue-only cache restores (ci.yml:1950-1960) |
test | affected tests against merge_group.base_sha (merge-group-affected.sh, ci.yml:2892) |
guard-manifest + guard-pins (6 shards) | run (ci.yml:3134). They do not run on push:main |
test-full | does not run on merge_group (ci.yml:3235: "NOT on merge_group either… full suite runs AFTER merge in post-merge-heavy.yml") |
full-set-one-machine | queue only: full guard pins plus full unit tests on one 64-vCPU spot box, one retry per failed file. Required by the verdict (ci.yml ~6565) |
queue-fail-fast | queue only. Cancels its own run when a verdict job turns red |
| evals, semgrep, pr-gates, docs-only-fence, regression-gate, override, pr-verdict | do not run (their if: conditions exclude merge_group) |
Which SHA the queue tests: github.sha is the head of the temporary branch gh-readonly-queue/main/pr-<N>-<base_sha> (I-gh). GraphQL confirms it is a chain (V): each entry's baseCommit is the previous entry's headCommit, and entry 1's base is the main tip 864d9a57. Each head is the squash of the PR on top of the commit before it. The <sha> in the branch name is that base commit, so the branch name does not show how many PRs the group contains. Every observed run name is a single pr-<N>.
To enter (PR head): all seven contexts must be green on the PR head SHA (I-gh: the queue only accepts a PR whose required checks pass). Because strict:false, those results come from refs/pull/N/merge as it stood at the last push, and they can be stale relative to main.
To leave (merge-group SHA): the same seven names must report success on the merge-group commit. In ci.yml they come from three different sources (V):
| Context on the queue SHA | Producer | Real or by-design pass? |
|---|---|---|
Nested Gate | merge-group-verdict (ci.yml ~6614, name: Nested Gate, scripts/ci/merge-group-verdict.sh) | Real, but it checks something different under the same name. On the queue it means "the whole core lane passed: checks, test, guard-manifest, test-aggregate, guard-pins, full-set-one-machine". On a PR, the nested-gate job runs .github/actions/nested-gate, which type-checks and builds the nested packages (mock-pms-service, testing-harness, graphify). |
| Type Check / Build / Lint / Unit Tests | merge-group-contexts matrix (scripts/ci/merge-group-context.sh) | Real. Recomputed from this run's own needs results. The script says "Nothing here is a stub". |
review, Regression Gate — Leasing (required) | merge-group-by-design matrix (ci.yml ~6670) | Pass by design. The step only runs echo "By design, not a verdict on the merge-group tree. A PR enters the queue only with ${{ matrix.name }} green on its own head." |
This is the stub pattern you asked about, and the repo names it openly. The bot review and the leasing regression gate are never evaluated against the combined tree.
merge-group-contexts and merge-group-by-design are named by expression (… && matrix.name || '<name> (not applicable)') so that a skipped job never posts under a required name (#9119). merge-group-verdict breaks that rule: its name is fixed as name: Nested Gate. So every PR-event ci.yml run leaves a skipped Nested Gate check-run on the PR head (V: PR run 37069793874's job list shows Nested Gate skipped). Whether that shadows the real PR-side result is the aggregation question the repo itself marks unsettled (U).
max_entries_to_build. States: AWAITING_CHECKS ×15, UNMERGEABLE ×8 (2 have headCommit:null, probably conflicts, I), MERGEABLE ×1. Entry 1 (#15725) was enqueued 21:28 and was still AWAITING_CHECKS at 22:00 (32 min) on its second build.enqueuer.login is fede-propflow (9) or gera-propflow (15), which are human credentials, not an App (V). The repo credits "an external loop" (auto-merge-all.yml:420, 488). Where that loop lives is U: it is not in propflowai scripts/ or .github/, and agentflow SYSTEMS.md lists only a reader (ci-factory-mq).success 676, cancelled 1,382, failure 0 (V, filtered by status=). Main received 390 commits since 00:00Z (V, commits API).guard-receiver-dot-blindness.drift.test.ts or decide-unit-tests-conclusion.test.ts:983, or Full set red), and queue-fail-fast then cancelled its own run (V). So a queue failure shows up as cancelled, never as failure.ci-CI-merge_group-code-${github.ref} with cancel-in-progress: true (ci.yml:331-333). github.ref is the unique queue branch, so one group never cancels another (V from the expression).check_response_timeout.Full set failed at 21:40, then rebuilt on main tip 864d9a57. #15606 re-entered later (V).merge_group checks_requested for each one.queue-fail-fast when they turn red themselves (V). Old runs that would have passed simply finish and are ignored. Concurrency cannot cancel them because each one has a unique ref (V).paths-ignore, so it is not evidence.History (V, ci.yml:229-245): the queue ran 07-14 to 07-21, was retired ("NEVER batched (66 trains for 63 PRs)"), and came back 2026-10-01 10:49Z (auto-merge-all.yml:420).
PR head (refs/pull/N/merge at push time, strict:false) │ 7 contexts green: Build, Type Check, Lint, Unit Tests, Regression Gate, │ review, Nested Gate(nested pkgs) ▼ [enqueue] ── by fede-/gera-propflow credential (loop location U) ▼ QUEUED ──► build group k = squash(PR) on head(k-1) [≤24 groups built at once] ▼ merge_group:checks_requested → ci.yml core lane (one run per entry, own ref) AWAITING_CHECKS ──(any verdict job red)──► queue-fail-fast cancels own run │ → Nested Gate/contexts post failure │ → UNMERGEABLE → REMOVED │ → every entry behind k REBUILT on new chain │──(no report in 30 min)──► REMOVED (I-gh) │──(conflict)──► UNMERGEABLE (headCommit null) ▼ all 7 green on group SHA (4 real, Nested Gate=core verdict, 2 by-design) MERGEABLE ──(all ahead green; ≤2 min wait; ≤16 per merge)──► squash to main ▼ push:main ci.yml (mg_validated skips tests) → promote window → deploys
15 workflows are disabled_manually (actions/workflows), including auto-merge-all.yml (since 2026-10-01 20:48Z; its last run was 20:48:16Z), claude-review-recheck.yml, claude.yml (@claude review), and the standalone semgrep, docs-only-fence, pr-size-tier and pr-body-gate. The callees still run through ci.yml's uses: (sampled PR runs: Semgrep SAST / Scan=success, PR gates / PR gates=success). auto-merge-all no longer runs at all, so its arm call (auto-merge-all.yml:1082) is moot. Enqueue happens through human credentials (§3.4).
| Stage | Trigger | What runs | Revision it checks | Concurrency | Failure behaviour / goes stale? |
|---|---|---|---|---|---|
| Commit | local | .husky/pre-commit: regenerates the route manifest only when route, page or surface files are staged (V) | index | – | blocks the commit if generation fails |
| Push | local | .husky/pre-push: symbol sweep, deleted-file importers, phone deploy lane, docs index, husky runtime (blocking); mutation controls warn-only; behind-count advisory. Then push-pacer.sh waits if >80 queued runs or >10 pushes/10 min (fails open) (V) | local origin/main | per machine | blocks the push; bypassed by --no-verify |
| PR CI | pull_request [opened, synchronize, reopened, labeled, unlabeled, ready_for_review, edited] + pull_request_review (ci.yml:169-222) | preflight ‖ plan → checks, affected test (capped), diff-scoped guard shard, evals, folded callees (docs-only-fence, pr-gates, semgrep, require-eval-diff…), regression gate, nested-gate, pr-verdict | default checkout = refs/pull/N/merge at event time (I-gh; no ref: override except ref: main at ci.yml:7044) | per event/label class + ref, cancel-in-progress except push (ci.yml:331) | goes stale when main moves, never re-run (strict:false); no full suite per PR |
| Bot review | claude-code-review.yml: opened, ready_for_review, reopened, synchronize, dispatch; not merge_group (V) | relevance (about 90 s settle wait + supersede check) → LLM review → encoder posts review | PR head (claude-code-review.yml:1011) | per PR, no cancel (1 running + 1 pending) | re-runs every push, incremental from the last full read; 🔴 → red |
review writers | several (V) | encoder check_run; ci.yml override (APPROVED, only if commit_id == head); recheck (blocking carry-forward, disabled); dangerous-diff-fence merge-hold commit status (pull_request_target, main's copy) | head SHA | per PR | how GitHub combines same-name writers is contested in-repo (U) |
| Fences | dangerous-diff-fence.yml (pull_request_target); stacked-pr-guard.yml (PRs with a non-main base, including auto_merge_enabled) | dangerous-globs label + review hold; disarms stacked PRs | main's copy of the workflow; PR read via API | per-PR groups | hold is cleared only by label/state |
| Queue | see §3 | ci.yml core lane plus full set on one machine | merge-group SHA | per queue ref | eject plus rebuild of everything behind |
| push:main CI | push main (docs paths-ignore) | merge-group-validated.sh: if a successful merge_group run had the same tree, test/guard-pins/test-full skip (ci.yml:437, fails open); checks still runs; vercel-promote; alert-main-red sets AUTO_MERGE_FREEZE | main SHA | per run; preflight cancels superseded runs | red → freeze + alert |
| Web deploy | vercel-promote in push CI | waits for the 30-min batch window (promote-window.env), then deploys the newest green tip | newest green main | single, no cancel | Lambdas wait on the whole CI run |
| Lambdas / worker / preview | workflow_run on a successful push CI | per-lambda promotes; the worker deploys the newest green main, not the commit that triggered it | main | per-lambda, no cancel | only after the whole CI run succeeds |
| Heavy backstop | post-merge-heavy.yml cron 7,37 | full unit plus full guard pins on main HEAD. On red: bisect, open an issue, auto-revert, hold deploys | main HEAD | single, no cancel | released on green |
refs/pull/N/merge at push time, the review reads the PR head, and the queue tests a chained group SHA. Only the queue's result is tied to what lands. On push:main the tree-equality check then skips the test lanes (V).docs/runbooks/ci-and-deploy.md:69 says "no merge queue, ADR-0107". claude-code-review.yml:110 says the stub was deleted, but it lives in ci.yml merge-group-by-design. The ci.yml preflight comment lists only 4 of the 7 required contexts. auto-merge-all.yml describes a guard that is disabled.Pinned to origin/main @ 864d9a57570 (64 workflows). Live data captured 2026-10-02 ~22:05Z. Tags: V = VERIFIED (file:line or command output), I = INFERRED, U = UNKNOWN.
| Stack | How it was provisioned | Region / AZs | Environment | Which labels reach it |
|---|---|---|---|---|
runs-on | CloudFormation, RunsOn app v3.3.2 | us-east-1, 2 AZs | production | every label with no env= |
runs-on-usw2 | CloudFormation | us-west-2 | usw2 | /env=usw2/region=us-west-2 |
runs-on-flex | Terraform (infra/runs-on-flex/, module runs-on/runs-on/aws//modules/flex v3.3.2, state in s3://propflow-terraform-state) | us-east-1, 6 AZs, VPC 10.3.0.0/16 | flex | /env=flex/region=us-east-1, including pool=ci-2cpu-arm64 |
list-stacks/describe-stacks. Flex has no CFN entry; it appears as ECS cluster runs-on-flex plus tagged resources.infra/runs-on-flex/README.md says the flex stack was "applied 2026-09-30" and "cutover of the heavy jobs is a later PR". Today the old runs-on stack is still Environment=production, so it has not been cut over. I: the 2-AZ stack the README meant to replace still serves every label without an env=.RunnerMaxRuntime=720, control plane on Fargate, AppBudgetDailyUsd=5 (control plane only, not runners), Private=false (no NAT), EncryptEbs=false, EnableCacheIsolation=false, config extends from .github-private.100/15/5. The flex Terraform sets 20/15/10. The README says 20/15/10 was "carried over as live", which the live CFN stacks contradict.runners/variables APIs return 403 (no admin:org). The repo runners endpoint lists 530 mostly-offline runs-on--i-<instance> registrations. I: stale per-job registrations, not a fleet.V: no .github/runs-on.yml in the repo; pools live in .github-private:.github/runs-on.yml (last change 2026-10-02T19:06Z, "keep 6 hot"). Nearly every job uses: ${{ vars.CI_RUNS_ON == '1' && format('runs-on={0}/...', github.run_id) || '<GitHub-hosted fallback>' }} V repo variables: CI_RUNS_ON=1 (set 2026-09-28) and CI_CAPACITY=west-only (set 2026-10-02T19:57Z). scripts/lib/no-hosted-runners.ts fails any hard-coded hosted label.
| Count | Label shape | Spot | Fallback | Where it lands today |
|---|---|---|---|---|
| 115 | runner=2cpu-linux-x64/cpu=2/ram=8, 15 Intel/AMD families | price-capacity-optimized | ubuntu-latest | runs-on (no env) |
| 33+1 | pool=ci-2cpu-arm64/env=flex (ci.yml glue jobs) | pool = on-demand t4g/m7g/c7g/m8g/c8g, image ubuntu24-full-arm64, 30 GB gp3 | ubuntu-latest | flex |
| 10 | 2cpu x64 spot=false | on-demand | ubuntu-latest | runs-on |
| ~10 | various 8cpu-arm64 (m7g/m8g/r7g/r8g ± d) | mixed | propflow-arm64-8core (a hosted larger runner) | runs-on |
| 2 | 1cpu-linux-arm64 t4g/c7g/m7g (queue-fail-fast, context-claims) | spot | ubuntu-24.04-arm | runs-on |
| 1 | checks: 4cpu/32-64 GB on PR, 8cpu on others, volume=80gb, spot=false on merge_group; west when CI_CAPACITY=west-only or run_id ends in 7/8/9 | mixed | 8core | usw2 |
| 1 | test: same switch as checks, 4cpu/16-32 GB on PR | mixed | 8core | usw2 |
| 1 | test-full: 8cpu, spot=false on push | mixed | 8core | usw2 |
| 1 | guard-pins (6 shards): 4cpu/16 GB, spot=false on merge_group | mixed | ubuntu-24.04-arm | flex |
| 1 | full-set-one-machine: cpu=64/ram=128+256, m7g/m8g/c7g/c8g/r7g/r8g | spot | 8core | runs-on (2-AZ), because this label has no CI_CAPACITY clause |
| 1 | one-machine-shadow: 8cpu spot, west switch | spot | 8core | usw2 |
V EC2 snapshot (~1 h of churn, not a day): runs-on-flex: 2,536 t4g.medium (1,911 spot, 625 on-demand), 331+29 m7g.xlarge, 198 m8g.xlarge. runs-on: 522 t4g.small, 447 m7i-flex.large, 212 m6a.large, 48 c7g.16xlarge + 24 c8g.16xlarge (the 64-vCPU full-set). runs-on-usw2: 133 on-demand m7g.2xlarge plus r8gd/m8g/m8gd spot. Anomaly (V): 625 flex t4g.medium instances are on-demand while 1,911 are spot. U: why the pool, defined as on-demand, also runs spot.
runs-on=<run_id>, and the runner names runs-on--i-<instance> (V) point to one machine per job (I). The only persistent capacity is the pool: weekdays 07:00-23:59 Central keep stopped: 20, hot: 6; off-hours keep stopped: 3. The config documents "stopped < 15 s, cold start < 25 s" (V).pool=ci-2cpu-arm64 glue jobs and 4cpu guard-pins. That window held 80 merge_group and 78 PR runs. I: a capacity burst. It also falls shortly after CI_CAPACITY=west-only was flipped (19:57Z), but the cause is U.container: only in semgrep.yml:48; docker buildx+ECR only in deploy-renewal-worker.yml:364-392; docker run in rule9-recheck.yml:313. No kubectl/kubernetes/k8s/eks/helm in .github/infra (control: runs-on matched 69 files).ci-followups.yml rerun-if-runner-killed, and ci.yml:3365 ("two spot reclaims … cost a full rerun and held deploys") moved push:main test-full to on-demand.actions/cache ×26, restore ×18, save ×8, setup-node cache: npm ×62; zero runs-on/cache or extras=s3-cache. ci.yml:123-131: s3-cache dropped in PR #8279 because its "ref/branch scoping was unverified". Each stack still has an S3 cache bucket with EnableCacheIsolation=false. I: buckets unused; isolation never answered.actions/cache/usage): 10.68 GB across 715 entries, against GitHub's 10 GB repo cap. I: LRU eviction is happening. Largest items: next-build on main 3.7 GB (2 entries, keyed by github.sha), node-cache (setup-node npm) on refs/pull/* 2.4 GB in 153 entries, node-modules+node-cache on main about 3.5 GB, and eslint-cache on main 520 MB in 538 entries (the key includes github.run_id, so each main run saves a new one).node-modules-${os}-${arch}-node24-${hash(package-lock)} ×43. It is saved only from refs/heads/main (ci.yml:855-858, "PR/merge-queue refs saving their own 294MB copies … blew the 10GB cache budget"). Also tsc-build-…-${sha}, next-build-…-${sha}, tsc-cache-…-${ref}, eslint-cache-…-${ref}[-${run_id}], pipeline-lab-llm-cache-…. I: GitHub scopes caches by branch, so PR and merge-queue runs read main's caches but write their own. The setup-node cache: npm saves are not covered by the main-only rule, which explains the 153 PR entries.next-build/tsc-build on main (max 600).upload-artifact@v4 and 5 download-artifact@v4 uses. These go to GitHub artifact storage. ci.yml has 3 uploads; nightly has 12..tsbuildinfo and .next/cache ride GitHub cache by SHA. U: whether reused pool instances keep disk state.ubuntu24-full-arm64 (V); other labels use a RunsOn default (U).push-pacer.sh throttles pushes at the source (ci-and-deploy.md:268 cites a "CI queue … 250 runs" episode that "pinned the RunsOn fleet") (V).CI_CAPACITY switch, a run_id-digit split, and a 6-AZ rebuild not yet cut over (V).admin:org (V).total_count figures are uncapped (V). The per-conclusion splits for PR, merge_group and review come from the API's newest-1000 cap, so they cover only the last ~10-18 h (V, caveat). Push is complete.
| ci.yml event | Runs/24h | Cancelled | Sampled jobs run | Exec-min/run (median) | Wall-min (median) | What "cancelled" means |
|---|---|---|---|---|---|---|
| merge_group (passing) | 2,260 total | — | 21 | 67.5 (heavy 4/8/64-cpu: 47.7) | 17.8 | — |
| merge_group (cancelled) | 1,539 (68%) | 37 (20 cancelled) | 55.8 (heavy 44.2) | 26.4 | Every sampled run (15/15) already had a red Full set (one machine) or Guard pins (k/6) before Queue fail-fast (scripts/ci/queue-fail-fast.sh watch) cancelled the rest (V). | |
| pull_request | 1,549 | 109 | 15.5 | 12.6 | 5.7 | top-level concurrency, cancel-in-progress on non-push events |
| pull_request_review | 1,482 | 181 | 4 | 0.9 | 1.4 | same |
| push (main) | 293 | 235 (80%) | 8 | 8.9 | 8.4 | 10/10 sampled: only Vercel Production Promote was cancelled |
grouping_strategy: ALLGREEN, max_entries_to_build: 24, max_entries_to_merge: 16, check_response_timeout_minutes: 30, and the only required check is Nested Gate (V, rulesets API). Each queued entry builds on top of all entries ahead of it, so one red entry turns red every group built behind it, and those groups are rebuilt. The red jobs come in consecutive run-ID clusters (V: runs 37058672018-37058962771 all failed Full set, and 37068878224-37069655940 all failed Guard pins).Guard pins shards (4cpu, on-demand in the queue) plus full-set-one-machine (64 vCPU, step "Full guard pins, then full unit tests"). Both appear in the same job list (run 37068662351), alongside Unit Tests (affected).push:[main], pull_request, merge_group, pull_request_review and workflow_dispatch (ci.yml:153-260). It is the only workflow with merge_group. Its workflow_call children (evals, semgrep, docs fence, pr-size-tier, sync-*) inherit those events. On push, merge-group-validated.sh skips work the queue already validated (ci.yml:506), so push runs are light (~9-37 exec-min, mostly the waiter jobs).test-full [1,2] did not run in any of the 61 sampled runs (V: absent). Comments say it runs on push:main mode=full and on review verdicts for hub-sized PRs (I).one-machine-shadow.yml runs on pull_request (1,055 runs/24h) and executes on 8cpu spot whenever the head SHA starts with 0-3 (about 25% of runs), with continue-on-error. U: its per-run minutes.7,37 (45 runs/24h), with unit-full ×2 and guard-pins ×3 shards on main HEAD. This is another full-suite pass.npm ci sites, all behind a node_modules cache restore: checks, test, guard-pins ×6, test-full ×2, seed-cache, full-set-one-machine. So a merge_group run does up to about 9 restores, or 9 npm ci on a lockfile miss. deploy-lambdas.yml has 22 install sites (315 workflow_run runs/24h, V). Totals: nightly 16, harnesses 11.checks (type check, lint, DDB escapes, build) runs the full repo on every PR, merge_group and push. Its median was 2.7 min on PR and 12.4 min on merge_group. Unit tests are affected-only on PR (test → mode=affected/delta/full) and full in the queue.Vercel Production Promote ~30 min (pool), Queue fail-fast ~15 min (1cpu), Preflight (debounce) ~3.3 min on push. ci.yml has 56-65 jobs per run, of which 44-52 are skipped. I: review runs (1,482/day × ~4 glue jobs) produce several thousand pool boots a day to deliver debounce and verdict results.
run_id for push, otherwise ref). cancel-in-progress: ${{ event_name != 'push' }}. Push runs never cancel each other; superseded pushes cancel themselves in scripts/ci/main-run-superseded.sh (ci.yml:483).vercel-promote is in group vercel-promote-production with cancel-in-progress: false (ci.yml:4828-4830). I: GitHub allows only one pending job per group, so each newer push's promote replaces the pending one. That replacement is the source of the 80% "cancelled" push runs.cancel-in-progress: true (e.g. semgrep, one-machine-shadow, claude-review-recheck). deploy-lambdas, intent-evals and renewal-evals* cancel only on PR events. false on claude-code-review (1,117 PR runs/24h), auto-merge-all, deploy-renewal-worker, sync-* and post-merge-heavy. deploy-lambdas has 22 job-level groups and nightly has 12.Tags: V verified, I inferred, U unknown. :N is a ci.yml line at the pinned SHA. Timings come from gh api …/jobs, 2026-10-02.
git ls-tree origin/main)| Count | What |
|---|---|
| 7,931 | *.test.ts(x) files. By directory: src/__tests__ 3,118 · src/lib 2,328 · src/app 934 · scripts 663 · src/components 379 · agents 298 · evals 130 · lambda 67 |
| 913 | .test.tsx files. 166 import @testing-library/react; 791 files carry @vitest-environment jsdom. The default environment is node (vitest.config.ts:282) |
| 670 | *.drift.test.ts(x) files |
| 28 | Playwright specs in e2e/ |
| 2 | *.contract.test.ts, which read contracts/change-term/scenarios.json |
| 12 | Extra vitest configs (harnesses) |
| 192 files / 765 violations | eslint-suppressions.json |
The selector (V). The selector is npx vitest list --filesOnly --changed $BASE (ci.yml ~2475, scripts/test-affected.sh, scripts/ci/merge-group-affected.sh). It picks two kinds of file:
Locally it also includes staged, unstaged and untracked files.
The base per event (V):
origin/<base.ref>, fetched fresh, with checkout fetch-depth: 0 (:2420). On pull_request, the checkout is GitHub's merge ref (I; guard-manifest-select.sh tests HEAD^2 for exactly this). So tests see base as of the push, not as of merge.merge_group.base_sha.mode=full.npm run test:affected): origin/main as last fetched. The script never fetches. It refuses an unresolvable ref or a missing merge-base.The cap (V). AFFECTED_CAP=200. What happens over the cap depends on the event:
| Event | Over-cap outcome |
|---|---|
| push | mode=full |
| pull_request | mode=capped. The required Unit Tests stays PENDING until a review verdict |
| verdict-review | decide-delta-escalation.sh runs --changed against the last fully green ancestor (mode=delta); otherwise full |
| merge_group | mode=full (the full-set job covers it) |
| local | exit 1, runs nothing |
Zeros (V). classify-zero-selection.mjs splits two cases:
coverage.include.untested, merge block): changed source inside src/lib/, src/app/api/, agents/clara/lib/ or lambda/ (vitest.config.ts:487-493) that no test imports.assert-selection-covers-changed-tests.sh fails if a changed test file was dropped. Selector crash → exit 1.
Gating (V, decide-test-lane.sh).
run-tests.test-full.Unit Tests already concluded.Blind spots:
--changed. PR #5471 caused a 14-hour deploy stall this way. CI compensates with:.drift.test. and decide-.test. file except 42 hard-coded slow ones (always-run-tests.mjs SLOW_EXCLUDED).mode ∈ {affected, delta, no-tests} (V). A hub-sized PR (capped) gets neither the affected run nor the drift set on its push. Only the fence subset of guard pins runs until a verdict arrives.--changed. A local green says nothing about tests that read .sh, YAML, JSON or workflows.forceRerunTriggers, which selects everything and escalates (I: no override in the config).EVAL_SUITES (opt-in RUN_EVALS=1)ADVISORY_TESTS (advisory-tests.yml)e2e/--depth=1 empty-selection failure is closed on main (V, depth 0).The list. guard-union-files.mjs builds one list, with floors checked per source, from:
drift.test, drift-guard),readFileSync, git scans, tree walks), and#alerts emitters.It holds about 1,035 files, per a guard-select.mjs comment (I as of today).
What each event runs (guard-select.mjs):
pull_request push, GUARD_PIN_KINDS=fence first drops about 330 bookkeeping pins. The PR then runs a subset:A PR push never goes FULL, even past 300 changed files.
Shards and timing. 3 shards on PRs and 6 on merge_group (observed). There is no --passWithNoTests, and each shard checks the list's count and sha256. Each shard takes about 2.1–3.0 min on PRs and 2.6–3.6 min on merge_group.
The integration gap it documents. On 2026-10-01 there were 122 merge_group Guard-pin reds. Each was on a bookkeeping pin whose fact the batch's PR had moved, under a green PR run. The merge-queue full set on fresh main is the only lane that sees a pin moved by a sibling merge.
| Category | What runs | Trigger | Scope | Runtime | Deps |
|---|---|---|---|---|---|
| Lint | eslint --cache plus the suppressions ratchet | PR, merge_group (if: event != push, :1726). push only warms the cache | global, cached | in CI Checks | – |
| Typecheck | tsgo --noEmit, plus typecheck:scripts | tsgo on PR/push only (:1167). Full tsc on schedule/dispatch only (:1334). On merge_group the verdict is fed only by scripts-tsgo (V, verdict step); main-tree typing there comes from the build-tree tsc (:2033), which feeds build_result (I) | global | CI Checks on PR 2.3–3.7 min (V, n=20) | – |
| Static ratchets | about 12 check:* scripts (ddb-escapes, settings-reads, symbol-sweep, docs-index, …; :1580-1910) | CI Checks | global or diff vs main | in CI Checks | – |
| Build | npm run build | merge_group and Dependabot only. On PR/push it is an echo, deferred to Vercel (:1930-1975) | global | merge_group CI Checks 11.7–13.1 min (n=4) | auth secret |
| Unit/component | Unit Tests (affected) | PR (non-draft), verdict, merge_group, push | affected, cap 200 | PR 2.8–8.3 min (median ~3); merge_group 3.2–6.0 | DATA_BACKEND=json; DDB calls go to a sinkhole endpoint (vitest.setup.ts) |
| Full unit + guards | Full set (one machine): 64 CPUs; guard union, then npm test; one retry per failed file; flaky quarantine and heavy-lane-red known-red exclusions (full-set-one-machine.sh:108-112) | merge_group only (and dispatch) | global | 8.8–11.0 min (n=4) | gh token |
test-full (2 shards) | only schedule, dispatch, Dependabot (:3236-3250). ci.yml has no schedule: trigger (I: effectively dispatch and Dependabot) | global | U | ||
| post-merge-heavy.yml: unit ×2 and guards ×3 on the main tip; bisects, files an issue, auto-reverts, holds deploys | cron every 30 min | global | 15.8–16.5 min (n=3) | AUTOMERGE_TOKEN | |
| Advisory | test:advisory plus mutation controls | push:main, nightly | 3 files + quarantine | ~12.9 min (n=1) | – |
| Regression Gate — Leasing (required) | relevance check, then quality-gate-poc npm run gate on head vs base fixtures (:6220-6480) | PR and review only. Stubbed "by design" in merge_group (:6670) | path-relevant | 0.3 min when not relevant | – |
| Contract | 2 tests over contracts/ | in the unit lane (fs readers; membership in the guard union is U) | – | – | – |
| Database | No DynamoDB Local or LocalStack: repo-wide git grep found 0 files (positive control AWS_ENDPOINT_URL_DYNAMODB found). Prod-backed scripts are manual | not in CI | – | – | AWS |
| E2E | only e2e/preview-probes runs, in ui-verify (:5875-5922). Other specs are manual (test:journeys, …) | PR only when a preview exists and UI was touched; continue-on-error. Previews are opt-in ([preview]) | subset | U | Vercel preview |
| Harnesses | harnesses.yml (7 jobs: lambda-bundles, portfolio, …) | push:main (path-gated by a changes job), weekly | path-gated | 1–7 min; recent runs mixed failure | some secrets |
| Evals (LLM) | intent, renewal{,-sms,-scoping}, renewal-replay, clara-behavior (daily budget), pipeline-lab (needs a preview), voice-comprehensive | workflow_call from ci.yml only when event_class=='code' (a PR code push) and pr-plan.paths.json globs match (:6099-6188). Never in merge_group or push. Some also run on weekly crons | path-gated, PR head | timeouts 30–60 min; actual U | subscription OAuth, ElevenLabs, AWS, Vercel |
| Nightly | nightly.yml: eval sweep, smoke, canaries, mutation registry | cron 0 8 0 (weekly at this SHA), but runs were observed daily 09-27 to 09-29, so the cadence changed recently (U) | global | 128–137 min, 3/3 failure (V) | AWS, Anthropic, ElevenLabs, MS, SMOKE_* |
| Deploy validation | vercel-promote (needs type_check, test-aggregate and build success, :4762), sns-forwarder canary, deploy-lambdas pre-deploy checks. npm run canary is manual (only a comment at :5970) | push:main | – | promote 29.6 min (n=1) | Vercel, AWS |
merge-group-validated.sh runs in push:main preflight. It matches the pushed commit's tree SHA against the last 20 successful merge_group runs. On a match it skips test, guard-pins and test-full (:2327, 3130, 3326).Unit Tests already concluded on the SHA. Guard pins skip on a prior green full set. Any new push, including a merge of main, re-runs everything.| Failure | PR | merge_group | after merge |
|---|---|---|---|
| Type break from a stale base | base as of the push (I) | tsc over the build on the queue tree (V) | push tsgo |
| Test broken by a sibling merge | no | affected vs queue base, plus the full set | heavy lane, with bisect and revert |
| Guard pin moved by a sibling | subset or fence only | full guard set (122 reds on 10-01 surfaced here) | heavy lane |
| Build break | no (deferred to Vercel) | yes | Vercel |
| LLM behaviour | path-gated evals | no | weekly/nightly crons |
Queue health (V).
Queue fail-fast (14.7–15.3 min) cancels a group the moment any verdict job goes red.Percentiles (small samples), live guard-union size, real eval durations, test-full cadence, and whether the full set's quarantine or known-red exclusions mask real breaks.
Workflow and script files were read at 864d9a57570. The PR-lifecycle lookup ran after a mid-task fetch, against origin/main = 89ea1dc5e2. Live API and AWS reads (read-only) were taken around 2026-10-02 21:50Z. Tags: V = verified (file:line or command output) · I = inferred · U = unknown.
ci.yml. Lambdas and the worker trigger on workflow_run: CI completed, gated on conclusion==success && event==push. V (deploy-lambdas.yml:86-88,122-124; deploy-renewal-worker.yml:35-36,107-109; ci.yml:4762)push: main path filter. They are not gated on CI and not batched (8.4). Vsrc/ → timeout 1200 npm run build (vercel.json:2) → Vercel deployment → propflowai.co alias → Vercel functions + ~30 Vercel crons. V
main through git integration. scripts/vercel-ignore.sh skips main, and skips previews unless the commit says [preview]. The skip lives in the dashboard Ignored-Build-Step setting. vercel.json has no ignoreCommand, and that is deliberate: one there would cancel the API promote. Vvercel-promote (ci.yml:4762):push, after checks (type check + build) and test-aggregate are green (4771-4776). Vvercel-promote-production, cancel-in-progress:false (4829-4830). One deploy runs at a time, one waits, and a third arrival cancels the waiting one. Vscripts/ci/promote-window.env: PROMOTE_WINDOW_DEFAULT_MINUTES=30; the repo var PROMOTE_WINDOW_MINUTES overrides it). The job allows up to 40 min of waiting (timeout-minutes: 80). Vdecide-promote-target.sh), checks ordering against the SHA production is actually serving, then warms the site and requeues the tip if it was cancelled. Vpromote-now.yml: a manual dispatch whose run tells a waiting promote to skip the window. Vmain-preview.yml: on a green main CI run, creates an API preview deployment and aliases propflow-main.vercel.app / main.propflowai.co (never backwards). Vprod-freshness-alarm.yml: a 30-min cron that compares main's green tip with the SHA Vercel production is serving and pages on a stall. Vdeploy-queue-watchdog.yml: a 10-min cron that finds deploy runs starved of runners. It auto-cancels only renewal-worker runs. Vdeploy-lambdas.yml, 4,280 lines)lambda/<name>/ + the src/lib/ it imports → npx tsx lambda/<name>/build.ts (esbuild) → dist/lambda.zip → aws lambda update-function-code → publish-version → update-alias live. This is raw AWS CLI: no SAM, CDK or CloudFormation. V**
LAMBDAS=, about line 230). lambda/vercel-log-drain exists in the tree but has no lane here; its deploy path is U. Vcompute-deploy job runs scripts/lambda-deploy-gate.ts per lane. It is an esbuild import-graph check over BEFORE..AFTER, where BEFORE is the head of the previous successful push:main CI run. Cancelled or red runs therefore fold into the next range. If the gate can't decide, the lane deploys anyway (fail-open). V (113-260)lambda-update-propflow-inbound-processor-prod). sns-slack-forwarder deploys as a canary inside ci.yml (5075). V[bundle-skip] can turn some of those successes into no-ops; how many is U.deploy-renewal-worker.yml)src, agents, services/renewal-worker, infra/temporal-worker, packages, config, lockfiles and a few others → source hash (git rev-parse HEAD:<paths> | sha256sum, 202-217) → arm64 Docker image → ECR propflow-renewal-worker:<sha12> → ECS Fargate service renewal-worker on cluster propflow-renewal-worker-prod (new task-def revision, ecs wait services-stable). After success the image gets the tag deployed-<env>-<srchash>. V
HEAD:src is in the hash, so almost every code merge rebuilds the worker. I:4393 runs image :399a99e259e1, the same SHA as Vercel prod (deployment 6818693735, 21:28Z). V The filtered runs list showed nothing for this workflow after 19:45Z although a deploy clearly happened, so run listings are unreliable as a freshness signal. V| Workflow | Trigger | Writes |
|---|---|---|
sync-specialists.yml | push: main, paths src/lib/tools/, agents/clara/lib/{agent/specialists,voice-agents}/ (76-82) | ElevenLabs agent / prompts / transfers / turn / language / voice-sound, shared fleet and --property appfolio-45 (336-454) |
sync-phone-numbers.yml | push (paths) + nightly cron + dispatch | ElevenLabs phone assignments (prod-sync, 233) |
sync-turnover-intake.yml | push (paths) | turnover-intake agent + prompts (89) |
dlq-alarms-refresh.yml | push (paths scripts/ensure-dlq-alarms.sh, lambda/lib/observability-alarms.sh) + weekly cron | CloudWatch DLQ alarms |
apply jobs run when github.event_name == 'push'. The ci.yml workflow_call copies (6066-6094) need plan, and plan runs only on pull_request/merge_group (ci.yml:5987). So on push, the standalone trigger is the only apply path and nothing applies twice. V/api/voice/personalization) can briefly run different commits. Iservices/ contains only renewal-worker. infra/runs-on-flex (CI runners) and infra/temporal-worker (the task-def template, part of the worker hash) have no IaC apply workflow; how they are deployed is U.post-merge-heavy.yml runs the full suite after merge and is not a deploy gate. Vsrc/lib, and the worker image copies all of src/. Vpropflow-prod. Version skew between units is bounded by the batch cadence. I(#N) squash PRs. gh pr list merged:>= returns 469. That is about 4.5× the "~100/day" premise. V Per-day counts this week: 212, 153, 381, 346, 430, 523, 386. Vpaths-ignore skips docs-only commits. V/I~/.claude/hooks/guard-bash.sh:669-682 denies git worktree add -b unless the command names origin/main. Lines 722ff deny depth-limited fetches. Vscripts/worktree-setup.sh:75-94 fetches and rebases the main checkout, then runs worktree add -b with no explicit base (94). .husky/post-checkout:428-432 then fetches and fast-forwards a fresh branch to origin/main. V MEMORY says the script is broken on the mini, so agents use a bare worktree add. IPUT …/update-branch is a capped self-heal for a wedged Unit Tests suite (auto-merge-all.yml:3467). Vstacked-base-sweep.yml is a nightly job that only reports. stacked-pr-guard.yml only disarms auto-merge on stacked PRs (39). V.husky/pre-push (158-180) reports the behind-count as advisory only and never fetches, so the count can be stale. V The push-pacer (pre-push:199 → scripts/ci/push-pacer.sh) rate-limits bursts and fails open. Vref (24 of 25 checkouts; the only one with a ref is ref: main at 7044). They test refs/pull/N/merge, which is the PR merged into main as of that event. Confirmed in ci.yml:2507-2510. Vstrict: false, so a PR does not have to be up to date. Required checks: Build, Type Check, Unit Tests, Regression Gate — Leasing, Lint, review. VNested Gate, plus a merge queue: SQUASH, build up to 24 entries, merge up to 16, min-wait 2 min, ALLGREEN, 30-min check timeout. Vmerge_group.base_sha and Full set (one machine) (6565). A hub-sized selection "executes nothing here… full suite runs after merge" (merge-group-affected.sh). Vreview and the Leasing gate are pass-by-design (merge-group-by-design), so the LLM review never sees the combined tree. Live ElevenLabs drift tests are skipped in the queue. Vpull_request.head.sha (e.g. 2511), and any push requires a fresh run. Vclaude-review-recheck.yml (on synchronize) carries only a BLOCKING verdict, as a separate Carried-forward BLOCKING verdict check-run (118). supersede-carried-review.yml replaces that stamp once a real verdict lands. Every rebase or merge of main therefore costs a full LLM review round. V| Metric | Median | p90 |
|---|---|---|
| first commit → PR open | 2.9 min | 23 min |
| PR open → merged | 141 min | 230 min |
| branch base → merged | 229 min | 307 min |
| main commits landed between base and the PR's squash | 28.5 | 58 (max 63) |
| queue dwell (squash committer date → mergedAt, a proxy) | 35 min | 155 min |
| commits per PR | 1 | 3 |
V (gh pr list + pulls/N/commits + git rev-list --first-parent). "Base" is the parent after any rebase, so this undercounts the original branch age. I
refs/pull/N/merge snapshot) is U. Only the queue tests the PR against them. V/ICollected 2026-10-02 ~22:00 UTC, read-only; main tip 429eca86187. Tags: [V] verified from logs, annotations or the API; [I] inferred; [U] unknown. Failures were classified from failing-job annotations and log greps, not from run conclusions. Excluded: claude-code-review verdicts, and cancellations from cancel-in-progress (push:main had 2,063 of these since 09-18 [V]).
cancelled. [V] PR #14934 added a Queue fail-fast job that cancels the whole run once any verdict job is red. Merge-queue runs ending failure went from 174 on 10-01 to 0 on 10-02, while cancelled went to 1,377. In a sample of 180 cancelled runs from 10-02, 170 (94%) contain a real failed job [V].Runs per day [V]:
| Day | Success | Failure | Cancelled |
|---|---|---|---|
| 10-01 | 369 | 174 | 149 |
| 10-02 | 674 | 0 | 1,377 (about 94% of these hide a failure) |
Reruns that came back green [V]:
1a. Before fail-fast (178 failed runs, all classified):
| Root cause | Runs | % | Evidence |
|---|---|---|---|
| A break already on main, tripped by later groups (DDB-escape gate: tests making real DynamoDB calls) | 91 | 51% | #14896 [V]: merged 17:05. 47 runs failed on 10 reply-release-* tests between 17:41 and 19:50. Fix 81783769491 says: "#14896 routed the staff-answer relay's line lookup through resolveStaffLineAndThread… tests now mock it." #14850 [I]: merged 15:41, and its own group passed. 44 runs failed on tour-reconfirm-staff-heads-up.test.ts between 15:38 and 16:34. I found no fix commit, so what ended that cluster is [U]. |
| The same drift, ratchet or guard test failing across many unrelated PRs | 77 | 43% | [V] e.g. guard-pin-kinds (16 runs / 14 PRs), workflow-grep-marker-ratchet (16 / 16), shell-pipefail-ratchet (12 / 7), import-guards (9 / 6), user-route inventory (10), dangerous-diff glob mirror (7), workflow-concurrency (6). Main has matching fixes, e.g. "regenerate guard-pin kinds sidecar (red main)" #14632/#14649/#14678. |
| One PR's own bug, retried | 5 | 3% | [V] atlas-record-only-settings, a single PR |
| Infra: AWS reclaimed the spot machine / exit 137 | 4 | 2% | [V] all on 09-30 between 23:17 and 23:20 |
| Lint plus spot | 1 | 1% | [V] |
| Textual conflict, compile, dependency, deploy | 0 | 0% | [V] Textual conflicts are ejected before any run starts (§4) |
Incidents: these 178 runs collapse to about 12 distinct incidents.
1b. After fail-fast: sample of 171 cancelled runs that contain a failed job. The sample is skewed: 100 come from 19:00–22:00.
| Root cause | Runs | % | Evidence |
|---|---|---|---|
| Same guard test across many PRs | ~75 | 44% | [V] sync-tour-mock-covers-both-views (33 runs / 19 PRs, 19:28–22:01), guard-receiver-dot-blindness (19 / 14), eval-catalog-coverage (11 / 11, within 7 minutes), property-owner-id-retired (5) |
| "Full set (one machine)" job (#15399) | 72 | 42% | [V] Log grep of 30 of them: the same clusters again (ws-lead-outcome + findtour-property-gate 11/30, voice-confirm-turnover-sync 8/30), plus quarantined files (#15203) |
| Infra: runner disk full ("No space left on device") | 11 | 6% | [V]; related fix #15452 (80 GB disk) |
| Unclassified | ~10 | 6% |
CI config churn is a cause in its own right [V]: about 20 merged changes to ci.yml between 09-30 and 10-02, including two reverts (#15220, #15372). Several of them change which tests the queue runs. [I] Some clusters are tests that never ran before merge until the queue started running them: a coverage gap showing up, not two PRs conflicting.
ci.yml, 09-18 to 10-02)703 failed runs [V]: 671 on attempt 1, 32 on attempts 2–3. Of about 4,100 total runs. Worst days: 09-29 (107/338) and 10-01 (224/509). 10-02 had 6/246. Reruns that came back green: 14 of 500 sampled successes (2.8%) [V].
By period [V]:
| Period | Main failures |
|---|---|
| Pre-queue | 644 |
| Queue on, 10-01 | 6 |
| Queue off, 10-01 20:00–23:00 | 45 |
| Queue on, 10-02 | 6 |
Caveat [V/I]: PR #14455 (09-30) and PR #15194 (10-02) made push:main skip test lanes the queue already ran. So the drop is partly real prevention and partly main testing less. This data cannot separate the two [U].
163 incidents (same test, gaps under 6 hours). Incidents lasting more than one run were red for a median of 0.5 h, p90 4.2 h [V].
| Category | Incidents | Runs | % runs |
|---|---|---|---|
| Drift / ratchet / guard / pinned-inventory tests | 55 | 247 | 35% |
| Behavior / unit tests (mocks, fixtures, expectations) | 59 | 201 | 29% |
| Unclassified* | 35 | 205 | 29% |
Infra: AWS VcpuLimitExceeded (runner fleet could not launch) | 5 | 20 | 3% |
| Deploy: Vercel promote / canary | 4 | 17 | 2% |
| Timeout / shard cancelled | 4 | 10 | 1% |
| Typecheck | 1 | 1 | <1% |
\*A CI self-test emits about 70 deliberate ::error lines, which crowds the real failures out of the annotations. A 50-log sample of this bucket resolves mostly to user-route-inventory, entity-conversation-routes and active-thread-invariant.
Was main red on code that was green on its PR? For the 22 largest test incidents I looked at the PR behind the first failing run [V]:
Unit Tests=success at the PR head.skipped on the PR.Unit Tests=failure only, i.e. it merged red.Two caveats:
73 "red main" or "collision" fix commits on main since 09-18, read by their titles [V titles; I categorisation]:
| Stated cause | Commits |
|---|---|
| Tests or mocks stale after a single PR ("red main after #X") | 29 |
| Pinned or generated artifact drift (line numbers, exact counts, sidecars, manifests, inventories, baselines) — e.g. #15745 "no longer pins an exact kind count (queue collision fix)", #15754 "per-file, not an exact shared count", #15751 "anchors on symbols, not line numbers" | 28 |
| Two PRs explicitly named as colliding ("#14660 vs #14931", "#12985 x #13017 semantic collision") | 7 |
Flake (jitter, daysAgo() milliseconds, fake timers) | 4 |
| Compile or import | 3 |
| CI config | 2 |
ci.yml)Failed / total by day [V]: 09-30 405/1,724; 10-01 460/1,589; 10-02 208/1,398. Sample of 30 failed runs, every 10th, from 10-01 and 10-02:
| Class | n | % |
|---|---|---|
| The PR's own repo-policy guard violation (drift/ratchet/fence, lint ratchets, timezone guard, legacy-field, mock-shape) | 10 | 33% |
PR size / dark-setting gate (sizedark) | 4 | 13% |
| Generated or pinned artifact out of sync (stale-base candidates)† | 4 | 13% |
| CI plumbing or infra: GitHub API rate limit inside the job (3; one [V], two in the same minute [I]); checkout/npm setup (1) | 4 | 13% |
| Duplicates: label-only re-statements (2), aggregator with no root cause (1) | 3 | 10% |
| The PR's own compile or runtime bug | 2 | 7% |
| Schedule-drift job (same PR twice) | 2 | 7% |
| LLM eval | 1 | 3% [U] |
†Stale-base candidates: #15648 failed the customer-fence baseline while touching 0 related files [I]. #15094, itself a red-main fix PR, failed on the inventory [I].
So about 55% are the PR's own fault, and mostly policy violations rather than functional bugs. About 13% are prone to a stale base, and about 23% are noise. The sample contains no clear flake [V, n=30].
Source: GraphQL RemovedFromMergeQueueEvent.reason. Note that the REST removed_from_merge_queue event also fires on successful merges. Coverage is 1,000 of 1,263 PRs, so counts are understated by up to about 20%.
| Reason | Events (PRs) | Median time to re-add |
|---|---|---|
manual | 168 (143) | 9.7 min |
failed_checks | 80 (65) | 20.5 min |
merge_conflict (textual) | 58 (49) | 36.7 min |
invalid_merge_commit | 2 | |
checks_timed_out | 2 |
manual removals came from the gera/fede accounts between 19:00 and 21:00 on 10-02. This looks like a bulk dequeue [I]; who did it is [U].failed_checks removals. Each bad entry fails the speculative groups behind it, which are then rebuilt rather than ejected [I].On PRs, the PR's own policy violations dominate.
failed_checks removal, and which files conflicted in each merge_conflict removal. Neither is exposed after the fact.