Contributing & tests
The three CI gates, test taxonomy, PR conventions, and your first PR.
Scope: how code gets from an idea to production in quill-router. This covers the three CI gates,
the test suite's structure, branch and commit habits, how review works (mostly coding agents), and
a first-PR walkthrough with a checklist.
All paths are relative to quill-router/. Repo state: main @ 778120494, 2026-10-19.
1. The non-negotiables (from AGENTS.md)
AGENTS.md is the entry point for every coding agent and human (CLAUDE.md:3 only points at it).
These are its hard rules:
| Rule | Source | Why |
|---|---|---|
CI has three gates. Run all three locally before saying you're done: uv run ruff check . && uv run mypy && uv run pytest -q |
AGENTS.md:27-31 |
CI runs exactly these (.github/workflows/ci.yml:27-30, :259-264) |
Never run ruff format . |
AGENTS.md:33-34 |
CI only runs ruff check. Formatting the tree produces a huge unrelated diff |
| Coverage must stay ≥ 70% | AGENTS.md:35 |
Enforced once over all combined shards (ci.yml:286-332, --fail-under=70 at :332) |
mypy is a real gate |
AGENTS.md:36 |
Passing ruff and pytest locally still fails CI if mypy fails |
Plain pytest skips proof_oracle and provider_health. If you touch async settle or billing, run the oracle yourself |
AGENTS.md:37-40, pyproject.toml:77 |
uv run pytest -q -n 4 -m proof_oracle tests/test_async_settle_proof_oracle.py |
| Run the FULL suite, not only your new file | AGENTS.md:42-45 |
STORE proxy pollution (issue #333) |
| Rebase before you trust a failure | AGENTS.md:47-49 |
main moves fast. The hourly price bot commits to it |
Money code (reserve/settle/refund/credits) needs a differential or conformance test. Review by eye is not enough |
AGENTS.md:81-87 |
Highest-risk code in the repo |
| Do not weaken a conformance test to make a backend pass | AGENTS.md:91-93 |
tests/conformance/ is the executable contract for the Store Protocol |
Read-only is not load-safe: no LIKE/regex/JSON scans of tr_entities.body in prod |
AGENTS.md:70-79, CLAUDE.md:4-6 |
The 2026-09-06 Spanner CPU incident |
| Deploy production-bound changes yourself unless told to hold, and verify after | AGENTS.md:53-58 |
Joseph's default |
The STORE pollution trap (issue #333)
STORE is a module-level _StoreProxy (src/trusted_router/storage.py:3880, :3947). It forwards
calls to a backend that can be swapped. monkeypatch.setattr(STORE, "x", fake) creates an instance
attribute that shadows __getattr__. On teardown, monkeypatch "restores" a bound method of the
old backend, which silently breaks later tests. It once broke 136 unrelated tests (issue #333,
"Inject the store instead of importing the STORE module global"). Patch the backend class
(for example InMemoryStore.create_workspace), never the proxy. The conftest does this itself and
says so in a comment (tests/conftest.py:163-215, especially :179). An autouse reset_store
fixture resets the in-memory store before every test (tests/conftest.py:105-109).
2. Tool configuration (pyproject.toml)
| Tool | Config | Lines |
|---|---|---|
| ruff | line-length = 100, target-version = "py311". Rules E, F, I, UP, B, S (pycodestyle, pyflakes, isort, pyupgrade, bugbear, bandit). Ignored: S101 (assert in tests), B008 (FastAPI Depends in defaults), E501 (long lines allowed) |
pyproject.toml:105-115 |
| mypy | python_version 3.12. Only checks src/trusted_router (tests are not type-checked). disallow_untyped_defs, check_untyped_defs, warn_unused_ignores, ignore_missing_imports, follow_imports = silent, allow_untyped_globals |
pyproject.toml:95-103 |
| pytest | timeout = 600 with timeout_method = "thread" (a wedged shard once cost 2.5 h). Default addopts = -m "not provider_health and not proof_oracle". asyncio_mode = auto, testpaths = ["tests"], pythonpath = ["src"] |
pyproject.toml:64-80 |
| markers | proof_oracle (expensive frozen-main differential, has its own CI job), provider_health (live provider/catalog state), catalog_as_built (reads the catalog without test vehicles) |
pyproject.toml:81-85 |
| coverage | branch = true, source = ["trusted_router"] |
pyproject.toml:87-93 |
| dev deps | pytest, pytest-asyncio, pytest-cov, pytest-httpx, pytest-split, pytest-timeout, pytest-xdist, hypothesis, mypy, ruff | pyproject.toml:43-62 |
xdist and --dist loadgroup. CI runs -n 4 --dist loadgroup. The loadgroup part matters:
each server-backed conformance backend (postgres, spanner-pg) is pinned to one xdist_group, so its
DDL runs serially (tests/conformance/conftest.py:225-241, ci.yml:227-232).
.test_durations sharding. pytest-split splits the suite using recorded per-test durations
(38,116 entries, about 49k test-seconds). Splitting by test count produced 7 m vs 21 m shards
(ci.yml:244-248). The file must be recorded on CI runners. Local runs skip the conformance
suites, record them as ~0 s, and unbalance the shards (.github/workflows/refresh-test-durations.yml:1-12).
To refresh it, dispatch that workflow, or merge shard artifacts with
scripts/ci/merge_shard_durations.py (ci.yml:178). Example: PR #1816 "ci: refresh .test_durations
from main run 38858291020's shard artifacts".
3. CI anatomy (.github/workflows/ci.yml)
Triggers: push to main, PRs to main, and workflow_dispatch. The price bot uses dispatch because
GITHUB_TOKEN pushes don't trigger workflows (ci.yml:3-11). The jobs run in parallel, so wall time
is the slowest job (ci.yml:13-16).
| Job | What it runs | Lines |
|---|---|---|
lint |
ruff check ., mypy, scripts/update_model_changes.py --check (catalog change history is current) |
:18-32 |
spanner-emulator |
Native GoogleSQL conformance, prod SQL acceptance and schema drift, package object budgets | :34-100 |
frontend |
tsc (committed .js must match .ts), ESLint on frontend/src, Stylelint on static CSS |
:102-118 |
production-import |
uv sync --no-dev, then import trusted_router.main (catches dev-only imports) |
:120-130 |
test |
12 shards, each pytest -n 4 --dist loadgroup --splits 12 --group N, with Postgres and PGAdapter service containers. Coverage per shard without a floor |
:132-284 |
coverage |
Combines all shard .coverage files and enforces 70%. Fails if any shard failed |
:286-346 |
test-post-cutover |
8 shards, same suite with the lifecycle clock pinned past every scheduled cutover | :348-416 |
proof-oracle |
6 shards × 2 clocks, -m proof_oracle, no coverage |
:419-457 |
browser |
node --test tests/js/homepage_*.test.cjs and npm run test:browser (Playwright, tests/browser/) |
:459-503 |
byok-format-ordering |
scripts.check_format_ordering (BYOK formats vs. what every enclave accepts) |
:505-542 |
fastpath, fastpath-spanner, fastpath-walks |
Go: gofmt, vet, go test -race, TLC comparisons, emulator-backed store/owner/auditor tests |
:544-670 |
proofs |
./proofs/check.sh (TLC model-checks every TLA+ spec) |
:672-684 |
Other workflows that matter to contributors:
- deploy.yml. Push to main touching src/**, scripts/deploy/**, scripts/entrypoint.sh,
frontend/src/**, Dockerfile, .gcloudignore, pyproject.toml, uv.lock or deploy.yml
auto-deploys to Cloud Run (deploy.yml:28-37). Its gate-on-ci job polls the CI run for the
SHA and refuses to deploy unless it is green (:57-80). A hotfix input can skip the gate.
A supersede check skips stale deploys (:424-445).
- proofs-guards.yml. Runs only when proofs/** changes. 16 shards re-run every guard-table
row (guard_sweep.py --verify): each guard is removed and must break exactly what its row claims
(proofs-guards.yml:1-55).
- typed-audit.yml. Daily at 11:43 UTC. Not a PR gate. Runs scripts/audit_typed_counters.py
against prod via WIF, plus a Spanner schema-drift audit against the emulator (typed-audit.yml:1-70).
- refresh-prices.yml. Hourly at :37. Scrapes provider prices. A sandboxed, LLM-rewritable
parser tier is AST-whitelisted. The bot commits the snapshot to main (refresh-prices.yml:1-30).
That accounts for the [bot] hourly price refresh: … commits by trustedrouter-pricing-bot
(163 commits in 60 days).
- refresh-test-durations.yml (manual), dependency-scan.yml (OSV), and Dependabot (.github/dependabot.yml).
4. Dev loop
flowchart TD
A[git fetch and rebase on origin/main] --> B[branch: codex/..., fastpath/..., fix/...-YYYYMMDD, jasmine/...]
B --> C[Code and tests. Patch backend classes, not STORE]
C --> D{"3 local gates:<br/>uv run ruff check .<br/>uv run mypy<br/>uv run pytest -q (FULL suite)"}
D -- red --> C
D -- "touch settle/billing?" --> D2["uv run pytest -q -n 4 -m proof_oracle<br/>tests/test_async_settle_proof_oracle.py"]
D2 --> E
D -- green --> E[Agent review rounds: Codex or Claude review. Fix findings, add commits]
E --> F[gh pr create: summary, verification, evidence]
F --> G[CI: lint, frontend, production-import, spanner-emulator,<br/>test x12, coverage>=70, post-cutover x8, proof-oracle x12,<br/>browser, byok, fastpath Go, proofs TLC]
G -- red --> R{Failing on main too?}
R -- "no, branch is stale" --> A
R -- "yes, real failure" --> C
G -- green --> H[Self-merge: squash, usually by author]
H --> I{Changed paths match deploy.yml filter?<br/>src/**, scripts/deploy/**, frontend/src/**, Dockerfile, pyproject, uv.lock}
I -- no --> J[Done. Docs-only changes don't deploy]
I -- yes --> K[deploy.yml: gate-on-ci waits for green CI on SHA,<br/>build image, staged regional rollout, watchdog]
K --> L[Verify behavior and prod health.<br/>Never call a partial rollout 'deployed']Diagram source
flowchart TD
A[git fetch and rebase on origin/main] --> B[branch: codex/..., fastpath/..., fix/...-YYYYMMDD, jasmine/...]
B --> C[Code and tests. Patch backend classes, not STORE]
C --> D{"3 local gates:<br/>uv run ruff check .<br/>uv run mypy<br/>uv run pytest -q (FULL suite)"}
D -- red --> C
D -- "touch settle/billing?" --> D2["uv run pytest -q -n 4 -m proof_oracle<br/>tests/test_async_settle_proof_oracle.py"]
D2 --> E
D -- green --> E[Agent review rounds: Codex or Claude review. Fix findings, add commits]
E --> F[gh pr create: summary, verification, evidence]
F --> G[CI: lint, frontend, production-import, spanner-emulator,<br/>test x12, coverage>=70, post-cutover x8, proof-oracle x12,<br/>browser, byok, fastpath Go, proofs TLC]
G -- red --> R{Failing on main too?}
R -- "no, branch is stale" --> A
R -- "yes, real failure" --> C
G -- green --> H[Self-merge: squash, usually by author]
H --> I{Changed paths match deploy.yml filter?<br/>src/**, scripts/deploy/**, frontend/src/**, Dockerfile, pyproject, uv.lock}
I -- no --> J[Done. Docs-only changes don't deploy]
I -- yes --> K[deploy.yml: gate-on-ci waits for green CI on SHA,<br/>build image, staged regional rollout, watchdog]
K --> L[Verify behavior and prod health.<br/>Never call a partial rollout 'deployed']5. Test taxonomy
tests/ has 922 entries: 855 test_*.py files at the top level, ~60 support modules (fakes,
mutation tables, fixture generators, oracles), and 6 subdirectories. About 36.7k tests pass locally
(PR #1860 body: "36,751 passed").
By area (top-level test_*.py, keyword buckets, counts approximate)
| Area | ≈ Files | Examples | Notes |
|---|---|---|---|
| Async settle / shadow v2 / proof oracle | 119 | test_async_settle_drain_oracle.py, test_async_settle_proof_oracle.py, test_async_settle_shadow_v2_gate_window.py |
108 files start with test_async_*. Backed by tests/async_settle_*_mutations.py and shadow_v2_* generators. 9 files carry proof_oracle |
| Provider adapters & BYOK | 119 | test_deepseek_*, test_together_*, test_tinfoil_*, test_byok_*, test_provider_* (26) |
One file or cluster per upstream |
| Money: billing, credit, settle, Stripe, Lightning, receipts | 108 | test_billing_* (16), test_credit_* (15), test_settle_* (15), test_lightning_* (10), test_stripe_* |
Needs differential or conformance coverage (AGENTS.md:81-87) |
| Pricing, catalog, models | 104 | test_pricing_* (13), test_catalog_* (15), test_leaderboard_* |
Includes dated lifecycle files like test_late_september_2026_retirements.py |
| Gateway, routing, request path | 88 | test_gateway_* (24), test_routing_*, test_authorize_*, test_video_* (12), test_fastpath_* |
|
| Deploy, infra, cloud, ops, trust | 69 | test_deploy_*, test_cloud_*, test_synthetic_* (11), test_trust_* (11), test_ci_workflow.py |
Workflow YAML and deploy scripts are tested too (tests/deploy_script_harness.py) |
| Storage backends & analytics | 63 | test_spanner_* (8), test_postgres_* (6), test_clickhouse_* (15), test_analytics_outbox.py |
|
| Auth, accounts, keys, console, OAuth | 42 | test_oauth_* (8), test_console_* (10), test_signup_*, test_company_* |
|
| Public site, docs, blog | 23 | test_public_* (13), test_blog_featured.py, test_batch_docs.py |
Copy and SEO assertions |
| Other (attribution, axiom, config, clients, lifecycle, …) | 120 | test_config.py, test_client_*, test_lifecycle_clock.py |
Subdirectories and adjacent suites
| Location | What | How it runs |
|---|---|---|
tests/conformance/ |
22 tests plus Spanner DDL/SQL inventory, manifests and allowlists. The Store Protocol contract across memory, postgres and spanner-pg (AGENTS.md:89-105) |
Server backends skip locally unless TR_CONFORMANCE_POSTGRES_DSN or TR_CONFORMANCE_SPANNER_PG_DSN is set. Docker commands are in AGENTS.md:95-103. In CI, service containers in test and the emulator in the spanner-emulator job |
tests/fakes/ |
In-process fakes. tests/fakes/spanner.py is a large fake Spanner (see PR #1860 lock fix) |
Imported by tests |
tests/fixtures/ |
Static fixtures | |
tests/browser/ |
23 Playwright *.spec.js (homepage, console, chat, OAuth funding, token exchange…) |
npm run test:browser. playwright.config.js boots uvicorn on :18081 with TR_STORAGE_BACKEND=memory |
tests/js/ |
Node node --test homepage regression tests |
CI browser job |
tests/shell/ |
Shell-script tests | |
tests-e2e/ |
Separate TypeScript Playwright project for the /chat playground. 22 specs (streaming, multi-model, picker, persistence, voice…) with mocked SSE |
Own package.json and playwright.config.ts. See tests-e2e/README.md |
proofs/ |
TLA+ specs (CreditDebt, TerminalOrder, LeaseLifecycle, AuditorCommit, KeyCapFence, AutoRefillHandoff, SurfaceCutover) model-checked by TLC (jar pinned by sha256) |
./proofs/check.sh (CI proofs job). check_mutants.py and guard_sweep.py --verify (proofs-guards.yml) |
fastpath/ (Go) |
Go tests incl. -race and TLC comparison |
CI fastpath* jobs |
What "proofs" means here
A spec covers a protocol whose failures are interleavings, cases a unit test can't reach. The spec
is written before the code (proofs/README.md:5-7). Every spec needs:
- a .cfg naming each invariant
- small bounds, stated
- no CONSTRAINT
- TypeOK first
- liveness where the design promises progress
- assumptions listed, each with a widening mutant (proofs/README.md:39-55)
manifest.toml lists mutants: one text replacement that must violate a named invariant. If
nothing can break a claim, the claim isn't shown to say anything (proofs/README.md:60-77).
*.guards.toml tables say which guard protects which claim. proofs-guards.yml proves each table
is true. The same idea of mutation tables shows up in the Python tests
(tests/async_settle_mutations.py, tests/gateway_edge_mutations.py, …).
6. Branch, commit and PR conventions
Who commits (last 60 days)
Joseph Perla 1,181 · Jasmine Lee 190 (+18 as jasminelee) · trustedrouter-pricing-bot 163 ·
dependabot[bot] 13.
Branch names (remote branch prefixes)
codex/ 359 · fastpath/ 66 · fix/ 39 · async-settle/ 36 · fable/ 20 · test/ 15 ·
infra/ 11 · feat/ 11 · ci/ 11 · dependabot/ 10. Also jasmine/<topic> for Jasmine's work.
codex/<topic>-YYYYMMDDis agent-driven work, e.g.codex/bedrock-provider-20261018,codex/transcription-20261015.fix/<topic>-YYYYMMDDis a dated bug fix, e.g.fix/settlelog-cancel-release-20261018.<subsystem>/<topic>covers longer programs, e.g.fastpath/edge-e1-role,async-settle/shadow-v2-opt-in.- Generated chores:
deprecations-202610190004,featherless-stepfun-202610180100,deps/security-bumps.
Commit subjects
Two styles coexist. Most commits (~410 of the last 800 non-merge commits) are plain imperative
sentences. The rest use a lowercase subsystem prefix (fastpath: 59, infra: 25, docs: 19,
catalog: 17, test(s): 31, deploy: 12, async settle shadow v2: 12, fix: 12, pricing: 11,
ci: 8, proofs: 8). Squash merges add (#NNNN). Design-program commits cite the design section
and step: (design 15.8 step 5) points to docs/design/async-settle-shadow-v2-activation.md §15.8.
(E1, second increment) refers to staged increments.
Real examples:
deploy: shadow v2 opt-in pins for the probe workspace (design 15.8 step 5) (#1853)
tests: fake Spanner reads hold the commit lock (#1860)
service: the edge as a part of the node (E1, second increment) (#1859)
catalog: add frontier, advanced, professional, efficient routing tiers (#1855)
store: refuse a terminal retried after its lease closed (#1843)
infra: pin the fast path release d99cdc049 (a retried terminal after close answers from the disposition) (#1851)
fix(bedrock): require explicit upgraded gateway origin
Schedule provider model retirements from notices received 2026-10-18 (#1852)
Serve the landscape homepage unconditionally and remove the old one
ci: refresh .test_durations from main run 38858291020's shard artifacts (#1816)
[bot] hourly price refresh: 2026-10-18T17:35Z
Commit bodies are long, plain prose: the mechanism, then the evidence. Agent-authored commits
end with Co-Authored-By: Claude …. Follow-up commits on a PR often record review rounds, for
example (from 28e447a0b):
Review round one (Codex, 2026-10-19), five findings, all closed: the edge
listens and checks its upstream before admission takes the ring row, ...
PR bodies
There's no PR template. The common shape, drawn from #1853, #1858, #1860 and #1857:
1. What changed and why. Root cause for fixes ("Fix (the class, not the line)", #1860).
2. Tests and verification with numbers. "On the old fake, both tests fail with the exact CI
error. With the fix, both pass", "The full suite passes locally: 36,751 passed", "293 focused
control-plane tests passed".
3. Explicit integrity statements. "No test was skipped, retried or loosened."
4. Rollout notes and opt-out for deploy-affecting changes ("Opt-out: empty the four lines", #1853).
5. Hold markers and companion PRs, e.g. "DO NOT MERGE UNTIL GATEWAY VERIFIED. Companion gateway:
Lore-Hex/quill-cloud-proxy#535" (#1858). The author then posts release-evidence comments with CI
run URLs before merging.
6. UI PRs embed QA screenshots committed under docs/homepage/qa/ (#1857).
7. Agent-generated PRs end with 🤖 Generated with [Claude Code](https://claude.com/claude-code).
Review culture: who reviews
.github/CODEOWNERSassigns* @jperlaand says so directly: the company "currently has one person in it… this file becomes load-bearing at first hire".- No branch protection and no rulesets on
main(gh api …/branches/main/protectionreturns 404, rulesets[]). Every one of the last 40 merged PRs had 0 GitHub reviews and was merged by its author (jperlaorjasminelee). - The substitutes are agent review rounds and CI gates. Claude (often "Claude Fable" per
the co-author trailers) writes code. Codex reviews it in numbered rounds, and each round's
findings are closed in follow-up commits ("Review round ten (Codex, 2026-10-19)…"). Some PR bodies
log P1/P2 findings per round: "round 1 … P1 none, one P2; round 2: APPROVE" (#1853). The
controls that don't depend on headcount are gated CI and measured or attested deployments
(CODEOWNERS header,
SECURITY.md). - Bots:
trustedrouter-pricing-bot(hourly prices; CI is dispatched on its branch before main advances), Dependabot, and generated deprecation PRs (deprecations-YYYYMMDDhhmm).
Agent tooling in the repo
AGENTS.mdis the shared instructions for codex-cli and Claude Code.CLAUDE.mddefers to it and repeats the Spanner query safety rule..claude/launch.jsonholds one launch config:uv run uvicorn trusted_router.main:app --port 18080.skills/trustedrouter-model-advisor/SKILL.mdis a pointer skill. The canonical copy lives inLore-Hex/LLM-advisorand is also served through the TrustedRouter MCP server's Skills Extension. It's a product skill for end-user agents, not a dev-workflow skill.- Some deep areas have their own handoff docs that agents must read first:
docs/storage-portability/HANDOFF.md(AGENTS.md:10-23) andsites/token-exchange/ADDING-MARKETS.md(AGENTS.md:5-8).
7. Your first PR: walkthrough
Worked example: a small, non-money change in src/trusted_router.
- Setup. Install
uvand Node. Runuv syncandnpm ci. If you'll touch browser tests, also runnpx playwright install chromium. Start the app withuv run uvicorn trusted_router.main:app --reload(README.md:134-139). - Read
AGENTS.md. If the area has a handoff or design doc (docs/design/…,docs/storage-portability/HANDOFF.md), read that too. - Branch from a fresh main. Run
git fetch origin && git switch -c fix/<topic>-$(date +%Y%m%d) origin/main. - Write the failing test first. Put it in the matching
tests/test_<area>_*.py(look at the bucket table above). Use theclient,user_headers,inference_keyandinference_headersfixtures (tests/conftest.py:217-260). If you must fake storage behavior, patchInMemoryStore.<method>, notSTORE.<method>. For flaky or concurrency bugs, force the interleaving deterministically instead of relying on timing (the PR #1860 pattern). - Implement. Fully type every new function: mypy's
disallow_untyped_defsapplies tosrc/. Keep lines reasonable, though E501 is off. Don't reformat files you didn't change. - Run the three gates. Use
uv run ruff check . && uv run mypy && uv run pytest -q. Add-n autoto speed up the full run. Ifruff checkcomplains, fix it or runuv run ruff check --fix <your files>, neverruff format .. - Extra gates by area:
- Settle or billing:
uv run pytest -q -n 4 -m proof_oracle tests/test_async_settle_proof_oracle.py. - Store or backends: start the Postgres/PGAdapter containers and setTR_CONFORMANCE_*_DSN(AGENTS.md:95-103). - Frontend TS/CSS:npx tsc,npx eslint frontend/src,npx stylelint "src/trusted_router/static/*.css",npm run test:browser. - Catalog data:uv run python scripts/update_model_changes.py --check. -proofs/:./proofs/check.shandpython3 proofs/guard_sweep.py --verify <Spec>. - Get a review round. Ask Codex or Claude to review the diff. Address findings in follow-up commits that name the round.
- Open the PR with
gh pr create. Body: what and why, root cause, tests with real numbers, "nothing skipped or loosened", and rollout or opt-out notes. - CI red? Rebase onto
origin/mainand re-run before you debug. A failure that also happens onmainis not yours, but tell someone, because it blocks every deploy. - Merge (squash). If your paths match the
deploy.ymlfilter, the merge deploys to production. Watch the deploy run and verify the behavior and health in prod (AGENTS.md:53-58). Runtime env vars only change throughscripts/deploy/rollout.shENV_VARS(AGENTS.md:64-66).
8. PR checklist
- [ ] Rebased on current
origin/main - [ ]
uv run ruff check .is clean (and I did not runruff format .) - [ ]
uv run mypyis clean, with all newsrc/functions typed - [ ]
uv run pytest -qpasses on the full suite, not only my file - [ ] No
monkeypatch.setattr(STORE, …). Backend classes are patched instead (#333) - [ ] Coverage won't drop below 70%: new code has tests
- [ ] Settle or billing touched:
-m proof_oraclerun, plus a differential or conformance test added - [ ] Store touched: conformance passes on memory/postgres/spanner-pg, and no conformance test weakened
- [ ] Frontend touched:
.tscompiled and the.jscommitted, eslint, stylelint and Playwright pass - [ ] Catalog or model data touched:
scripts/update_model_changes.py --checkpasses - [ ]
proofs/touched:check.shand the guard sweep pass, with each new claim backed by a mutant - [ ] No ad hoc production Spanner scans were used in the investigation (
AGENTS.md:68-79) - [ ] Runtime config changed through
scripts/deploy/rollout.shENV_VARS, not by hand - [ ] PR body covers what and why, root cause, verification numbers, "nothing skipped or loosened", and rollout or opt-out notes
- [ ] Companion PRs (e.g.
quill-cloud-proxy) linked, and hold noted if ordering matters - [ ] At least one agent review round done, with findings closed
- [ ] I know whether this merge auto-deploys (path filter), and I'll verify prod afterwards