Skip to content

Gate Contract

scripts/agent-gate.sh is THE gate. A builder claiming “the gate passed” must have run this script and pasted its summary block verbatim. Ad-hoc cargo invocations do not count. This rule exists because epic #646 shipped three false-green reports from ambiguity about “which commands count” — specifically, feature-gated tests silently skipping and partial runs reported as full runs.

CI-enforced as a nightly deep-check (issue #1269, reconciled with epic #1360). The gate is no longer local-only: .github/workflows/gate.yml runs the full scripts/agent-gate.sh (never --only) in CI as a nightly, path-independent deep-check backstop (schedule: cron + workflow_dispatch for on-demand runs). It is NOT a required per-PR check — under epic #1360’s tiered model the ONE required, always-running PR check is the light .github/workflows/pr-gate.yml (fmt + cqlite-core clippy -D warnings + all-feature build + fast tests; no Docker/datasets/agent-gate). The nightly gate.yml lane fetches the pinned datasets and sets CQLITE_DATASETS_ROOT so the dataset-dependent components execute rather than skip, and uploads the SUMMARY block as an artifact. So a change that breaks a gate component (e.g. node-bindings) that the light PR check cannot see is still caught within 24h and surfaces on the Actions dashboard.

What a green required covers (issue #2910)

Section titled “What a green required covers (issue #2910)”

Branch protection requires exactly ONE context, required (.github/workflows/pr-gate.yml), and a GitHub Actions job cannot needs: a job in another workflow. Until #2910 that meant every tier in a sibling workflow — the Flight e2e tier, the parity lanes, every label-gated suite — was invisible to gh pr merge --auto: a PR could land green with its most important integration test pending, failed, or never triggered (the real instance: PR #2906’s wiring-evidence e2e test ran nowhere, because flight-ci.yml was paths:-filtered and its heavy job needed a ci:flight-full label the PR did not carry).

required now aggregates. Concretely:

  • A declared registry. .github/ci-gating-tiers.yml names every gating tier by workflow file and by the exact check-run context it emits (a check run’s name is the emitting job’s name:), plus an exempt: block listing every other pull_request workflow with a reason and an issue link.
  • required fails closed on any registered tier that is failure/timed_out, still non-terminal at the aggregation deadline (60 min default, per-tier override, always strictly below the job’s timeout-minutes so expiry is a reported red rather than a cancellation), or absent. Absence is NEVER read as inapplicability: a registered tier always emits its context, and an inapplicable tier reports that as an explicit success. Unregistered check runs (perf, docs recipe smoke) stay advisory and cannot block.
  • …but a false RED is an outage too. Failing closed applies at the DEADLINE; mid-poll, the states that are transient and self-correcting are re-polled. A cancelled tier is routine — marking a draft ready for review or adding a label cancels the tier’s in-flight run under cancel-in-progress — so it is treated as superseded while a replacement is plausible (supersession is detected positively: the replacement mints a higher check-run id), and fails once the grace lapses or the deadline arrives. A 5xx/rate-limit/DNS blip reading the check-runs API is retried under backoff and fails only on persistence. Neither weakens a genuine negative into a pass.
  • The diff mandates the tier, not the label. A registered tier’s own classifier decides applicability from git diff --name-only base...head. A mandating diff runs the tier with or without its ci:* label; the label survives only as an opt-in for non-mandating diffs. No step of the delivery flow asks you to work out which tiers are out of band, or to apply a label.
  • Enrolment is forced. pr-gate.yml now has two jobs: pr-gate-core (the gate’s own fmt/clippy/test/policy steps) and required (needs: [pr-gate-core], if: always(), the unchanged branch-protection context) which does the aggregating. scripts/ci/validate-workflows.rb runs as a step in pr-gate-core, and required fails unconditionally when the core job did not conclude success — so the rule still reds required, one job removed. It fails when a pull_request/pull_request_target workflow is neither registered nor exempted, when a registered workflow carries a blocking trigger filter (paths, branches, or a types: set that is too narrow to fire on every new head sha or wider than the aggregator observes), when its emitting job’s condition is not exactly ${{ !cancelled() }}, when no step of that job both reads a dependency’s .result and can exit non-zero, or when a registry entry is dangling. A new tier that forgets to enrol reds required. (always() is rejected on a tier gate job: it runs the job while the run is being cancelled, turning needs.*.result == cancelled into a failure conclusion, which makes the supersession grace unreachable and reds required on every routine supersession.)
  • A migration state reds in seconds, not after an hour. The registry is read from the base ref while the emitter comes from the tree the event ran (the merge commit, for a pull_request event). If the base registers a tier that tree provably cannot emit — its workflow absent, no PR trigger, types: excluding every activity type that could put the context on this head sha (not merely the event that started this run: check runs accumulate on the head from whichever event minted them), branches: excluding this base, or no job with that name — required fails on the first poll and names the remedy: rebase, or ci:waive:<tier-id> if the tier is deliberately being renamed or retired (a registry change only takes effect once merged). Inconclusive evidence never produces that verdict, and the verdict is never a pass.
  • Arming --auto stays correct. GitHub releases the merge on required going green, and required cannot go green until every registered tier has reported success — so keep arming immediately (#2667) and never poll a PR’s own CI.
  • Residual — re-run order. A tier re-run after required has already gone green cannot be retracted by a finished job: re-run the tier, then re-run required, in that order. scripts/flow/premerge-assert.sh <pr> <certified-sha> <gate-of-record-summary> [<delta-summary>] remains the closer’s last look — and since #3465 it is where the gate of record stops being a convention: the third argument is REQUIRED, and the script refuses the merge unless that file holds one full ==== AGENT-GATE SUMMARY ==== block with RESULT: PASS, tree-integrity: PASS, no nested-under: line (#2874: a nested sub-gate runs at the SAME tree, so the sha binding provably cannot see it), dirty: no (#3648) and commit:/tree-start: covering the certified sha. The dirty: requirement is the third property the sha binding cannot see: a dirty: yes run certified the sha PLUS uncommitted TRACKED edits (the gate’s capture is --exclude-standard, so never a gitignored log) and stamps the very same commit:/tree-start: it would have stamped clean. It is matched AFFIRMATIVELY — an absent, empty or unrecognised value REFUSES rather than being read as clean — and there is deliberately no env opt-out, because a dirty tree is always re-gateable. A --lite summary is refused by name anywhere, and a --delta summary as the THIRD argument (their headers are distinct by construction) — which is exactly the PR #3408 escape: 22 lite PASSes and no full gate. The OPTIONAL fourth argument is how a --delta re-cert certifies a merge. #1892 mandates --delta, “never a repeat full gate”, for a test/docs-only diff on top of a full PASS at anchor X, and mandates the PR record BOTH blocks — so a 3-arg-only guard red on correct, mandated input. In that shape the third argument is the ANCHOR’s full PASS (its sha need not be the certified sha) and the fourth is one ==== AGENT-GATE DELTA SUMMARY ==== block carrying MODE: delta (asserted affirmatively, the inverse of the full block’s belt), RESULT: PASS, tree-integrity: PASS, dirty: no (#3648 — required of the anchor block too, since a dirty anchor hangs the whole chain off a tree nobody can reconstruct), a delta-anchor: naming exactly that anchor — an (UNRESOLVED) anchor refuses — and its own commit:/tree-start: at the certified sha. The chain is closed end to end; a delta block ALONE is still the #3408 escape and still refused. And since #3653 the anchor must be ON THE CERTIFIED SHA’S HISTORY. Everything above proves the two blocks AGREE about a sha, never that the sha belongs to THIS PR: Case B’s anchor identity rested on the delta run’s SELF-DECLARED delta-anchor: line, so any full-gate PASS plus a delta naming it satisfied the chain — the #3616 cross-lane class surviving in the one path Case A’s sha binding does not cover. git merge-base --is-ancestor <anchor> <certified> closes it offline, and the verdict is three-valued because --is-ancestor’s rc 1 is itself three-valued (#3544: in a shallow clone it also means “the connecting history is absent”). BOUND (rc 0) proceeds and is recorded affirmatively as anchor-ancestry: BOUND on the PREMERGE: DELTA-RECERT line — a silent pass is indistinguishable from a check that never ran. NOT-ANCESTOR (rc 1, both objects present, the repository proven complete via git rev-parse --is-shallow-repository = false) is exit 2, naming both shas. Everything UNMEASURABLE — no git, not inside a work tree, either object absent, shallow or shallowness unknown, --is-ancestor exiting ≥ 2 — is exit 3 under its own PREMERGE: ANCHOR-UNVERIFIABLE marker with a per-cause remedy, because an unmeasurable result is UNKNOWN and “fix the box” is a different operator action from “your chain is wrong”. The walk does not run in the lane (roborev job 355): $GIT_DIR/info/grafts rewrites parentage and SURVIVES --no-replace-objects (#3544 job 285’s measurement, re-measured here), so a graft alone manufactures BOUND — and on this fleet grafts live in the COMMON git dir every lane shares, so the planter is a peer lane as well as an accident. The reads and the walk therefore run in a throwaway git init scratch whose only view of the lane is GIT_ALTERNATE_OBJECT_DIRECTORIES (pure object storage: no config, hence no grafts, no replace refs, no promisor), and failing to build it is UNVERIFIABLE, never a fall-back to the live repository. --is-shallow-repository deliberately still reads the LANE: a fresh scratch is never shallow, so probing it there would make the shallow guard a vacuous pass. The scratch’s environment is load-bearing (job 358) — measured, GIT_DIR overrides -C, and GIT_TEMPLATE_DIR/--template= seed a planted info/grafts into the new repository — so every git call, discovery reads included, runs under env -i + an allowlist (PATH, TMPDIR, then GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM=/dev/null) with an explicit empty --template=. The reads are bounded by the advisory’s own runner — the EXTERNAL commands only (git and mktemp -d), recorded as anchor-reads: bounded-<n>s+<g>s(external:git,mktemp,sh;UNBOUNDED:command-v+pwd-builtins). The scratch dir is deliberately not deleted (a race-free delete needs openat/unlinkat, absent in shell, and all lanes run as one user), so it is left for the OS to reap. An anchor-path operation audit in the script header records, per operation, whether it is bounded and whether its target is validated — added because seven findings here were those two questions asked of different operations one round at a time. What remains unbounded is command -v and one $(pwd) diagnostic, both builtins over local state — and with no timeout/gtimeout supporting --kill-after the check REFUSES, naming a one-command remedy. That reverses an earlier ruling (“run unbounded, declare it, never refuse”): a hang in this guard blocks the merge anyway, so the choice is hang silently vs refuse with a cause, and the refusal strictly dominates. Hand-rolling a portable runner is ruled out as new process-lifetime code. The commit-graph is disabled on those reads (-c core.commitGraph=false, job 361): it reaches the scratch through the alternate, is not content-addressed, and git trusts its parent edges — measured, a forged graph changes what rev-list --parents reports, though on git 2.43.0 it did NOT change merge-base --is-ancestor, so the flag is defence in depth pinned structurally. core.multiPackIndex and reachability bitmaps were measured as not consulted for this call and deliberately left alone. Then the boundary is DECLARED, not enumerated again (the #3746 / job-311 precedent, after three rounds found three routes into one mechanism): every Case B success line carries one constant — ancestry over this box's SHARED object store and SCRATCH namespace: objects, metadata and scratch TRUSTED, not verified (#3746) — closes accident/drift, NOT a same-UID peer. That is the terminus of the hardening line (job 390): all lanes run as one user, so a peer can write the scratch as well as the object store (it can plant a graft in the scratch between git init and the walk), no permission boundary exists, so the claim is narrowed and the hazard assigned to #3746. A later same-UID-peer instance is that declared boundary, not a new defect. It proves ancestry over what this box’s shared store presents; it does NOT prove the anchor is on the PR as GitHub sees it, nor anything against a peer that can WRITE that store. A further route in the family is a residual under that declaration rather than a false claim here. What a PREMERGE: OK does NOT prove (#3650), printed on the success path as PREMERGE: SCOPE. It proves the diff is unchanged since certification and that a full gate PASSed on THAT EXACT TREE. It does not prove the change was certified against the main it will join: a squash-merge composes the diff with main’s CURRENT tip, so for any PR whose base is behind main the certified tree and the merged tree are different objects (measured on #3358/PR #3362 — a head gate FAILing only because a known flake’s fix was on main and not in that base; the malign direction is a PASS at a stale head hiding an interaction with something that landed in between). A gate on the merge result is #3650 slice 2 and is deliberately not part of this mechanism. #3650 slice 1 DID land, as a non-blocking advisory: PREMERGE: ADVISORY lines. They carry scripts/flow/base-staleness.sh’s report — N commits behind the merge-base with origin/main (never the base ref’s tip, #3392) and M of those touching the diff’s blast radius, defined as (paths the diff touches) + (a hard-coded, no-env-override gate-global set): content that can change ANY gate’s verdict regardless of the diff — a NAMED, COMMITTED list (GATE_GLOBAL_PATTERNS), never an inline glob, whose membership asserts exactly one predicate: content here can change a gate’s verdict INDEPENDENTLY of the diff (.config/nextest.toml, the toolchain pin, the Cargo manifests, scripts/agent-gate.sh, scripts/ci/**, scripts/tests/**, cqlite-core/tests/support/**, test-data/**, .github/workflows/**). scripts/tests/** is in it because the gate does not merely READ that roster, it EXECUTES it (tooling-tests runs ~16 of them), so one commit touching one of those files reds EVERY lane’s full gate. Measured against the case that produced the issue: on PR #3362 the culprit commit and the diff share NO path, so path intersection alone would call that certification fresh exactly when it was not; intersection + gate-global fires on 37 of 107 commits behind (35%) — measured at origin/main b1e8598a2, subject 4bc6b913a, the sha quoted because behind is a function of where main was — and the run NAMES the culprit (matched 5e08db201 gate-global .config/nextest.toml) so the detection is attributable rather than coincidental on a count. The list is declared NON-CLOSED in the output (gap 2 of 2, beside the dependency-closure gap), and the two path sources are pinned rename-symmetric and root-relative — porcelain git diff honours diff.renames/diff.relative and plumbing git diff-tree does not, so unpinned, a PR that renames a path would lose the old path and report blast-radius 0 RECOGNISED on a genuinely stale base (a fail-open). It is information, not a verdict — it cannot change premerge-assert.sh’s exit code, and an absent, failing, timed-out or UNMEASURED advisory is reported and non-fatal. Its 60s bound carries a SIGKILL escalation (--kill-after), because plain timeout <secs> only SIGTERMs and then waits, so a child ignoring TERM would keep the merge critical path blocked indefinitely; the runner is resolved as timeout then gtimeout (GNU coreutils on macOS) with --kill-after support PROBED, and where none exists the advisory is SKIPPED and reported, never run unbounded or behind a bound a child can ignore. Anchoring is the other half of “information, not a verdict”, so its output is ANCHORED: every line, stdout and stderr, begins with BASE-STALENESS: , every dynamic field is control-character sanitized (git permits newlines in paths, and one would otherwise emit an unprefixed line), the verdict appears only on a verdict line carrying a closed-set token, and the script’s own static template text carries none of PASS/OK/RESULT:, asserted structurally over the source. The earlier, absolute claim — “its output carries no PASS, OK or RESULT: in any run” — was FALSIFIED BY REVIEW and is recorded as changed, not softened: the advisory prints repository-controlled paths verbatim, and the tracked path test-data/scripts/CI_SMOKE_TEST_USAGE.md contains OK. Declared residual: a path may contain a reserved substring, and the anchor is what makes that harmless. Its no-finding verdict is NO-STALENESS-RECOGNISED (a scan result, never FRESH/CLEAN), M = 0 prints 0 RECOGNISED and never a bare 0, and every run declares that the blast radius is not a dependency closure. A consumer that acts on it (slice 2) MUST treat exit 5/UNMEASURED as STALE, never as fresh. The three PREMERGE: SCOPE lines are RETAINED, because the advisory does not close the gap they disclose. The sha half of the same check closes a second, different escape (PR #3616): a real gate, someone else’s. A closer located its gate run dir by recency (ls -t /tmp/agent-gate.*), read a PEER LANE’s dir, saw 33 of 37 components PASS and was about to merge #3616 on PR #3580’s verdict — everything about it was real, and only the run-id: line exposed it, read by a human. premerge-assert.sh cannot verify run-id: (it did not launch the gate, and #2874’s reader contract belongs to whoever did), so requiring BOTH commit: (7 hex) and tree-start: (12 hex) to match the certified sha is what turns a cross-lane verdict into a mechanical refusal: a peer’s summary names the OTHER PR’s head.
  • Break-glass is per-tier, and it actually works. ci:waive:<tier-id> (an owner action) excuses a tier that is absent or pending at the deadline; it can never excuse a failed or cancelled one, and there is no blanket waiver. The label takes effect without a re-run: required re-reads the PR’s current labels on every poll, and pr-gate.yml subscribes to labeled/unlabeled so applying one to an already-finished gate starts a fresh run that sees it. Each honoured waiver emits a warning annotation and a job-summary line naming the tier and the person who applied the label — resolved from the PR’s labeled events, not the actor of the run (who is usually whoever pushed or hit re-run); an unresolvable attribution says so rather than guessing. A registered tier may not cancel its in-flight run on a label event, so the hatch does not fight the tier it waives.
  • A waiver is bound to the head sha you applied it for. A label persists across pushes, so “waive an absent tier on sight” plus a leftover label would waive every later head’s tier in the seconds before it could report — a permanent bypass. The immediate waiver therefore requires the labeled event to be newer than that head sha’s first CI activity (and, for a pending tier, the run must be the one the label event itself started). Push a commit and your waiver stops short-circuiting: the tier is polled for the full deadline, and if it reports a failure in that time it reds the gate. The waiver still applies at the deadline, so it delays a verdict rather than pre-empting one. To get the instant hatch back on the new head, remove and re-apply the label — the diagnostic says so.
  • The waiver’s EVIDENCE state is always on the record (issue #3033). Both behaviours above depend on reading this PR’s labeled events, and a broken read used to look exactly like an ordinary PR: an empty feed, one vague warning, a waiver that quietly waited for the deadline with nobody named. The job summary now always states what happened — Waiver evidence: n/a (no ci:waive: label present); READ OK — feed read (N labeled event(s)), M of them for a ci:waive: label in force; the same READ OK with 0 for a waiver label in force now (the read worked and carried nothing usable, so attribution stays UNRESOLVED); or UNREADABLE (broken read …) carrying the HTTP status, or the command’s exit status when there is none. 401/403/404 means the token may not read this PR’s events — check the workflow’s permissions:; a 403 naming a rate limit or any 5xx is transient; no HTTP status means the client failed (gh absent, bad --jq) and a re-run will not help. The counts are feed total, the ci:waive: history and the subset in force precisely because the feed carries every label’s events and labeled events are immutable: an event for a label since removed is history that can bind nothing. A healthy read is evidence that the read works, never that a waiver bound — binding is a per-tier verdict against that head sha’s first CI activity. An unreadable read is never a failure of required: it withholds the early waiver and the attribution, and the waiver is still honoured at the deadline.
  • The report never claims more than it observed (issue #3033 round 4). The same defect recurred one layer out: the summary asserted absence (no ci:waive: label is present) even when the LIVE label read had failed and the labels in hand were the run-start payload — a confident false negative about a waiver applied mid-run, which is what the polling window exists for. So the label read’s trustworthiness is now tracked and reported: a fallback to the payload reports UNKNOWN (label read UNTRUSTED) with the failed-read count and status, and it makes the in-force count UNKNOWN rather than zero. A failed or ambiguous read can only ever withhold a waiver, never grant one, and none of these states can change required’s verdict — that comes from tier evaluation alone. The same discipline was then applied to every line of the block, not only the ones a reviewer named: claims about labels are phrased against “the label set this run is using” plus that set’s provenance, and a run that admits a read failed carries no denial that one did — so you never get permissions: advice beside an “authorization is not the problem” line, and the suite pins that property across the combined states rather than the wording.
  • A mistyped waiver label is named as a typo (issue #3033 round 6). A waiver label is ci:waive:<tier-id> with a LOWER-CASE tier id ([a-z0-9][a-z0-9-]*), which is what the evaluator matches. Anything else — ci:waive:Flight for tier flight — waives nothing, so the evidence read is never even attempted for it and the summary reports INVALID WAIVER LABEL — it waives NOTHING, naming the offending label and the fix. Previously such a label was reported through the UNREADABLE/UNAVAILABLE diagnostics, which sent the reader to the workflow’s permissions: block for what is a capitalisation typo. An off-shape label applied beside a valid one is still named once, under the valid label’s state.
  • Labelling is cheap. Subscribing to label events must not make every ci:perf / board-mirror / needs-decision label — or the waiver itself — restart a 30-minute gate. So a label mutation never cancels the in-flight run (cancellation is conditional on the event action; the shared concurrency group still guarantees only one run per PR can report required), and the label-triggered run skips pr-gate-core and reuses the core result already recorded for the same head sha. That reuse is fail-closed: absent, pending, failed, or skipped-on-a-non-label-event all red required, and the run’s own skipped check run cannot stand in for the real one.
  • Only GitHub Actions can satisfy a tier. A check-run name is global to the commit and anything holding checks:write can mint one, so required verifies each check run’s producer (app + an Actions run URL) fail-closed. An unverifiable run neither satisfies a tier nor shadows the genuine one, and it is named in the red.
  • Unknown never reads as pass, inside a tier either. A tier’s gate job validates its classifier’s applicability verdict: an empty or non-boolean verdict reds the tier instead of reporting “not applicable”, and a verdict that claims the tier applies while its work was skipped reds it too.
  • The check is not defined by the thing it checks. required evaluates the aggregator, its ruby modules, and .github/ci-gating-tiers.yml from the pull request’s base ref, so a PR cannot gut the aggregator or move its own tier into exempt: and go green on instructions it wrote. Practical consequences: a registry change takes effect after it merges, and renaming a registered tier’s context in the same PR needs a second PR or a ci:waive:<id> (the base registry still expects the old name). The enrolment policy still validates the HEAD tree — it is judging your change.
  • A tier’s mandate covers what reaches it at runtime. For the Flight tier that is cqlite-flight/**, cqlite-core/**, test-data/**, Cargo.toml/Cargo.lock, rust-toolchain.toml, the shared setup-rust-ci action and the workflow itself — one verdict for the whole tier, so a core-only diff runs the end-to-end tests rather than the --lib subset. A registered tier may have only ONE applicability output; two predicates behind one context is how a mandating diff silently reaches the cheap half.

Offline proofs live in scripts/tests/test_aggregate_required_tiers.sh (every check-run state, both re-run directions, self-exclusion by run identity, waiver cases) and scripts/tests/test_gating_registry_policy.sh (the enrolment rule and its wiring). Both run in the gate’s tooling-tests component and both prove non-vacuity with an always-pass stub.

The gate mirrors the enforced CI gates (.github/workflows/ci.yml, ci-minimal-features.yml) plus the local smoke suite:

ComponentCommand
fmtcargo fmt --all --check
clippyRUSTFLAGS="-D warnings" clippy, scoped per-package (issue #1844 — see below)
core-testscargo test -p cqlite-core --features cli-helpers (one test skipped — see script)
integration-testsseven named --test targets in cqlite-integration-tests
write-testscargo test -p cqlite-core --features write-support (lib + roundtrip + compaction)
cli-testscargo test -p cqlite-cli --test unit_tests
pub-surfacebash scripts/ci/check-pub-surface.sh — cqlite-core crate-root declaration-consistency guard (issue #1712): asserts an unconditional, non-#[doc(hidden)] top-level pub mod NAME; in cqlite-core/src/lib.rs is not gated by an inner #![cfg(...)] inside NAME’s own file. Answered entirely from source — the declaration’s attributes structurally from lib.rs, the module file’s PROLOGUE (rustc-verified to hold every inner attribute) — and it REFUSES rather than guess on any input it cannot classify (unrecognised pub mod shape, module file resolving to neither/both legal paths, unreadable file, block comment in a prologue, unclassifiable inner attribute). NOT public-API drift detection: no snapshot, no --regenerate — that half was removed deliberately in #1712 (five findings, one unbounded-scanner defect class); the principled route is #3366. Source-only, sub-second; SKIP-aware (loud) only on an absent cqlite-core
dep-duplicatesbash scripts/ci/check-dep-duplicates.sh — the ADVISORY duplicate-dependency ratchet (issue #1700). Measures cargo tree -d --workspace --target all (the audit’s own invocation; the BARE cargo tree -d resolves the ROOT PACKAGE only and reports a fraction of it, and without --target all cargo measures the HOST target — so a COMMITTED baseline would mean a different thing on a Linux lane than on a macOS one, and each would report a phantom advisory delta against the other’s numbers) and compares the duplicate-instance / duplicated-crate counts against the committed baseline scripts/ci/dep-duplicates-baseline.txt — a generated file with ONE documented regeneration command, bash scripts/ci/check-dep-duplicates.sh --regenerate. It emits no FAIL at all, by mandate: an increase is recorded PASS with a loud, textually distinct ADVISORY-INCREASE block naming the delta AND the crates that grew / became newly duplicated, because a legitimate new dependency can add a duplicate no local decision can collapse and [patch]/upstream-fighting pins are explicitly out of scope. It also cannot pass vacuously: PASS is keyed on the guard’s own affirmative `verdict NO-INCREASE
tooling-testsbash scripts/tests/test_agent_gate_summary.sh (SUMMARY-capture regression, #1175; SKIP-aware on missing python3)
minimal-buildcargo build -p cqlite-core --no-default-features --features all-compression
flight-testscargo test --no-fail-fast -p cqlite-flight --lib --bins — the UNIT suite only, plus a mandatory run-time census naming the 42 integration targets it does NOT run (issue #1699; descoped on measurement, #3384)
legacy-heuristicscargo build -p cqlite-core --features legacy-heuristics, then cargo test --no-fail-fast … --lib + --test targets DERIVED from cargo metadata — membership by a cfg site anywhere in the target’s module closure, or by required-features (#1699)
feature-iso-parquetcargo test -p cqlite-core --no-default-features --features all-compression,parquet --lib --no-runwithout delta-scan (#1699)
feature-iso-delta-scanthe mirror — without parquet (#1699)
all-features-checkcargo check and cargo clippy … -- -D warnings, both at -p cqlite-core --all-features --all-targets (issue #3453). The only component that enables the OpenTelemetry stack: clippy above excludes observability/observability-testing/metrics by #1844 design, core-tests runs --features cli-helpers, minimal-build runs --no-default-features — so before this lane no cargo invocation in the gate ever passed observability. Package-scoped, not --workspace (the duckdb bundled-source amalgamation belongs to cqlite-cli alone, so this stays minutes, not the #916 cost). Declares its DERIVED feature set on every run and FAILs closed if --all-features no longer enables its subject. Compiles and lints only — it executes no test, so the runtime half of the class stays pr-gate-core’s (see below). Never SKIPs
smokebash test-data/scripts/smoke-test-all-tables.sh (against a freshly built debug binary)

All components run even after a failure so one run reports everything.

Compiling a feature is not covering it. The scoped clippy matrix below enables ~30 cqlite-core features at once under --all-targets, so a feature can be test-compiled on every full gate and have executed nothing — and a combined feature set is exactly what MASKS cross-feature coupling (an item gated on feature A referencing feature B’s items compiles fine while both are on). Measured when these lanes landed: turning execution on for legacy-heuristics surfaced 4 tests that had never run once, two of which assert behaviour CQLite deliberately does not support (#3372, #3374); and flight-tests surfaced 14 cqlite-flight targets that execute nowhere — not locally, not in CI — because their module-level #![cfg(feature = "observability-testing")] is off in every lane that runs them (#3375). When you add a feature flag, ask which lane executes it, not which lane compiles it. experimental is the remaining known instance (#3373).

Why the isolation lanes use cargo test --lib --no-run, not cargo check --all-targets. The incident class they exist to catch (#1978) is an ungated #[cfg(test)] module referencing a feature-gated item, so a bare cargo check — which never compiles test targets — would be blind to it. But --all-targets overshoots in the other direction: measured, it compiles cqlite-core’s ~100 integration test files, which assume default features, and fails on storage::serialization, storage::write_engine and cqlite_core::query being configured out. That is noise, not leakage. --lib --no-run compiles the lib with its cfg(test) modules and pulls in no integration target; minimal-build is the precedent.

A narrowed lane DECLARES its narrowing, at run time, on every run. flight-tests executes the unit suite only, and prints a census naming how many integration targets it does not run (counted from cargo metadata, never hard-coded), why (their ~50% non-determinism, #3384/#3383), and who does run them (CI’s Flight tier, which required fails closed on). This is not politeness: a lane that omits coverage silently is indistinguishable from one that covers it, which is the defect this whole component set exists to remove — so a silent narrowing would reintroduce it one level down.

Derive, never curate. Both executing lanes compute their subject set from committed source at run time, so a newly added gated file is picked up and a feature joining default shrinks the excusal set, with no gate edit:

  • legacy-heuristics takes its candidate targets from cargo metadata — not a tests/*.rs glob, which cannot see a target gated only by required-features nor a directory-style tests/foo/main.rs — and includes one when a cfg site appears anywhere in that target’s module closure (mod declarations in every visibility form, plus #[path], resolved transitively). Its allowed-zero set is derived the same way: a target is excused only when NO file in its closure carries a surviving positive cfg site, and a manifest-gated target is never excused at all.
  • flight-tests derives its unit-target set (lib + every bin) from cargo metadata, so a newly added binary cannot run zero tests unnoticed — a hard-coded list beside a --bins selector is a second registry that drifts silently.

A cfg shape the census cannot evaluate — any(…), not(…), cfg_attr — is reported unclassified, never excused: a token list cannot tell a conjunction from a disjunction, and any(legacy-heuristics, X) is reachable here.

The module closure has the same class of blind spot, one level down, and it is reported the same way. A target can reach a child module through a cfg on the mod declaration itself — #[cfg(all(feature = "state_machine", feature = "cli-helpers"))] #[path = "support/datasets_root.rs"] mod datasets_root;, the shape shared test helpers actually use (3 such targets in cqlite-core today). The closure followed such a child while discarding the attribute gating it, so a legacy-gated test inside the subtree counted as executable at this lane’s feature set, an ungated sibling kept the target non-zero, and the co-required census reported no gap — the one thing that census exists to find. Such a subtree is now emitted as a DECLARED GAP naming the target, the module and the cfg text, with a cfg-gated-subtree gaps: N RECOGNISED census line that is affirmative at 0 (so a pasted census shows the scan ran) and that states its own non-exhaustiveness in the emitted text. The RECOGNISED qualifier is load-bearing rather than decorative: a bare 0 reads as a verified all-clear, and this scan is documented as incomplete, so the census must say that a clean result means nothing was recognised — never that nothing is there. The same applies to the co-required census, whose populated branch also carries the qualifier: the disclaimer belongs on the surface a reader acts on, not only in the source comments.

It is declared, not fatal, and the distinction is load-bearing: failing the lane on it was implemented, measured against the real tree, and reverted — those helpers are correct code, and a lane that reds on correct input is the lane agents learn to waive. The UNRESOLVED half stays fail-closed, because the two unknowns are not equivalent: an incomplete source set is permissive in membership, the polarity scan and the census alike, whereas an unevaluated one is merely unattributable. Anything appearing in that stream matching neither report is a FAIL — a closed grammar, since an unrecognised report is an unmeasured state and inheriting the permissive branch for it is the shape this component set exists to remove. A failed derivation is a FAIL naming the derivation, never a fallback to “nothing enabled”, which would silently excuse every gated target.

--no-fail-fast on both executing lanes. cargo test stops after the first failing test binary, and a lane whose purpose is surfacing never-executed rot must surface all of it in one run. Measured: the first run reported only P0_4_modern_format_rejection_tests; the next then reported 3 further failures the first had hidden.

A lane in --list is not a lane that works. feature-iso-parquet reports PASS (0s) warm, so presence and a green verdict prove nothing about whether it can fail. scripts/tests/test_agent_gate_feature_matrix_lanes.sh (opt-in; it performs real compiles, so it is not a default component) plants each lane’s incident-class break in a throwaway git worktree — never the live checkout, since #2926 makes a mid-run tree mutation a gate FAIL — and requires the lane to red and to stay green unbroken, so it cannot pass by failing everything. It also requires the red to name the planted symbol: a bare red is not evidence either, because an unrelated breakage produces an identical exit code and SUMMARY line. Recorded observation: docs/reports/ah6-1699-feature-matrix-lanes.md.

--workspace --all-features enables every feature on every package, which pulls in two costly artifacts on every gate run in every worktree:

  • the source-built DuckDB C++ amalgamation (cqlite-cli duckdb-tests feature), and
  • the full OpenTelemetry/OTLP stack — both the tonic and reqwest transports (observability/observability-testing on core/cli/flight/bindings).

Neither is reusable by any other gate component (-D warnings gives clippy a distinct compile fingerprint), so they were pure per-gate tax. The clippy component therefore runs a scoped per-package lint that still covers the whole workspace with -D warnings but excludes only those two feature families. parquet/arrow are NOT excluded — they are reachable in normal builds (the cli-helpers→state_machine→cqlite-core/parquet chain) and stay linted. Both the full gate and --lite use the same scoping. See run_clippy() in scripts/agent-gate.sh.

Coverage of the excluded features is moved, not deleted: set CQLITE_CLIPPY_FULL=1 to run the historical cargo clippy --workspace --all-targets --all-features -D warnings matrix. .github/workflows/gate.yml (the nightly deep-check) sets it, so a lint that only fires behind duckdb-tests or observability* is still caught within 24h. The per-package feature lists in run_clippy() can drift as features are added; that nightly full pass is the drift backstop.

The gate aborts with exit code 1 if no *-Data.db files exist under $CQLITE_DATASETS_ROOT/sstables. Fetch them first, and export the export CQLITE_DATASETS_ROOT=<abs> line the script prints — on a box that already had the variable set, the fetch populates that root, not the checkout’s (issue #3131):

Terminal window
bash test-data/scripts/fetch-datasets.sh

This prevents the failure mode where dataset-dependent tests silently pass on an empty dataset by returning 0 rows.

The fixture contract has two halves — the SSTable bytes and the committed CQL schemas that decode them — and the FULL gate now fails closed on either. The two markers are deliberately distinct text so a pasted SUMMARY separates the causes. The corpus guard runs first, so a run missing both reports #2078, the half an operator must act on.

Missing-fixtures fail-closed (issue #2078). The FULL gate FAILs CLOSED when the fetched validation corpus (test_basic/…) is absent, even though a fresh worktree’s committed byte-parity reference *-Data.db files keep the raw Data.db count > 0 (previously a false PASS via SKIP). It stamps missing-fixtures: FAIL-CLOSED (#2078) with the remedy (bash test-data/scripts/fetch-datasets.sh); AGENT_GATE_ALLOW_MISSING_FIXTURES=1 restores the lenient SKIP and stamps a visible missing-fixtures: OPT-OUT (…) line. --lite/--only are unchanged (lenient).

Missing-schemas fail-closed (issue #3148). The FULL gate also FAILs CLOSED when the committed CQL schema fixtures are unreachable, stamping missing-schemas: FAIL-CLOSED (#3148). It checks readability of the specific canonical .cql files the dataset-backed components consume, not directory existence, because a partial copy is the realistic failure. Two causes, each with its own remedy line:

  • a canonical .cql under the resolved schemas root is not a readable regular file — remedy: unset CQLITE_SCHEMAS_ROOT, or git restore --source=HEAD -- test-data/schemas;
  • CQLITE_SCHEMAS_ROOT was set to a relative path and was rejected — remedy: export an absolute path, or unset it. A relative override cannot mean the same thing on both sides: the gate resolves it against the repository root, cargo resolves it against each test binary’s package directory, so the SUMMARY would certify one schemas root while the tests read another.

On success the SUMMARY carries a positive schemas: N/N canonical .cql readable under <root> (<source>) line, so a pasted block shows the check RAN. --lite/--only stay lenient. Before #3148 the preflight validated only the corpus: a layout whose sstables/ was complete but whose schemas were unreachable passed with STATUS: OK, built for ~8 minutes, then failed core-tests + memory-budget on opaque missing-.cql panics — worse than no preflight, since the recorded “fixtures verified” pointed triage at the diff under test.

There is deliberately no opt-out for missing-schemas:, unlike #2078’s AGENT_GATE_ALLOW_MISSING_FIXTURES. The fetched corpus is legitimately absent on a fresh box; committed source in a checkout never is. An unreachable schemas root means a broken checkout or a stale override, so an escape hatch could only buy a vacuous green.

The schemas root itself is resolved checkout-relative, never as a .. sibling of $CQLITE_DATASETS_ROOT — see Test data.

Component-set skew fail-closed (issue #3544). scripts/agent-gate.sh is read from the tree under test, so a branch whose copy predates a component-set expansion on main runs the OLD script, reports a true N/N nonpass=0, and is silent about every component added since. Merge-cleanliness cannot see it (git merge-tree returns CLEAN — the skew is semantic), and required cannot backstop it: .github/ci-gating-tiers.yml exempts the CI feature-matrix lane because the local gate owns it, so each side’s coverage is justified by the other’s and the component is exercised by neither. Measured: PR #3467’s gate would have certified 31 of 35 components.

At the mode dispatch — before the #1825 slot and before any component — every mode now compares its component set against origin/main’s (never a line count, never a blob hash: a 2,000-line refactor that leaves the set alone is not a coverage problem). The branch side is the running gate’s own in-process COMPONENTS array; the baseline side is read as DATA, from the committed manifest scripts/agent-gate.components — see The baseline is data, never code below. The baseline is fetched in the same invocation as the comparison, because a remote-tracking ref is a cached observable and a stale one returns “no skew” against a superseded main. Every SUMMARY block carries a component-set: line:

  • component-set: PASS (36/36 vs origin/main <sha40> via the committed manifest) — affirmative, and it names the baseline sha and how the baseline was read: a verdict that does not name its baseline cannot be audited;

  • component-set: FAIL-CLOSED (#3544) — this tree is BEHIND origin/main <sha40>; N … MISSING … : <names> — the branch is behind (origin/main is not an ancestor of HEAD). Remedy: git fetch origin && git rebase origin/main;

  • component-set: DECLARED (#3544) — this branch REMOVES … <names> — components are missing, origin/main IS an ancestor of HEAD, and the components are absent at HEAD too, so the removal is in this branch’s own committed diff. Loud, not fatal: the author has nothing to rebase, only the information is actionable, and a guard that reds on correct input is the guard agents learn to waive. (Behind and removing fails as BEHIND first, and reaches DECLARED only after a rebase.)

  • component-set: FAIL-CLOSED (#3544) — … PRESENT in the gate script AT HEAD … — the UNCOMMITTED verdict. Ancestry alone is not provenance: “is origin/main reachable from HEAD?” is not “did this branch’s committed diff remove the component?”, and answering the first while asserting the second was a reproduced false PASS — deleting one component from the working copy alone produced a non-fatal DECLARED in a certifying mode, so a full gate would have certified 35 of 36 components under a factually false line. The provenance is therefore measured against HEAD’s own component set, not the proxy “is the tree dirty” (which would red every mid-edit branch and still prove nothing on a clean-but-stale one). An uncommitted addition still PASSes — extra components are never skew. Remedy: commit the removal, or restore the component; never rebase.

  • component-set: FAIL-CLOSED (#3544) — baseline NOT measured (<kind>: <detail>) — the fetch failed, origin is missing, origin does not NAME the canonical upstream, git is absent, neither the baseline’s manifest nor its gate script could be read, the manifest was empty or ungrammatical, the gate script’s COMPONENTS=(…) declaration could not be read as text, whether the baseline carries a manifest could not be determined at all (baseline-probe-unmeasured), HEAD’s own set is unmeasurable, or the probe could not be BOUNDED. Never a SKIP and never a fallback to an empty baseline: an empty baseline excuses every branch, which is the vacuous pass inverted. The bound is itself a named capability (timeout/gtimeout/a pure-bash watchdog/none) and an unboundable host does not run the fetch at all — a missing capability must not inherit the permissive branch, and an unbounded fetch could hang --lite on a network stall or an auth prompt.

  • component-set: FAIL-CLOSED (#3544) — the LOCAL component manifest is not usable (manifest-missing|manifest-garbage|manifest-stale)this tree’s own scripts/agent-gate.components is absent, ungrammatical, or out of step with its COMPONENTS array (order included). Remedy: regenerate and commit it — never anything to do with origin.

A component present on the branch but absent from main is not skew (this branch may be the one adding it) and is recorded as [branch-only, NOT skew: …] inside a PASS.

The baseline is data, never code (REQ-3544-01). The first design derived the baseline set by extracting origin/main:scripts/agent-gate.sh and running it (bash <fetched> --list). Six of that mechanism’s seven High-severity review findings traced to that one decision, and its three fixes each moved the hole one layer outward — a symbolic remote name, then the validated URL, then the URL in argv. That is the signature of a shared channel between data and control, where this project’s standing ruling (issue #3312) is to remove the channel rather than choose a rarer delimiter. So the baseline now comes from git show <sha>:scripts/agent-gate.components, parsed under a closed grammar: one component name per line, blank lines and # comments skipped, and anything else — including a name with leading or trailing whitespace — a named refusal (a parser that trims is a parser that guesses).

State what this converts the findings into rather than claiming they are gone: a redirected or hostile baseline now yields a wrong component list, which the comparison itself detects, instead of arbitrary code execution with the developer’s credentials. Everything built for the old mechanism is kept as defence in depth: the canonical-identity and transport/host/port/path pinning, the isolated fetch (the validated URL written into a 0600 config by a shell builtin, so it never enters argv), the verified transfer hop, the mode-dependent bound, shallow-ancestry handling and the redact-and-flatten detail path.

A check must be inside the window it certifies — not before it, not after the harm. The pre-flight ran before the concurrency-slot wait, and the post-slot tree recapture then reset the certification window: an edit made while the run was queued became the new starting tree under a stale component-set: verdict. The recapture is deliberate, so the pre-flight is repeated inside the window it opens. And HEAD’s manifest was trusted while the local one is verified against the local declaration every run — so provenance now reads HEAD’s committed COMPONENTS declaration directly and does not consult HEAD’s manifest: remove the second source rather than reconcile it.

A symlink is a blob, and a graft outlives --no-replace-objects. The presence probe accepted every blob, but a symlink is one — the difference is the mode — so the working-tree validation followed the link and saw a full manifest while git show <rev>:<path> printed the link’s target text: agent-gate.components -> fmt validated locally and published a one-component baseline. And $GIT_DIR/info/grafts rewrites parentage while --no-replace-objects does not disable it, so on the object-reuse path (where ancestry still ran in the live repository) a graft could reclassify missing components from a fatal BEHIND to a non-fatal DECLARED. Ancestry now runs in the isolated repository on both paths. The pattern worth carrying: every live-repository read preserved for speed has turned into a route.

Stop rendering the value rather than sanitising it again. The rejected-origin diagnostic was the fifth finding in one family — raw URL, then unflattened, then unredacted stderr, then scheme-only redaction, then query strings and multi-@ authorities. Every fix improved the sanitiser, and the set of places a secret can hide in a URL does not close, so the URL is no longer published at all: the line names the axis the origin was rejected on, plus the normalised identity only when that identity is itself grammatically clean. Note the layer distinction that cost a regression on the way: the normalised value is a comparison key, not a diagnostic string — reducing it made every local path compare equal.

An allowlist has to reach the sites a later change adds. The migrated object reads ran under a bare env, inheriting the caller’s environment — the same hole as round 13, re-opened at the new sites. Every git call in the pre-flight now runs under env -i plus the one allowlist, including the state probes (injected config could have made a real partial clone look non-partial and re-opened the fast path). Two corrections came with it: a config file does not keep a URL out of every argv — git passes the configured URL to a transport helper, whose command line then carries the token — so a credential-bearing origin is refused (userinfo must be absent or exactly git); and a specified control must be required to have worked — the chmod 600 on the isolated config is fail-closed with the resulting mode verified.

A local read can be a network operation. In a partial clone, ls-tree/show/cat-file answer a missing object by fetching it from the promisor remote — under the live repository’s local config, so an insteadOf plus an enabled external protocol executes a remote helper, and the lazy fetch also writes objects into the shared store. That was the third route of one family, so every baseline and HEAD object read, and the ancestry walk, now run inside the isolated scratch repository, with the lane’s object directory supplied as an alternate — pure object storage, carrying no config and therefore no promisor and nothing for a helper to be invoked from. Ancestry compares against HEAD resolved to a sha in the checkout, because inside the scratch the ref HEAD would mean the scratch’s own unborn HEAD. The fast path is gated on the clone not being partial, and GIT_NO_LAZY_FETCH=1 is carried as a belt rather than the control (git ≥ 2.36; an unset variable does nothing silently).

Untrusted repository state is bigger than config. Closing git’s config sources and treating “untrusted repository state” as closed with them left three holes. Replacement refs: refs/replace/<sha> transparently substitutes another commit, so the pre-flight reported the canonical sha while reading a forged, smaller manifest — and passed. GIT_NO_REPLACE_OBJECTS=1 plus --no-replace-objects on every lane-local object read closes it. The transfer hop could execute: git fetch in the live repository reads its local config — only the environment is sanitisable, a .git/config is a file — so a local url.*.insteadOf with protocol.ext.allow=always rewrote the scratch path to an ext:: remote helper and ran commands during the fetch, before the sha comparison that was supposed to make the hop “untrusted but safe”. A check placed after a harmful effect can only report it, never prevent it — so where the harm is execution, the control must be that the execution cannot be reached. The test asserts unreachability, with a positive control proving the attack does execute in a plain repository. There is therefore no import: the scratch object store is exposed through GIT_ALTERNATE_OBJECT_DIRECTORIES (an object source, not a transport), and nothing is written into the shared .git — no pack, no ref, no FETCH_HEAD. That is safe for the reason the transport was not: every read is by a sha whose provenance is the isolated chain, and git objects are content-addressed. A leaked scp-form credential, the third instance of one family, is fixed by narrowing what is accepted (scp userinfo must be exactly git) rather than widening the scrubber a third time. And cleanup now runs on signals (INT/TERM/HUP), because bash runs no EXIT trap for a signal that still has its default disposition — the second axis of “cleanup registration precedes resource creation”.

The isolated hop’s environment is an allowlist, and objects are fetched only when absent. Neutralising GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM and stopping there left the hop inheriting GIT_CONFIG_COUNT/KEY_*/VALUE_*, GIT_CONFIG_PARAMETERS and GIT_TEMPLATE_DIR — all three measured to redirect a fetch through url.<attacker>.insteadOf (the template by seeding the new repository’s own local config, which global/system neutralisation cannot touch). A redirect of the isolated hop is worse than one of the transfer: both observations then come from the attacker, so the equality assert compares two values that agree and emits a false PASS. Every isolated git call therefore runs under env -i plus an allowlist — admit what git needs to reach and authenticate to the remote (each entry with its reason), clear everything that can change what it fetches or what it runs — so a new git environment variable is cleared by default instead of having to be discovered. Lane-local reads are deliberately not wrapped: the only value needing provenance is the sha, and everything addressed by it is content-addressed.

The sha itself comes from a ref oracle (git ls-remote, no objects), and objects are fetched only when this repository does not already hold that commit. Measured on one fleet box: a plain fetch into the fresh scratch repository cost 3.74 s and 92 MB of full history on every gate invocation; the oracle costs 0.29 s and 120 KB, and the whole pre-flight went 3.9 s → 0.51 s. --filter=blob:none (0.73 s, 6.4 MB) was rejected on measurement: the manifest and gate blobs are read in this repository at the baseline sha, so a blob-filtered transfer leaves them absent exactly when main has changed them — a correct tree failing. “Fetched in this invocation” is about the staleness of the ref value, which is still read live; and the oracle’s output is remote-controlled text, so it is validated (object id of a known length plus the ref name that was asked for) rather than parsed — baseline-ref-unparsable otherwise.

The local manifest is asserted against the running array on every run, fail-closed, before anything is fetched. That assert is what makes a manifest baseline trustworthy at all: without it the file is an unverified claim, and a branch that grew COMPONENTS without regenerating the manifest would — once merged — leave main’s manifest short, so every later branch would compare against a too-small baseline and silently excuse real skew. Regenerate with:

Terminal window
{ sed -n -e '/^[^#]/q' -e p scripts/agent-gate.components; scripts/agent-gate.sh --list; } \
> /tmp/agent-gate.components && mv /tmp/agent-gate.components scripts/agent-gate.components

One transitional fallback, also data-only — and it is unreachable by assertion, not by reasoning. The baseline’s tree is probed first, as its own step, with three outcomes:

probe resultbehaviour
presentthe manifest and nothing else. Every failure of that read — unreadable, bound exceeded, ungrammatical, empty — is an error; the textual path is a hard refusal here.
verified-absentthe transitional text extraction, and the component-set: line names it.
could-not-tellREFUSE (baseline-probe-unmeasured). Not the fallback.

“The fallback is self-limiting — unreachable once the manifest is on main” was true and not enough: that is a property somebody reasoned about and nothing measured, so a refactor, a baseline pointed at an older commit, or an accidentally deleted manifest would silently re-enable the brittle path — a pass derived from the absence of a bad signal. And git show cannot answer the question: its non-zero exit conflates “no such path” with “bad object” with “the repository could not be read”, which is the two-valued-predicate error (a predicate that must collapse “cannot tell” onto one of its answers always picks the permissive one). git ls-tree <rev> -- <path> separates them affirmatively — rc 0 with an entry, rc 0 with no entry, rc ≠ 0 — and an entry that is not a blob is its own refusal. The payoff is mechanical expiry instead of trust: once the manifest is on main, every later baseline measures present, so path 2 is dead code that any attempt to enter errors, at the cost of one extra bounded probe.

When it does run, the extractor reads the gate script’s single-line top-level COMPONENTS=(…) declaration as text — never executed — and refuses loudly on any shape it does not recognise, naming it (“is not a SINGLE-LINE literal”; a multi-line or computed array, more than one declaration, a character outside the name grammar). That refusal branch is itself tested with a reflowed array, because an untested refusal on the exact axis known to be brittle is not coverage. It is format-brittle in a shared direction — a reflow on main refuses for every branch at once, which is fail-closed rather than a false green, and the named diagnostic turns a fleet-wide stop into a five-minute fix. Every baseline-bearing verdict line ends by naming its baseline source (— baseline read via the committed manifest / — baseline read via the TEXTUAL FALLBACK: … VERIFIED ABSENT at that sha), so use of the fallback is visible rather than inferred.

And it ends by naming the OBJECT provenance too, because the shared object store is TRUSTED, not verified per-read (; objects: baseline REUSED from this lane's SHARED store / baseline FETCHED from the canonical remote, HEAD's own from this lane's SHARED store / provenance NOT RECORDED — each followed by store TRUSTED, not verified (#3749)). Git does not rehash a packed object against the id it was asked for on an ordinary read, and on this fleet every lane on a box is a worktree of one shared .git, so a planted pack/index can make a canonical sha resolve to a shortened manifest — a false PASS. What an ordinary read does verify is the pack CRC and the zlib stream, which catch accidental damage (bit rot, a torn or truncated write) but never rehash content against its own name.

#3749’s owner ruling split that into two subjects, and overturned the earlier framing. This page used to call a peer lane planting objects “a non-invoker route, hence a defect”; that is retracted. Deliberate forgery by a same-host peer is invoker-class and out of model — the #3312 triage rule already says same-host actors able to write these scripts are invoker-class, and a peer wanting a false PASS can simply edit scripts/agent-gate.sh, which is cheaper than forging pack data; no check inside a process defends against the party that controls the process. Accidental corruption is in model, and its control is a periodic full-rehash sweep: scripts/check-object-store-integrity.sh runs a full git fsck (never --connectivity-only, which does not rehash content) over the shared store and emits an anchored four-valued VERIFIED/CORRUPT/UNSWEEPABLE/UNMEASURED verdict — exit 0/4/6/5, and a consumer must not read UNMEASURED as clean. It runs at machine onboarding (bootstrap-agent-machine.sh section 5d, where VERIFIED is the only [ok], so an unmeasured host cannot pass --strict) and on the worker supervisor’s throttled per-iteration cadence (default every 6h). There, CORRUPT stops the supervisor loudly instead of holding — corruption is non-self-clearing, so a hold-and-repoll loop would spin to the wall-clock budget taking no useful action — and so does UNSWEEPABLE (below), while UNMEASURED is journalled and paged once and deliberately does not stop the loop, because refusing to run any worker over a hygiene probe that could not run is a self-DoS. That sweep is periodic, not per-read, which is why the emitted clause still says TRUSTED, not verified.

The fatal verdict is affirmative, and it had to be made so. A git fsck over a store up to eight peer lanes are concurrently writing prints error: lines on a healthy store (measured on this fleet: invalid reflog entry naming a different branch each run, on a quarter to a half of all runs), so recognising damage from the text shape of a diagnostic made a healthy box page high, stop its supervisor and fail --strict bootstrap. The class comes from fsck’s exit bitmask — bits 1 ERROR_OBJECT/4 ERROR_PACK are the subject; 2 ERROR_REACHABLE/8 ERROR_REFS/ 16 ERROR_COMMIT_GRAPH/32 ERROR_MULTI_PACK_INDEX are not demoted to clean but get their own non-passing UNMEASURED cause. Except that 2 ERROR_REACHABLE has two causes, and routing both to UNMEASURED was a false negative on real corruption. That bit fires for a stale reflog entry naming an object a peer’s gc pruned (routine, not the subject) and for an object genuinely absent while a live ref, the index or HEAD still needs it (corruption). Both reproduce, so the reproduction discriminator cannot separate them, and UNMEASURED is deliberately non-fatal to the supervisor’s loop — so workers kept running against a demonstrably damaged store, reported as “not measured”. They are separated by a third walk with --no-reflogs, which drops the reflogs from the reachability roots and keeps everything else: a complaint that survives it is reachable from a live root and is CORRUPT, with a remedy that says the reflog remedy does not apply; one that clears is REFLOG-SCOPED and stays UNMEASURED. Measured on git 2.43.0 on real fixtures, both directions: a blob deleted while HEAD’s tree names it exits 2 with and without reflogs; a commit deleted after reset --hard, named only by the reflog, exits 2 with and 0 without. This is not the --no-reflogs suppression rejected earlier — that decided whether to report and was measured not to help; this decides which cause, on a class that has already reproduced twice, and can only make the verdict stronger. Passes 1 and 2 never carry the flag (asserted structurally over the shipped call sites), an attribution walk that is killed, unlaunchable or unclassifiable leaves the complaint UNATTRIBUTED and non-passing, and a damage bit appearing only in the third walk has not reproduced across the sweep walks. The verdict set stays closed at three: the split is in the cause text, not in a fourth token two readers and a structural assert would have to learn. And “the roots the walk has” was itself an unchecked assumption, wrong in both directions. A review finding held that --git-dir=<common> discards linked worktrees’ private administrative context, so a missing object needed only by a lane’s private HEAD or index would be overlooked. Measured on git 2.43.0, that is false: a common-dir fsck does walk every registered worktree’s private HEAD, its private index, and the HEAD of a prunable worktree — all three surviving --no-reflogs, hence already CORRUPT. What it does not walk is a linked worktree’s per-worktree refs (refs/worktree/*, refs/bisect/*, refs/rewritten/*; the main worktree’s live in the common dir and are walked): delete an object named only by one and the sweep exits 0 VERIFIED, because the HEAD reflog echoes the id and that echo clears under --no-reflogs. The covered roots are pinned by fixtures; the hole is a DECLARED GAP, and the probe built to close it was removed. Three measurements rule out the fsck-shaped fixes and are recorded so nobody re-derives them: git fsck <sha> replaces the default heads (so private roots can never be appended to the sweep walk), an explicit-head fsck still scans the whole object directory and so costs a full rehash, and rev-list <missing-root> dies 128 (so --missing=print cannot answer the case that fires). What was built instead was an O(refs) question — enumerate each linked worktree’s refs, subtract the common ones, ask cat-file --batch-check whether the remainder’s targets are present — and its first review returned three false-VERIFIED routes of its own, two of them High: a present root whose reachable child is missing is invisible to a target-presence check; a per-worktree ref is discarded by the name subtraction when a common ref shares its name, even pointing at a different, missing object; and a failed awk/sort/comm degrades to a zero-root census and then permits VERIFIED. A mechanism added to prevent one false clean produced three new ways to reach one, in one review — so #3229’s ruling (a guard with known documented false-PASSes is worse than no guard), its companion (subtraction cannot introduce a false PASS) and #3544’s own posture (a check that claims nothing false is worth more than one claiming a closure it does not deliver) all point the same way. The class also has zero instances on this fleet, measured twice independently on 2026-09-02: 14 registered worktrees, all three namespaces absent from every linked worktree’s admin dir and from the common dir, 0 mentions in packed-refs — live per-worktree refs would have made removal leave a live hole, and the right answer would then have been to fix the three findings. The declaration is emitted on every run: declared-gap lines, on all three verdict classes (what a run did not walk does not depend on what it found), in 1 RECOGNISED form rather than a bare count, naming the un-walked namespaces, the coverage that is not in the gap (the measurement above, which is what keeps the gap one namespace wide) and the fleet measurement with its date, since “zero instances” expires. One measurement is kept for whoever revisits this: git worktree list --porcelain is fail-open — it silently drops a worktree whose admin gitdir file is missing (rc 0, no diagnostic) — so any future enumeration must be filesystem-first over $GIT_COMMON_DIR/worktrees/*, git’s own administrative directory, with the command only as a cross-check in the direction it can fail. The mask does not end at 31, and assuming it did dropped real damage. A range check over 1..31 — reasoned from 128 being die() and 127 & 1 being 1 — classified 33 (32|1) and 36 (32|4) as unclassified and therefore UNMEASURED, a false negative on genuine object corruption. Measured on git 2.43.0: a truncated multi-pack-index exits 32, and that store with one corrupted blob exits 35. So the damage bits are tested independently, and first, and an unrelated bit can never mask them; only a status at or above 124 (timeout/shell conventions, die() and signal deaths above them) is refused bit-testing outright, and a status carrying a bit outside the supported mask is unclassified rather than folded into a class whose remedy would be wrong — which is how it degrades safely as git adds bits.

And that range has two halves, which sharing one verdict made a false negative on real commit-object corruption. Measured on git 2.43.0: overwrite the loose object a live ref points at with unparseable bytes and fsck prints fatal: loose object <sha> … is corrupt and exits 128, while the same damage to a blob — or a valid object stored under the commit’s name — exits 3 (2|1) and is caught. So the exact damage this control exists for lands outside the mask, where unclassifiedUNMEASURED ⇒ the supervisor writes a fresh throttle stamp and keeps spawning workers. 124..127 is the timeout/exec convention — the fsck never ran, a fact about the box’s tooling, and it stays permissive; 128+ is git’s own die() or a signal death — it ran and did not finish, and reproduced on both walks it is the stopping verdict UNSWEEPABLE (exit 6). The boundary is argued from the exec chain rather than assumed: env/nice/timeout each report their own failures inside 125..127, so a status at or above 128 is the innermost command having actually started, which together with the launch marker below makes “fsck ran and died” an affirmative observation. On that cause UNSWEEPABLE claims no cause and prints no repair (scoped to the fatal cause: the verdict gained a second one, below, which does name its cause) — the alternative was to read the fatal: text for a damage signature, i.e. the text-shape classifier again, narrower, with every wording nobody enumerated falling back to the same permissive state. It tells the operator to run the walk by hand and act on what the fatal: line names (that line is kept in the findings for display only; it reaches no branch), and it makes a completed sweep the condition for resuming. The verdict set is closed at four, and the closure is over dispositions, not over a count: a token is what every consumer keys its behaviour on, so a state that stops the box cannot be expressed as another verdict’s cause text, while two causes that both continue (the reflog split above) must be.

UNSWEEPABLE has a second cause: the store’s own fsck.* config, which the environment allowlist cannot reach. env -i plus the allowlist closes git’s environment config sources; a local config is a file in the repository, not an environment variable, and git fsck is configurable. Measured on git 2.43.0 against a real fixture (a commit object with no author email — an ERROR-severity message, exit bit 1, the sweep’s own damage class): plain fsck exits 1, while both fsck.<msg-id>=ignore and a fsck.skipList naming that object exit 0 — so the sweep reported VERIFIED about a damaged store, the exact false affirmative this control exists to prevent. It is in model because the file is the shared .git/config a peer lane, or an accident, writes. It is refused, not overridden: fsck.<msg-id> is an open-ended key space (one key per message id, new ids with new git versions), so a -c per key is a list that is wrong the moment git adds a message, and a partial override is the “one axis closed, space declared done” shape this issue has hit five times. So any fsck.* key in the configuration the walk would read means no walk at all and UNSWEEPABLE, naming the keys and the config --list --show-origin --name-only that lists them, and claiming no damage — nothing was rehashed. It stops rather than reporting, because the policy is a persistent property of the repository and because an fsck.<msg-id> naming an id this git does not know already stops the box through the fatal cause, so the permissive reading would stop a box whose config merely breaks fsck while letting one that suppresses real damage carry on. “Could not ask” stays permissive and is its own state: a failed probe, or an rc-0 answer listing no key at all (an unread policy, not an empty one — every repository’s config declares core.repositoryformatversion), is UNMEASURED with its own cause and no walk, so VERIFIED is reachable only from a policy that was read and found empty. And both consumers’ text is now derived from the token alone — the latch records only the verdict, so a reader naming the fatal mechanism was affirmatively false on the other cause.

And a status is only a bitmask if the command that produced it actually ran. fsck’s status space is shared with the shell’s, so the classifier had a precondition it never checked: the two capture redirections are part of the fsck command, and a failure to open the scratch output file (a full or reaped TMPDIR) means bash execs nothing and exits 1ERROR_OBJECT. Both passes then fail identically, both “reproduce”, and the sweep emitted CORRUPT about a store it never opened — the same false-CORRUPT harm as the text-shape classifier, arriving through the shell instead of through a concurrent writer. It cannot be inferred from the status, which is the point; the evidence is affirmative — a marker written inside the redirected group as its first statement (a redirection failure on a compound command means the body does not execute at all) plus both capture files existing afterwards — and its absence is a launchfail class routed to UNMEASURED, never bit-tested. Generalise it: before reading a status as a structured value, establish that the process whose convention you are applying is the process that produced it.

And no non-clean walk is fatal on one observation: it is re-run once as a discriminator — a concurrency artefact does not survive a second independent walk, damage does — never a retry-until-clean loop (the reachability attribution above is a third walk answering a different question; MAX_SWEEP_WALKS in the sweep script is the declared ceiling, and every caller’s bound is derived from it). The two channels must also agree before the fatal verdict is acted on. Both consumers read the exit status and the anchored verdict line, and both tested them with || while the comment above each asserted the conjunction — so an exit 4 with no verdict line, or a stray CORRUPT line under any other status, was enough to create a sticky, box-wide, operator-cleared latch that halts every lane. Blast radius decides the direction: a false latch stops four lanes until a human notices, while one more iteration over a genuinely damaged store costs the next sweep, which reproduces it and latches it properly. A disagreement is therefore routed to UNMEASURED and named (INCONSISTENT sweep result), never folded into the generic “could not measure” line; the UNMEASURED tests stay disjunctive on purpose, because a disjunction that can only reach a non-passing branch cannot manufacture a verdict. The CORRUPT verdict is persisted for the box in its own create-only file (<stamp>.STOP, which records which stopping verdict it is, read affirmatively — it was .CORRUPT until a second stopping verdict existed, and a file whose name asserts damage is a confidently-wrong claim on disk for a verdict that establishes no cause; an unrecognised second line is a cause-free stop and is never defaulted to the damage text), because a timestamp-only stamp let the detecting lane stop while its peers skipped their own sweep for the interval and kept spawning workers over the damaged store. Putting the verdict in that stamp was the first fix and was itself a defect: stamp writes are unsynchronised overwrites, so a lane whose sweep started before the detection could finish after it and replace CORRUPT with VERIFIED. Two consecutive review rounds found a defect in that one shared mutable cell, so the channel is removed rather than serialised — the latch’s only transition is ABSENT → PRESENT, created under set -C so the kernel arbitrates and a losing writer cannot overwrite the winner, while the timestamp stays freely overwritable because moving a timestamp backwards costs one extra sweep. A lock was considered and rejected: it makes the read-modify-write atomic but leaves a value any writer can move backwards, so “CORRUPT is sticky” would rest on every future writer remembering to honour it. The latch does not expire and is cleared by hand (rm -f <latch>, named in every message that mentions it); a create that could not happen is reported, never read as a latched box. The stamp’s key is resolved by the sweep script itself (--print-store), never by a git call in the supervisor — a bare git rev-parse --git-common-dir inherits the caller’s environment, so an inherited GIT_DIR keyed the stamp on another repository — and it is a digest of that canonical path, because flattening is not injective: replacing / and every unsupported character with _ maps /tmp/a/b/objects and /tmp/a_b/objects to one name, so two repositories on a box shared a throttle stamp and a CORRUPT latch. The form is <sanitised tail>.<16 hex of sha256>, where the tail is readability only and carries no identity. The digest is taken from the RAW path, in the resolver — digesting the RENDERED value was itself the next defect (round 5). Round 4 made the key injective over the flattening and then fed the digest the value --print-store had already passed through the sweep script’s sane() escaper, which is a display encoding and a lossy one: a path holding a real newline and one holding the two literal characters \n produced ONE key, which is the same shared-stamp-and-latch harm arriving through the fix for it. So store_key lives in the sweep script — the only process holding the raw bytes — and publishes a second anchored store-key line that the caller validates and uses verbatim, while the supervisor’s own digest helper is gone, so no lossy value is left for a future caller to digest. The transferable rule: a value sanitised for DISPLAY is not an IDENTITY; derive a comparison key before any rendering, and where both are needed publish two fields rather than deriving one from the other. Three digest tools (sha256sum, shasum -a 256, openssl dgst -sha256) cover both platforms, the output is parsed from both ends and then validated as hex, and a host with none fails closed to the empty key — no throttle, no latch, announced once naming both causes — never a silent fall back to the colliding form. cksum is present everywhere and deliberately unused: CRC32 is not collision-resistant, and a colliding key is the defect being removed.

The latch is created before the throttle stamp, re-read before every return that leads to a spawn, and the remaining race is declared rather than closed. The stamp used to be written for every outcome before the CORRUPT branch ran, so between those two writes a peer read a fresh non-corrupt stamp and throttled past a corruption about to be recorded; and a lane whose own sweep said VERIFIED returned toward a spawn without re-reading the latch, so a peer latching the box during that lane’s two fsck walks was never seen. Both are ordering fixes, neither needs a lock: latch first, stamp second, and one obj_sweep_stop_if_latched helper before the throttled, VERIFIED and UNMEASURED returns alike — one rule instead of a set of paths to audit. And ordering was not enough: the stamp is written only if the latch is CONFIRMED present, else it is forced stale (round 9, item 1). “Latch first, stamp second” left the stamp unconditional, so on the one branch where the latch could not be persisted — a writable stamp inside a directory that is not writable, so creating <stamp>.STOP fails while rewriting <stamp> succeeds — the detecting lane stopped (correct) and still advertised a freshly swept box (not correct): its peers read the fresh stamp, skipped their own sweep for the whole interval, and kept working against a store already confirmed damaged. The stamp write is now gated on obj_sweep_latch_present, the afterwards-existence check and never the create’s exit status (which cannot tell “a peer got there first” from “the directory is unwritable”); where the latch is missing the stamp is forced to epoch 0, because a peer whose sweep started earlier can finish during this one and write a fresh timestamp. Forcing can only cause MORE sweeping, so it cannot manufacture a false clean.

And the printed remedy has to work, which it did not (round 9, item 2). All three copies of the repair instruction said git fetch --force origin, which repairs nothing: --force only permits non-fast-forward ref updates and re-downloads no objects, so with the advertised tips unchanged the negotiation can transfer nothing — an operator following it exactly kept the corruption and gained the impression of a repair. Measured on git 2.43.0 against planted damage, by the sweep’s own verdict: --force leaves a corrupt loose object, a flipped pack byte and a missing object untouched; git fetch --refetch origin (git 2.36+) restores a MISSING object but not damaged content, whose bytes stay in the object directory where fsck still finds them — so content damage needs the pack or loose object deleted first, after which --refetch verifies clean. A fresh clone works, but only from the canonical remote over the network: git clone <local path> hardlinks the object files, so a clone of the damaged repository is damaged too. Hence the sweep owns the text (it knows the damage class, and the measurements live beside it) and both consumers QUOTE its verdict-detail lines instead of paraphrasing, with a fail-closed fallback so a stopped box is never left with no remedy — and clearing the latch is gated on a re-run of the sweep reporting its affirmative verdict.

That rule was still a set of SITES, and round 5 found the fourth — so the read is now at ENTRY, above every branch. The documented opt-out OBJ_SWEEP_INTERVAL_HOURS=0 returned before any latch read, so switching the sweep off also switched off an already-recorded CORRUPT verdict. Patching the fourth site would have left a fifth, because the design required every early return to remember the check; the read is therefore hoisted to the top of object_store_sweep, whose prologue is now declarations and assignments only, and the invariant is a property of the FUNCTION (“it cannot be entered without a latch read”) that holds for branches nobody has written yet — asserted structurally. The post-sweep reads stay: a peer can latch the box while this lane spends up to MAX_SWEEP_WALKS walks. Cost: one --print-store resolution per iteration (measured 5 ms) on a path that previously paid none. The latch question is also four-valued, because [[ -e ]] is false both for an absent latch and for one the process cannot look at: unknown STOPS under its own reason (object-store-latch-unreadable, never as CORRUPT — nothing observed damage), and absent requires the absence to be established through searchable ancestors-d on the holding directory is itself two-valued (false both for a directory that does not exist and for one whose ancestor cannot be searched), so the probe walks up to the deepest ancestor it can stat and counts the absence only when that one is searchable, and unkeyed is the one permissive answer, announced once, because no key means no latch was ever named and stopping would make a missing sweep script halt every lane for want of a hygiene probe.

The thundering herd is closed, with the hazard its fix introduces. The throttle read and the sweep invocation were unsynchronised and the stamp is written at the END of a sweep, so when the interval expired every lane read the same stale stamp and started its own full-store fsck — not merely a CPU cost: the walks are I/O-bound, so contention pushes them toward the per-walk bound, and an expired bound is UNMEASURED, a correlated loss of the measurement on every lane at once. The serialiser is a per-store claim directory created with mkdir (the latch’s kernel-arbitrated create-only primitive, and deliberately not flock, since this file’s single-instance lock is mkdir-based precisely because macOS ships no flock(1)). A loser does wait, which reverses the original design. It used to skip that iteration’s probe and carry straight on to the spawn path after one latch read, on the argument that “a peer is doing it right now” is a complete answer for a 6-hourly hygiene sweep — a complete answer about the SWEEP and no answer at all about the SPAWN, because the loser knows a full-store fsck is in flight (that is why its claim failed) and that fsck can end in CORRUPT or UNSWEEPABLE, latching the box. The window it discarded was the peer’s entire sweep: 13-80s measured on this fleet, up to MAX_SWEEP_WALKS × OBJ_SWEEP_TIMEOUT_SECS (600s at the shipped defaults) by construction — not the narrow post-read spawn gap the residual note described. The loser now waits, re-reading the latch, the stamp, the claim and the stop file every OBJ_SWEEP_CLAIM_POLL_SECS (granularity, not a bound), bounded by the claim’s own derived staleness value: the same 660s that says “this claim may be taken over” says “stop waiting for it”, so a dead peer cannot wedge the wait and there is no second, driftable constant. A timeout is not a clean result and does not carry on — when the peer neither finishes nor vacates, that claim is stale by construction at the deadline, so the next acquire recovers it and the loser becomes the sweeper. And a completed peer ends the wait — it does not send the loser back to contend. The wait’s completed (the throttle stamp went fresh) was treated as “go round and acquire again”, and it is reached without sleeping, on the pass that observes the stamp — so a peer that writes the stamp and then dies before releasing leaves the claim present and the stamp fresh, every acquire answers held, every wait answers completed in microseconds, and the lane spun CPU-hot until the supervisor’s whole MAX_HOURS budget expired (measured: 2165 completions in 20s). The age-based recovery built for exactly that dead peer could not save it, which is the transferable part: completed was tested before the claim’s deadline on every pass, so the recovery the design relies on never ran. The fix adds no second notion of “the peer is gone” — a fresh stamp is the same answer the throttle at the top of the sweep gives, so the lane skips exactly as it would have there, which is strictly less work than the acquire-and-release round trip the ordinary case used to make. The loop is now bounded in wall time by construction and by a value it already derived (exhausted and completed break; neither surviving state is required to have slept, so the loop itself is bounded by the iteration’s own claim_stale budget), never by MAX_HOURS. The general rule: a state a loop can reach without sleeping must not be a loop-continuing state, and a bound placed after a fast-path test is not a bound at all. Otherwise, only where peers keep handing the claim around for the whole budget does the lane skip, journalled and paged as NOT SWEPT AND NOT MEASURED, never as clean, and never as a stop (a peer sweeping is also the shape of a healthy box recovering a wedged claim, and stopping four lanes over a hygiene probe’s contention is the self-DoS that file refuses everywhere else). The throttle is re-read after acquiring, which is what stops the claim converting a herd into a queue of redundant sweeps. A stale claim must not wedge the box, which would be worse than the herd, so the recovery threshold is derived: a claim cannot be stale while the sweep it represents could still be running, so it is the sweep’s own MAX_SWEEP_WALKS (read from that script) × this supervisor’s per-walk bound + slack = 3 × 200 + 60 = 660s; a claim with no parseable start time reads as stale on purpose (its cause is a lane killed between the mkdir and the write, and the other reading wedges the sweep forever), and where the bound cannot be derived no claim is taken at all and the previous behaviour is restored, announced. The claim is a registered resource released by the EXIT trap, so the CORRUPT path — which ends the process — does not leave peers waiting out the bound. Not closed, and pre-existing: this file installs no INT/TERM handlers, so a signalled supervisor releases neither its claim nor its lock; for the claim that is a delay, not a wedge. What is not closed, per path and with its real magnitude — a residual whose size is understated is worse than one described honestly, and a residual left standing when it could be closed is the other half of that. Path 1, the claim loser: closed — it was the peer’s whole sweep (13-80s measured, 600s by construction), now bounded by one OBJ_SWEEP_CLAIM_POLL_SECS. Path 2, a lane’s own sweep: covered by the entry read plus the post-sweep reads. Path 3, the last latch read to the worker spawn: narrowed from up to an hour to one pre-spawn prologue, and it is not zero. The hold loop used to run after the last latch read and never re-read it, so at the shipped defaults a lane could sit for BUILD_HOLD_MAX × HOLD_POLL_SECS = 12 × 300s = 3600s (leftover-worker family: LEFTOVER_HOLD_MAX × HOLD_POLL_SECS = 900s) and then spawn onto a box a peer had latched meanwhile. preflight_wait is now a wrapper: the loop lives in preflight_wait_holds and the STOP latch is re-read on every return out of it, so the hold window is closed structurally — a property of the function boundary, not an enumerated set of returns (round 3 fixed three such returns and round 5 found a fourth; that shape does not converge). What remains is run_iteration’s pre-spawn prologue: stamp_claim, i.e. one claim-heartbeat.sh stamp ref push to origin, around an rm/mkdir/assignments — a measured lower bound of 0.26-0.28s for a bare git ls-remote origin HEAD on this fleet, unbounded in principle since the supervisor puts no timeout on that push, and not the “milliseconds” a purely local prologue would give. It is bounded in harm (that lane’s next iteration reads the latch at the top of its sweep and stops, and nothing certifies a merge in that window without a full gate, which a latched box refuses), and closing it entirely still needs per-store synchronisation shared by the sweep and the spawn decision, which is split to its own issue. Path 1 and the hold window are closed; the pre-spawn window is open; the race is not gone. The sweep is also not interruptible — its two walks run in a child process — so the supervisor checks the stop file and the wall-clock budget immediately before the sweep, and bounds the exposure in wall time: its per-walk default is the sweep script’s own bound divided by MAX_SWEEP_WALKS (200 vs 600/3), so every walk it can pay for still costs no more than one walk at the script’s bound. The walk count is read from the shipped script, not re-typed in the test: the relation was asserted with a hard-coded factor 2, so adding the third walk would have kept it green while the supervisor’s real worst case rose to 900s — a relation whose own constant is restated is two magic numbers wearing a relation’s clothes. Raising a bound, or the walk count, is a change to somebody’s stop latency.

Three alternatives were rejected by the same ruling, recorded so they are not re-derived: per-lane full clones (a permanent multi-GB tax for an out-of-model threat); per-read rehashing (the fourth carve into one pre-flight, charged to every --lite round); and removing the reuse optimisation — whose original argument still stands, because removal does not close it. The ancestry walk and the provenance leg read HEAD’s committed content, which has no source other than that store — the working tree cannot substitute, since the UNCOMMITTED verdict exists precisely to compare against what is committed — so a forged HEAD object still turns UNCOMMITTED (fatal) into DECLARED (non-fatal) after removal, while charging every --lite round for a half-closure (measured 3.41 s / 93 MB full, 3.58 s / 45 MB at --depth=1; shallow is not cheaper — it still ships the tip’s whole tree). A check that claims nothing false is worth more than one claiming a closure it does not deliver.

The baseline’s identity is validated before the fetch. Trusting a remote merely named origin made git remote set-url origin <anything> a git-config-shaped opt-out — and it fires by accident in the fork workflow, where origin legitimately names a contributor’s fork whose main may be months stale, so the guard compares against the wrong baseline and stamps a PASS. origin must therefore name the canonical upstream host includedgithub.com/pmcfadin/cqlite, one hard-coded literal, matched by exact equality after normalising the spellings git accepts for that one repository (https://, http://, git://, ssh://, scp-like git@host:owner/repo, optional userinfo, an ssh:// port, an optional www., an optional .git, any case).

Owner/repo alone is not enough, and “err toward accepting an ambiguous host” was wrong here. It accepted https://evil.example/pmcfadin/cqlite and — needing no hostile host at all — any local path ending in those two segments. While the pre-flight still ran the baseline’s copy of the gate that admitted arbitrary code and not merely a wrong baseline; under REQ-3544-01 what it buys is a baseline of unknown provenance, from which no PASS may be derived, so the check stays exactly as strict — as defence in depth. Anything unverifiable from the string (an ssh config alias, a mirror, a bare local path, file://, a look-alike host such as github.com.evil.tld) is a named non-PASS (remote-not-canonical), as is an origin with no URL (remote-unreadable) — each with its own remedy, never a silent pass and never a SKIP.

The grammar is closed axis by axis — transport, userinfo, host, port, path — each with one stated rule, because three successive rounds were “too permissive” in a new place (no host; host but no transport; http:///git:// accepted). https:// is now the ONLY accepted transport. http:// and git:// authenticate nothing, so an on-path impersonator of the hostname supplies the objects this run certifies against — and when the rule was written those objects were executed, which is why it was a High.

ssh://, git+ssh://, ssh+git:// and scp-form git@host:path were accepted and are now REFUSED (ssh-transport:<form>). The isolated environment must admit HOME so OpenSSH can find keys and known_hosts, and OpenSSH then also honours ~/.ssh/config, where a Host github.com rule can rewrite HostName or run ProxyCommand/Match exec. That is a redirected baseline and arbitrary execution behind a URL string that passes the identity check. It is in model because HOME is shared: every lane runs as one user with a writable home, so the planter is a peer lane, not the invoker. It was met by descope, not hardening — a bounded residual was unavailable because ProxyCommand executes, and the usual mitigation (a redirected baseline degrades to a wrong component list the comparison detects) does not reach a harm that lands during transport, before any comparison. Measured cost: nil — every lane and CI already use https://, so an ssh-form checkout fails closed with the remedy named. A non-default port is a different endpoint and is rejected. Userinfo is accepted — GitHub Actions rewrites origin to https://x-access-token:<TOKEN>@github.com/…, so rejecting it would red a legitimate CI checkout — and is therefore redacted from every rendering, because SUMMARY blocks are routinely pasted into PR comments.

Two related properties of the same pre-flight. The baseline is fetched into a private per-run refs/worktree/… ref, never FETCH_HEAD: --refmap= removed the shared tracking-ref write and left FETCH_HEAD carrying the baseline, and FETCH_HEAD is itself a single shared mutable file that a concurrent fetch overwrites between the fetch and the read — the run would then compare against a commit it never fetched. And git merge-base --is-ancestor’s rc 1 is three-valued too: in a shallow clone it also means “the connecting history is absent”, so it is read as “not an ancestor” only in a repository proven complete (unmeasurable shallowness ⇒ INDETERMINATE); otherwise a legitimate committed removal in a shallow checkout reds as BEHIND — a false FAIL on correct input.

Hermetic self-tests use local origins, which are deliberately not canonical, so they substitute the artifact: one shared helper (scripts/tests/lib/agent-gate-canonical-pin.sh) rewrites the canonical literal in the fixture’s own scratch copy of the gate and then verifies the pin took. Never a settable seam — the first design accepted local paths so that the fixtures would work, which made the test hook and the vulnerability the same fact.

Fail-closed in the certifying modes only — the full gate and --delta, the two whose blocks are recorded in a PR. --lite and --only stamp the same line with an ADVISORY-* token and cannot fail on it: --lite runs every fix round and must not require the network to function. There is deliberately no opt-out, for the same reason as missing-schemas: — a branch behind main can always rebase, so an escape hatch could only buy a vacuous green.

Disk exhaustion is NAMED in the block (issue #3800)

Section titled “Disk exhaustion is NAMED in the block (issue #3800)”

A full gate that dies because the disk filled used to report, in the one artifact doctrine tells an agent to retain, minimal-build: FAIL (611s) beside 36/37 PASS and tree-integrity: PASS. Nothing named the host, so the reader debugged a minimal-features build that was never broken. Measured: lane-3634/target at 101G mid-full-gate, peaking at 143G on a 295G disk shared with two other lanes at 68G and 57G, 0 bytes free at the failure; a re-run on the same tree at the same sha PASSed once a peer freed space.

Every SUMMARY block that carries a component table — the full gate’s terminal, --lite’s, --delta’s, --delta’s python-tier REFUSED block, the #2926 tree-integrity component-BOUNDARY FAIL and the two hidden selftest hooks — now carries exactly one disk-exhaustion: line, on the missing-fixtures: / missing-schemas: marker precedent. Its value set is closed:

  • RECOGNISED (#3800) — names the signature, the component and <log>:<line>, says the observation is consistent with disk exhaustion on this host, and gives the remedy: free space and re-run before treating the non-PASS as a defect in the diff. It says in the same breath that this is evidence, not proof — the diff is not thereby cleared. An earlier wording asserted the failure was “NOT a defect in the diff”, which this scan cannot support and which contradicted the attribution rule below: a failing test may legitimately print a signature into its own log, and a diff can itself drive disk usage (a new feature-matrix lane, a large fixture, a runaway build).
  • 0 RECOGNISED (#3800) — either every subject was read and nothing matched, or there was no non-PASS component and no in-memory subject to scan. Never a bare 0: a bare zero reads as a verified all-clear from a scan that documents itself as incomplete.
  • UNMEASURED (#3800) — a subject could not be read. UNMEASURED is never “no ENOSPC”; the clean verdict is keyed on the affirmative fact that every subject was read, never on the absence of a match. grep’s exit status is read as three-valued for exactly this reason.

Properties worth knowing:

  • Adding the marker to a block does NOT make that block’s cause observable to it — ask what the SUBJECT SET is on the new path (roborev job 301). This is the most transferable thing in the issue. The #2926 tree-integrity boundary block was given the line precisely because tree-integrity: FAIL is reachable from ENOSPC: the capture manifest is written into $LOG_DIR, and TREE_CAPTURE_FAIL_REASON is a fixed constanttree-capture-failed; the tree cannot be proven unchanged — that can never name disk. But the scan’s subject set was non-PASS component logs, and on that path _tree_identity fails independently of any component, its write-error text lands in no component log, and the components are typically still PASS. The subject set structurally could not contain the evidence, so the block would have emitted an affirmative 0 RECOGNISED on exactly the path the line was added for — a false clean reading, and the standing anti-pattern of a positive verdict keyed on the absence of a bad signal in a set that cannot hold it. Strictly worse than the exemption it replaced. When you extend a diagnostic to a new failure path, ask whether the evidence can physically land in the subject set — not merely whether the line is emitted.

  • So the subject set went to TWO KINDS: non-PASS component logs, plus the gate’s own capture-failure text, held IN MEMORY (a THIRD followed one round later — see the declared-boundary bullet below). _tree_identity captures the stderr of its manifest write (and of the batched git hash-object) and hands it back on the rc-2 channel, which is disjoint from the identity channel by return code — a channel the text cannot influence. Each capture-failure site records it as a named subject (DISK_MEM_SUBJECTS, via _tree_note_capture_failure). It is in memory and never a spill file, for the same reason the scanner avoids _ansi_stripped_log: under ENOSPC the file cannot be written, so a file-backed subject would be empty exactly on the run that had the answer. There is one signature loop for both kinds (_disk_scan_subject file|text) — never a second matcher — and a here-string is deliberately not used for the text kind, because bash materialises <<< as a temporary file. The in-memory subjects are counted in the census, so 0 RECOGNISED can only be claimed on a run where they were read too. A match is named in our vocabulary (IN-MEMORY subject '<label>') with no fabricated <log>:<line>, and no captured byte is interpolated (#3312 — the text is OS/libc-controlled). A capture recorded with no text, and a TREE_CAPTURE_FAILED with nothing recorded on the channel, are each UNMEASURED naming that, never clean.

  • It is pinned by a REAL ENOSPC, not by the line’s existence. The suite points the capture manifest and its .report at /dev/full, so the shipped _tree_identity fails with the platform’s own strerror; fixture provenance (rc 2 and real No space left on device text) is asserted before the verdict, and a mutation control shows the same run collapses to 0 RECOGNISED … (3/3 PASS) once the in-memory subject is removed — the false reading itself. /dev/full is Linux-only and macOS is a first-class gate host, so that case declares its skip and a host-independent injected case (with a negative control) carries the property everywhere.

  • THIRD INSTANCE ⇒ THE SUBJECT SET IS NOW DECLARED, NOT CARVED AGAIN (roborev job 304). record_result writes $LOG_DIR/<component>.result. Under ENOSPC that write fails, its error text goes to gate stderr — neither a component log nor an in-memory subject — and the parent’s fail-closed guard then synthesises FAIL 0 for a component whose own log is clean, because it may genuinely have succeeded and died on the write. Both channels empty ⇒ an affirmative 0 RECOGNISED, the same false-clean shape as the tree-capture case one writer over. Three consecutive review rounds each found a different unwatched gate-internal writer, and the standing ruling on a repeatedly-carved guard is to consolidate and state the boundary rather than patch the next instance. Three parts:

    1. A verdict the gate could not READ is an UNMEASURED subject, whatever caused it. Absent, unreadable, or malformed — the STATUS token validated against the closed PASS|FAIL|SKIP set and the seconds field against an integer, because an ENOSPC write typically leaves the file created and empty (open+truncate succeeds, the write does not). Recorded in DISK_UNREAD_VERDICTS, counted in the census, and it can never contribute to a clean reading. Strictly more general than ENOSPC. Every .result reader routes through the one reader _disk_verdict_read — a private two-field verdict read anywhere else records nothing, and the suite REDS on one structurally. The UNMEASURED arm now takes precedence over “no non-PASS component to scan”, and that order is load-bearing on a reachable shape: a .result reading PASS abc records PASS in the table while its verdict was not fully read, so the old order rendered the affirmative clean reading over an unmeasured subject on a run with nothing else wrong. Found by mutation — removing the reorder left every other case in the suite green.
    2. record_result captures its own write failure IN MEMORY, on the existing _disk_note_capture_failure channel — never a spill file, which under ENOSPC is exactly what cannot be written. That note reaches the parent only from the parent shell: the serial MAIN lane and every --lite/--delta component. A SIDE-lane component runs in a backgrounded subshell (#1737) where the append is lost, and the marker-file idiom used elsewhere for that boundary is unavailable by construction under ENOSPC. That path is covered by (1) instead — the missing or malformed .result renders UNMEASURED naming the component. Partial and declared beats a false clean.
    3. The emitted scan: field declares its SUBJECT set as well as its signature set: the three kinds — (a) logs of non-PASS components, (b) the gate’s own in-memory capture-failure subjects, (c) components whose .result verdict could not be read — plus the gate-internal writers known to sit outside them, named so the boundary is a fact a reader can check rather than a hedge: the _fm_* sidecars, node-bindings.leak-lane, summary-integrity.fail, and the heartbeat file. An ENOSPC failure in any of those is a declared false negative. The line reads NON-EXHAUSTIVE by construction ON BOTH AXES.

    The transferable rule: when a diagnostic’s subject set has been extended three times, publish the boundary. A marker that names its own blind spots is worth more than one implying a completeness it does not have — the same reasoning as the closed signature set declaring its own non-exhaustiveness, one axis over.

  • It is an ATTRIBUTION, never a verdict. It never reads, sets or influences OVERALL/RESULT. A matched signature is evidence about the host, not proof the diff is innocent, and this marker does not own the verdict.

  • Its SCOPE is “carries a component table”, not “is terminal” — and the exclusions are DECLARED at the site. The script has 25 emit_summary / _emit_terminal_summary call sites; 7 carry a component table and all 7 append the line. The other 18 — the three pre-flight FAIL-CLOSED blocks, the component-set FAIL, the two summary-integrity FAILs, the shared forwarder, the five self-test hooks, the four --delta usage ERRORs, the two --delta refused-before-execution blocks and the --only no-Data.db pre-flight — are emitted where no component has run. They have nothing to attribute: the line could only render a misleading 0 RECOGNISED … (0/0 PASS), and each block already names its own cause with its own dedicated marker, so adding it there would be noise that dilutes this marker’s meaning. Each carries a # disk-exhaustion-exempt: <reason> comment, on the component-set-exempt: idiom, and the suite censuses every emit site as MARKED / EXEMPT / GAP — a new emit site carrying neither REDS it. The --delta REFUSED paths split and the distinction matters: the python-tier refusal HAS a component table (file-size / fmt / scoped-tests run before it) and IS marked; the two refused-before-execution blocks say NOT RUN — refused before execution in their own delta-scope: line and are exempt. This claim read “every terminal block” until roborev job 299, and the then-structural test could not detect that it was false: it derived its subject set from sites containing _fm_summary_line, i.e. exactly the already-compliant sites. A guard whose subject set is the compliant set is a guard that cannot fail — the same shape as #3544’s own claim-exceeds-check finding, and the reason the replacement enumerates every site and lets the unaccounted ones fall into GAP.

  • That circularity had a SECOND LEVEL, and it cost the 7th site. Narrowing the claim to “carries a component table” fixed the wording and left the census deriving its table-bearing subset from _fm_summary_line alone — one renderer’s name. So the one block that renders a component table with its own printf '%-18s %s (%ss)', the #2926 tree-integrity component-boundary FAIL (_tree_boundary_meta_lines), was invisible to the derivation and shipped a round exempted, on the stated grounds that its cause was already named — a mid-run tree mutation. It is not. tree-integrity: FAIL has a second cause that is disk exhaustion: _tree_identity writes its capture manifest into $LOG_DIR, and when that write or its validation fails the block is stamped with TREE_CAPTURE_FAIL_REASON — a fixed constant, tree-capture-failed; the tree cannot be proven unchanged. An ENOSPC on the logs filesystem therefore produces a verdict that can never name disk and that reads as a git/worktree problem: a worse instance of this issue’s own defect than the minimal-build: FAIL that opened it. The general rule it leaves behind: “its cause is already named” only excuses an omission if the named cause can name THIS one — a fixed reason string cannot. The derivation is now over the row FORMAT, not a renderer’s name: a function is a component-row renderer if its body emits %-18s %s (%s on a code line; a row site is any code line outside every renderer’s own body that emits that format or calls a renderer; row sites map to their emit site and are deduped, so the count is table-bearing blocks. A third renderer is recognised with no edit to the suite. A permanent control plants into the second renderer specifically — the two pre-existing controls both plant into the _fm_summary_line family, so neither can fail if the derivation goes blind again — and a further case asserts the derived sites reach the census through ≥ 2 distinct renderers, so “7 sites” cannot be satisfied by one renderer seven times.

  • The boundary site is the one MID-RUN emit, and the line says so. That block is published from inside a run that has not finished, so both halves of the line declare a partial window: the free-space pair is relabelled free(start->boundary, MID-RUN PARTIAL WINDOW: the run had NOT finished), and the scan field adds SUBJECT SET ALSO PARTIAL — only the components whose verdict was recorded by that boundary are subjects, and the ones that had not run are not a clean reading. Labelled, never omitted: a partial figure read as a terminal one understates a collapse that had not finished happening, but dropping the field would discard the most useful disk fact the block can carry.

  • The signature set is CLOSEDNo space left on device, os error 28, Disk quota exceeded — and the line declares that non-exhaustiveness on every run. A bare ENOSPC token is deliberately excluded: it occurs in this repository’s own test names, comments and doctrine, so it would fire on a log that merely mentions the class.

  • Only non-PASS components are scanned. A PASSing component’s log carrying the phrase describes a run that recovered and explains nothing.

  • It reads the RAW log, not _ansi_stripped_log. That helper materialises a sibling file, and a disk-exhaustion diagnostic that needs free disk in order to run is useless exactly when it matters. Safe under #3400 because these three signatures are pure payload (libc strerror, LLVM/ld, Rust’s io::Error Display) and never a coloured cargo status word — a claim the suite measures with a real ANSI-coloured fixture rather than asserting.

  • No log-derived text reaches the block. Only our signature name, the component name (from the gate’s own COMPONENTS) and an integer line number. A component log is compiler- and test-controlled; interpolating it would let a newline break the block frame and forge a RESULT: line.

  • The free-space field is a start→emit DELTA (start→boundary, labelled partial, at the one mid-run site), measured with POSIX df -Pk (macOS is a first-class gate host and df -h output differs by platform). An instantaneous emit-time read is misleading: peers free space between the failure and the emit, which is this issue’s own re-run evidence. An unmeasurable df renders free: UNMEASURED and does not downgrade the log-scan verdict — the two facts are independent.

Pinned by scripts/tests/test_agent_gate_disk_exhaustion.sh (hermetic, in tooling-tests, registered above that component’s python3 SKIP branch because it needs nothing beyond bash). The boundary block’s own end-to-end assertions — one attribution line, both partial-window declarations, and RESULT: FAIL unchanged by it — live in scripts/tests/test_agent_gate_tree_integrity.sh, which already drives a real mid-run mutation through the real gate.

This is a diagnostic, not a capacity fix. Nothing here makes the slot cap disk-aware, refuses or queues a gate on low free space, budgets disk per lane, or shares one CARGO_TARGET_DIR per box; that is tracked under #3434 / #3763 / #3755. Related: per-worktree target/ for a full gate is ~100–145GB, not the ~25–30GB docs/development/gate-ops.md claimed until #3800 — three concurrent full gates need roughly 430G on a 295G disk.

Terminal window
# Full gate — the only run that counts
scripts/agent-gate.sh
# Fast iteration loop — NOT the gate of record (issue #1821)
scripts/agent-gate.sh --lite
# Test/docs-only re-cert after a full PASS at X — NOT the gate of record (issue #1892)
scripts/agent-gate.sh --delta X --anchor-run-id <X's full-gate run-id>
# Debugging aid only — output marked PARTIAL, never counts
scripts/agent-gate.sh --only fmt,clippy
# List components without running (also --lite-list / --delta-list)
scripts/agent-gate.sh --list

Exit codes: 0 = PASS, 1 = FAIL/REFUSED (--delta), 2 = usage/anchor error (--delta), 3 = PARTIAL (--only mode).

Tiered gate: --lite iterate, full gate once (issue #1821)

Section titled “Tiered gate: --lite iterate, full gate once (issue #1821)”

The gate is tiered. scripts/agent-gate.sh --lite runs only the reduced component set (LITE_COMPONENTS: file-size + fmt + scoped workspace clippy + roborev-lints

  • blast-radius-scoped tests). It is the fast iteration loop, NOT the gate of record — it emits a DISTINCT ==== AGENT-GATE LITE SUMMARY ==== block (MODE: lite) that must never be pasted as the full SUMMARY. Iterate on --lite every fix round; run the FULL scripts/agent-gate.sh exactly once before merge. --lite never replaces the full gate.

--lite is NOT a flat ~1–5 min budget — its cost is a FUNCTION of the diff (issue #3764). There are two cost drivers, and only ONE of them scales with your diff.

  1. clippy is NOT diff-scoped. --lite dispatches the IDENTICAL run_clippy the full gate does — run_component clippy run_clippy at scripts/agent-gate.sh:17233 and :18220 respectively — i.e. the issue #1844 per-package scoped workspace matrix at :9357, and run_clippy never reads the diff. (The whole-workspace --all-features --all-targets pass is the CQLITE_CLIPPY_FULL=1 path only; do not read the scoped matrix as that one.) So every --lite pays clippy IN FULL whatever the diff: measured over 188 completed lite runs it is a no-op warm, 2–7 min part-warm, and 16–24 min cold.
  2. scoped-tests is diff-scoped, and it has a fan-out leg. It RUNS the touched package’s --lib plus the diff’s new --test targets (owners by longest-prefix path match over cargo metadata, from merge-base(HEAD, <base>)...HEAD — where <base> is the FIRST of origin/mainmainorigin/mastermaster that resolves (:16870) — plus git diff HEAD, i.e. the uncommitted diff over TRACKED files only, untracked excluded; defaults to cqlite-core --lib when no rust package is in the diff) — and when a changed path is under cqlite-core/src/ it ALSO runs cargo test -p <pkg> --all-targets --no-run for every workspace member that DIRECTLY DECLARES a dependency on cqlite-core (the --no-deps metadata edge) and owns a --test target (issue #2658: COMPILE-CHECKED, never run). That leg — not “touched packages”, which consult no dependency edge at all — is why a core-src diff annotates 9–11 package sets, and its --all-targets is what balloons target/debug/deps (+18 GB in a single round — reported by another lane in issues #3763/#3764, not measured here). cqlite-core/tests/** does NOT trigger the fan-out; cqlite-core/src/** does.

Measured bands (completed runs, one fleet box): a narrow, WARM-clippy diff is median 1.4 min (n=43) — so the ~1–5 min this page used to claim is that case exactly, a FLOOR and not a range; a cqlite-core/src/ diff is median 20 min, range 3.8–43 min (n=20). The two bands are marginal over DIFFERENT subsets and do not compose — a 1.4 min run is by construction one that paid no cold clippy, so the cold-clippy band is not additive on top of it. Beyond those, lane-3612 reports (not measured here) up to ~104 min under peer load in issue #3764.

And --lite is EXEMPT from the issue #1825 gate-slot cap (as are --delta and --only) — it runs outside slot arbitration entirely, so on a shared box its build competes with a peer’s gate of record for disk and CPU with nothing arbitrating it. There is NO admission check for --lite today, and issue #3763 owns that gap — read this as a budgeting fact, not as an instruction to apply one yourself.

Division of labor (issues #1855, #2084). In the worker → subagent model, an implementer subagent (sstable-developer) edits, commits, pushes, and verifies with --lite/targeted tests only — it must never invoke the full gate. The ONE full gate of record runs inside the disposable flow-closer subagent (spawned per issue by flow-implement), which invokes it via run_in_background with the summary-file pattern and never idle-waits — a subagent idle-waiting on a 12–25 min gate gets killed by the stall watchdog and orphans its child gate process (issue #1855). Review runs before that gate (see Delivery pipeline).

Test/docs-only delta re-certification: --delta (issue #1892)

Section titled “Test/docs-only delta re-certification: --delta (issue #1892)”

Once the full gate has PASSed at a commit X, a post-review polish round whose only changes are tests and/or docs does not need a whole new full gate — the full gate at X already validated clippy, core-tests, bindings, parity, and smoke against the production code, none of which the polish round touched. Re-certify the X..Y diff with:

Terminal window
scripts/agent-gate.sh --delta X --anchor-run-id <X's full-gate run-id>
# or read the run-id from the recorded full SUMMARY (refuses a non-full block):
scripts/agent-gate.sh --delta X --anchor-summary-file <path-to-X-full-SUMMARY>

--delta verifies the diff X..Y (committed + working tree) touches ONLY what the re-cert can EXECUTE: rust cargo test code (.rs under tests/ dirs, *_test(s).rs anywhere), python binding tests (bindings/python/tests/ — run by the issue-1893 python tier), Node.js binding tests (bindings/node/__test__/*, run against an ALREADY-BUILT native module), shell self-tests (scripts/tests/*.sh), and/or docs (*.md anywhere; top-level-anchored docs/, website/ only — issue #2081 moved node/shell from refused to executed). It is fail-closed: anything else (src, scripts, workflows, Cargo.*, config, test-data, or an unbuilt node module — it NEVER builds with cargo and never passes vacuously) makes it REFUSE and name the offending files — a production change always requires a fresh full gate. On pass it runs only file-size + fmt + the diff’s changed test targets (the same blast-radius scoper --lite uses) and emits a DISTINCT ==== AGENT-GATE DELTA SUMMARY ==== block (MODE: delta, recovery default .agent-gate-delta-summary.txt) carrying a delta-executors: line naming which executors ran.

The delta block is not the gate of record and carries an explicit gate-of-record: line naming the full PASS at X plus the anchor run-id, so it can never be pasted as a full SUMMARY. Record BOTH artifacts in the PR: the anchor’s full SUMMARY (the gate of record) AND the X..Y DELTA block. Any production change resets this — the next gate of record is a fresh full scripts/agent-gate.sh PASS.

Standing backstop (owner condition, 2026-07-04). Long-term quality is backstopped by the nightly full run on main: .github/workflows/gate.yml (deep-check) re-runs the FULL gate with CQLITE_CLIPPY_FULL=1, deeper than the local gate. Delta re-certification leans on that nightly as the net for anything a test/docs round scoped past. --delta (like --lite and --only) is EXEMPT from the machine-wide concurrency cap.

CITE-AND-WAIVE: waiving a red on a genuinely prose diff (#3042, narrowed by #3250)

Section titled “CITE-AND-WAIVE: waiving a red on a genuinely prose diff (#3042, narrowed by #3250)”

A genuinely prose diff cannot change the compiled binary, so a test failure in its full gate is by definition pre-existing on main or a flake, and the correct response is CITE-AND-WAIVE — never a source patch to turn the gate green (that is a real change smuggled in under a docs diff, certified by nothing, and it masks the actual main-red).

The waiver’s precondition is that the diff touches no compiled input: no src, no Cargo.*, no build script, no workflow, no test-data. That qualifier is a path-shape test, and a path shape is not evidence. Don’t judge it — run it:

Terminal window
git diff --name-only origin/main...HEAD | bash scripts/ci/classify-docs-only.sh
# exit 0 => docs-only (the waiver is available)
# exit 1 => full (the waiver does NOT apply — the failure is presumed yours)

scripts/ci/classify-docs-only.sh is the same classifier that decides whether pr-gate-core runs at all, so running it here asks the gate’s own question rather than a paraphrase of it. It answers documentation only on an affirmative allowlist match — prose/images/legal text, inert report artifacts under docs/, and code-bearing report formats (.json, .html) only inside an artifact-bearing directory — and fails closed on every unrecognized extension and on every extensionless path. scripts/tests/test_classify_docs_only.sh pins that behaviour, and it runs inside the full gate’s tooling-tests component.

Why the old wording was unsafe, in one concrete case. This repository ships measurement harnesses under docs/reports/*-artifacts/ by convention, so a PR-#3222-shaped diff contains src/main.rs and Cargo.toml under docs/reports/ws0-3026-artifacts/ws0-cqlite/scan-harness/. It satisfies “no src, no Cargo.*textually while being false materially — measured, the three merged docs/-only PRs #3222 / #3081 / #3216 carried 35 / 30 / 1 executable or config-as-code paths and reported required green in 13–16 s against a ~14-minute baseline. An agent correctly following the old text would waive a red that was genuinely its own.

Everything else about the rule is unchanged:

  1. Confirm the diff really is non-compiling input — with the classifier, not by eye.
  2. Identify the failure as a known main-red issue or a known flake; reproduce it on a clean origin/main checkout if it is not already filed, and FILE it if not.
  3. Record the waiver in the PR body, naming the failing component and the issue number it belongs to. A waiver with no cited issue is not a waiver — it is an unexplained red.

Conversely, if ANY compiled input is in the diff the waiver is void: the failure is presumed yours until proven otherwise.

Same definition, two sides. This is the gate-side spelling of the review-side rule in roborev findings: “docs-only” means a code-free census, never a docs/ path prefix. The two definitions are cross-referenced deliberately so they cannot drift apart again — #3229 fixed the reviewer’s copy of this bug and #3250 fixed the gate’s.

A fresh machine that will run the gate should first run bash scripts/bootstrap-agent-machine.sh (see docs/development/agent-machine-setup.md): it installs/verifies the accelerators below, the datasets, gh auth + the project scope, and roborev’s local config, then prints the gate’s accelerators: line as a health check.

Accelerators are LOUD when missing (issue #1848)

Section titled “Accelerators are LOUD when missing (issue #1848)”

Every optional accelerator the gate depends on is auto-detected, and every SUMMARY block (full and --lite) carries a machine-checkable line:

accelerators: sccache=on nextest=on lanes=on

On Linux the line additionally carries a mold= token and a perf= token (byte-identical / no tokens on macOS — both are Linux-only):

accelerators: sccache=on nextest=on lanes=on sccache-health=ok sccache-cap=32212254720 sccache-used=1375141619(4%) mold=linked perf=ok
cpu-budget: wrapper=nice ncpu=16 max-concurrency=1(pinned) cores-per-gate=16 build-jobs=16(derived) test-threads=16

(The cpu-budget: line is a sibling of accelerators: and carries the resolved slot cap and where it came from — see the concurrency-cap section below.)

  • sccache — cross-worktree compile cache (~25.6% faster fresh builds).
  • nextest — parallel core-tests (the gate’s long pole).
  • lanes — parallel gate components (needs bash ≥4.3 for wait -n).
  • mold (issue #2859, Linux only) — the fast linker, wired via a per-machine ~/.cargo/config.toml managed block. States: mold=linked (wired) · overridden (a global RUSTFLAGS is suppressing the wired flags — don’t export one on a worker) · present-unconfigured (installed but not wired — re-run bootstrap) · absent.
  • perf (issue #3249, Linux only) — can this box be profiled at all? A free read of /proc/sys/kernel/{perf_event_paranoid,kptr_restrict} through shell builtins. Free is an enforced cost, not a slogan: the emit-time path runs zero external processes and zero command substitutions (a $( ) forks a subshell, so a value read back through one would not be free — hence the token comes back through a caller-named variable), and the helper is sourced once per gate run, not per summary. test_agent_gate_summary.sh case perf-free kills any regression: it counts the substitutions statically and re-runs the extracted path with an unresolvable PATH under xtrace subshell counting. States: perf=ok (unprivileged per-CPU profiling and kernel symbols available) · paranoid-<N> (perf_event_paranoid = N ≥ 1 forbids CPU-wide events, so the perf stat -C <cpu> the measurement doctrine mandates is denied — a permission verdict, not a missing capability; agent images ship 4, which on Debian/Ubuntu kernels denies perf entirely) · kptr-restricted (paranoid is fine but kptr_restrict != 0, so kernel frames resolve to bare addresses — a silent attribution loss) · absent (the /proc controls are not present, e.g. a container — tune the host) · unknown (present but unparseable; never guessed). Anything but ok on a box you intend to measure means re-run bash scripts/bootstrap-agent-machine.sh --yes, which installs /etc/sysctl.d/99-cqlite-perf.conf and then verifies it by running perf stat -C 0 -e cycles. Rationale + security posture: docs/development/fleet-runbook.md.

State values: on (detected & used) · absent (missing → the gate prints a loud WARN: on STDERR with the one-line install command) · off (intentionally disabled via CQLITE_DISABLE_SCCACHE / CQLITE_DISABLE_NEXTEST / AGENT_GATE_JOBS=1; no warn) · lanes=serial (degraded by bash <4.3). An intentional opt-out is off, never absent. This exists because a machine silently ran ~3x slower for weeks with sccache and nextest both un-installed and no signal. If a pasted SUMMARY shows absent, install the tool — the state is visible in the block, not just scrollback.

Two further tokens carry the sccache capacity facts (issue #3727): sccache-cap=<bytes> and sccache-used=<bytes>(<N>%)measured bytes, with no provenance classifier (lead ruling req-3727-w4: reporting stays, interpreting goes; the 7-state source suffix, the value-grammar map and the remediation WARNs were removed, and their state-combination knowledge lives in the follow-up issue). Each has an explicit unmeasured(<why>) / na(sccache-not-in-use) rendering, so a byte count is always an affirmative measurement.

The cap is read from the running server’s JSON and attributed to a server by a differential, which is the one thing you must not skip when reading a --show-stats number: with no server running, --show-stats does not start one and answers max_cache_size from the CLIENT’s own environment, and a null cache_size does not tell the two apart (a running server with an empty cache reports null too). A running server’s answer does not move when the client’s SCCACHE_CACHE_SIZE changes; a client’s does. Unattributed therefore renders unmeasured(no-running-server) or unmeasured(unattributed) — never a byte count.

sccache reads SCCACHE_CACHE_SIZE once, at server startup, so raising the value has no effect until sccache --stop-server. Measured trap: 30G is 30 GiB but 30GiB and 30GB are SILENTLY DISCARDED to sccache’s 10 GiB default, and a bare integer means BYTES — with no diagnostic anywhere. sccache-health cannot answer any of this: it is the sum of four ERROR counters with no capacity input, so a warn there can never be cleared by raising the cap and a permanently-full, thrashing cache reads ok. Persist and verify the cap with bash scripts/bootstrap-agent-machine.sh --fix-sccache-cap, which correlates the /etc/environment line, the value a fresh non-login PAM session sees, that value in bytes (its own isolated sccache oracle) and the bytes the running server enforces. Declared residual: a LOGIN shell can see a different value (on this fleet /etc/profile.d runs after pam_env) and that context is no longer measured, so the verdict is scoped to the non-login session in its own output.

Machine-wide concurrency cap (issue #1825)

Section titled “Machine-wide concurrency cap (issue #1825)”

Running many sessions/worktrees at once used to let ~15 full gates hit the CPU simultaneously (load 30–60), which SIGKILLed gates mid-core-tests. The full gate now takes a cross-process bounded semaphore: at most N full agent-gate.sh runs execute machine-wide at once. Excess invocations queue (block) for a slot and print one line — waiting for gate slot (N in use)… — then proceed when a slot frees. They never fail from the cap, and a non-interactive caller blocks cleanly rather than spin-failing.

  • --lite, --delta, and --only runs are EXEMPT — never queued. --lite and --delta are cheap by design; --only is a PARTIAL run (and is used by nested tooling self-tests, so capping it could self-deadlock the queue).

  • N defaults to max(2, floor((ncpu-2)/4)) — a conservative fraction of cores that still lets a couple of gates run on a small box. Override with CQLITE_GATE_MAX_CONCURRENCY.

  • Every SUMMARY says where N came from (issue #3414). The cpu-budget: line stamps max-concurrency=N(pinned|default|invalid|clamped), the same idiom as build-jobs=N(derived|caller) beside it: pinned = the env var held a valid integer ≥ 1 and was used verbatim, default = it was unset so N is the formula above, invalid = it was empty or non-numeric and was silently discarded for the formula, clamped = it was a valid integer < 1 and was silently raised to 1. 3 and 3 because nothing set it are different operational facts — read N(default) on a fleet box as the pin is not provisioned.

    The remedy DIFFERS BY TOKEN, and getting that wrong sends you in a circle. A default box has no pin line at all, so bash scripts/bootstrap-agent-machine.sh --fix-gate-pin (or --yes) persists one. An invalid or clamped box ALREADY HAS the line, holding a bad value — and bootstrap deliberately never rewrites an existing value, because a box running >1 gate on purpose must not be clobbered — so re-running it there is a silent no-op. Fix the VALUE by hand in /etc/environment. Bootstrap says the same thing at the same fork, as gate-pin: NOT-HONOURED.

  • SIGKILL-safe stale-slot reaping: each slot is an fcntl.flock held by a small background daemon (scripts/lib/gate_slot_daemon.py) that the gate starts and monitors. Because the daemon opens the lock fd after it is forked, the gate’s heavy children (cargo/nextest) never inherit the lock — a SIGKILLed gate frees its slot within one poll interval even while orphaned children run on. A crashed gate can never permanently leak a slot.

  • Works across worktrees (shared slot dir, not per-checkout) and composes with the per-gate component parallelism (AGENT_GATE_JOBS) and sccache: the cap bounds the worst case (several sessions hitting their one full gate at once), the others cut average load and per-compile time.

Environment knobs (all optional):

Terminal window
CQLITE_GATE_MAX_CONCURRENCY=4 bash scripts/agent-gate.sh # raise N on a big box
CQLITE_GATE_SLOTS_DIR=/path bash scripts/agent-gate.sh # slot dir (default $TMPDIR/cqlite-gate-slots)
CQLITE_GATE_POLL_SECS=1 bash scripts/agent-gate.sh # queue/liveness poll (default 2s)
CQLITE_GATE_DISABLE_CAP=1 bash scripts/agent-gate.sh # force-disable the cap

The cap fails open (disabled, with a loud stderr note) when python3 or the daemon is unavailable — the gate must never be un-runnable because of the cap. A hermetic self-test proving queueing at N, --lite exemption, and SIGKILL slot release lives at scripts/tests/test_gate_concurrency_cap.sh and runs inside the tooling-tests component.

Capturing the gate: the summary-file redirect is the DEFAULT (issues #1175, #2079)

Section titled “Capturing the gate: the summary-file redirect is the DEFAULT (issues #1175, #2079)”

The SUMMARY block is the only gate text an agent retains — and the raw gate log (thousands of lines) must never be read into a persistent agent context. So the required default invocation everywhere — the full gate AND each --lite round — sets a summary file in advance and reads it, rather than streaming stdout:

Terminal window
AGENT_GATE_SUMMARY_FILE=/tmp/gate-summary.txt \
bash scripts/agent-gate.sh > gate.log 2>&1 < /dev/null
cat /tmp/gate-summary.txt # complete SUMMARY block; gate.log is never read into context

This is the default because it is also the robust path. Under non-foreground capture (a script/pty, a buffering wrapper, a “drain-until-EOF then write” reader, or a backgrounded pipeline) a streamed SUMMARY block can be lost entirely: a gate component sometimes leaks a descendant (a cargo/rustc build server, a daemonizing test, etc.) that keeps the gate’s stdout pipe open, so an until-EOF reader never sees EOF, gets killed by a timeout, and discards its in-memory buffer — even though the gate exited 0. (Detaching the gate’s own stdout cannot fix this: the leaked child still holds its inherited copy of the pipe write-end.) The summary file does not depend on the stream at all — pick the path in advance and read it:

  • Set AGENT_GATE_SUMMARY_FILE=/path before running. The gate writes the complete SUMMARY to that exact path with plain redirection, so the file is complete no matter what happens to stdout. cat it afterward; it always contains the full block (start marker → RESULT: PASS/RESULT: FAIL → end marker). Prefer run_in_background (or a long timeout) so a subagent never idle-waits on the gate and gets watchdog-killed (issue #1855).

    Terminal window
    AGENT_GATE_SUMMARY_FILE=/tmp/gate-summary.txt \
    bash scripts/agent-gate.sh > gate.log 2>&1 < /dev/null
    cat /tmp/gate-summary.txt # complete SUMMARY, even if gate.log truncated

Completion probe — anchor on PASS|FAIL; INCOMPLETE is a liveness placeholder, not a verdict (#3041; mechanism follow-up #2908). The gate writes RESULT: INCOMPLETE (gate did not finish) into $AGENT_GATE_SUMMARY_FILE at launch (its EXIT-trap startup sentinel, written before the #1825 slot is even acquired) and only overwrites it with RESULT: PASS/RESULT: FAIL on completion. The placeholder is deliberate — it makes a killed/orphaned gate detectable — but it means a bare grep -q on the bare RESULT: token is satisfied the instant the gate starts, before a single component has run. An agent polling that way can read a just-launched (or queued) gate as a finished one and advance toward merge on a verdict that does not exist. The only correct predicate is:

Terminal window
# RECORD grammar — full / --lite. Anchored + token-terminated; it MUST keep refusing PARTIAL, and
# ERROR and REFUSED with it. Widening it would weaken the gate-of-record probe for nothing.
grep -qE '^RESULT: (PASS|FAIL)([[:space:]]|$)' "$AGENT_GATE_SUMMARY_FILE" # a VERDICT ⇒ gate finished
# ONLY grammar — `--only <component>` ONLY, never the gate of record (#3750). `--only` demotes a
# SUCCESSFUL run to `RESULT: PARTIAL`, so the record grammar spins on green. The EXIT STATUS (3) is
# primary; this is the fallback for a detached run whose exit code you never observe.
grep -qE '^RESULT: (PASS|FAIL|PARTIAL)([[:space:]]|$)' "$AGENT_GATE_SUMMARY_FILE"
# DELTA grammar — `--delta <anchor>` ONLY. It is the only mode that can terminate ERROR or REFUSED
# (7 emit sites, all inside `run_delta`; REFUSED goes through `emit_summary "$(_tree_result REFUSED)"`,
# so grepping `emit_summary REFUSED` finds nothing while the token IS emitted). A --delta poller on
# the RECORD grammar hangs on a terminal outcome. This set is gate-liveness.sh's own enumerated
# terminal set, token for token — ONE source of truth, so it carries PARTIAL (which --delta cannot
# emit) and the reader's defensive REFUSED, with ERROR the emit you will actually meet.
grep -qE '^RESULT: (PASS|FAIL|PARTIAL|ERROR|REFUSED)([[:space:]]|$)' "$AGENT_GATE_SUMMARY_FILE"
# Safe to widen COMPLETION here, and it would not have been before #3750 separated completion from
# verdict: the verdict is a separate affirmative read, so three grammars are not three chances to be
# wrong. Better than any of them: ask gate-liveness.sh.
# COMPLETION IS NOT A VERDICT. `PARTIAL` says the run ENDED. Read the component's OWN line separately:
bash scripts/gate-component-verdict.sh "$SUM" --mode only --component tooling-tests --run-id <id>
# exit 0 PASS / 1 NOT-PASS / 4 COULD-NOT-MEASURE (no verdict available, whatever the reason) /
# 64 USAGE. A completed run whose component SKIPped is NOT a pass, and
# a LITE or DELTA block is REFUSED (4). A tree-integrity token of FAIL returns NOT-PASS (1) — the
# gate declared that run non-certifying, which invalidates every component in the block — while
# SKIP/PENDING/absent return 4, since tree stability was then never measured.
#
# NOT a completion probe, and no opinion about liveness — NEVER in a loop. Establish completion
# first (exit status, or gate-liveness.sh, the three-valued liveness authority and the only one of
# the two that may be polled). A retryability taxonomy here was DESCOPED (#3750): it told a lane a
# LIVE gate was permanently unmeasurable, and an obedient lane relaunches it — two gates, one path.

A terminal RESULT: INCOMPLETE means “still running, or died” — never a certification.

  • If you don’t set it, the gate writes the same complete block to the documented default $PWD/.agent-gate-summary.txt (gitignored). If your streamed capture looks truncated (missing the ==== END AGENT-GATE SUMMARY ==== marker), cat that file — it is always complete.

Concurrency caveat (#1175): the default $PWD/.agent-gate-summary.txt is per-checkout, not per-run. If you run multiple top-level gates concurrently in the same checkout, each MUST set a unique AGENT_GATE_SUMMARY_FILE or they will clobber each other’s recovery artifact. Separate worktrees get distinct repo roots and so distinct default paths — already isolated, which is CQLite’s normal model. The run-id: line lets a caller that captured the invocation’s run-id confirm it is reading the right run; a caller with no expected run-id and a fully-lost stream cannot disambiguate two same-checkout runs, so it must use a unique path.

Nested / self-test sub-gates are auto-isolated (#2874): a gate spawned by an enclosing gate inherits AGENT_GATE_PARENT_RUN_ID; if it does not pin its own AGENT_GATE_SUMMARY_FILE it defaults to its OWN $LOG_DIR/summary-primary.txt (never the checkout default) and stamps nested-under: <parent-run-id>, so a nested run can never clobber the parent gate of record. And a mid-run summary clobber (a foreign run-id appearing in the file) is caught at the next component boundary — and at the terminal emit — with a named summary-integrity: FAIL line + RESULT: FAIL, never a bare INCOMPLETE death. The old #2751 workaround (run the full gate without AGENT_GATE_SUMMARY_FILE) is therefore obsolete — the summary-file redirect is the default again, and running it alongside a peer lane’s gate self-tests on one box is safe. (Two top-level full gates sharing one checkout default still need distinct paths, per the caveat above.)

No-clobber + reader contract (#2874). When a gate finds the contended path already holding a FOREIGN run-id (a live peer owns it — the only way this arises is two top-level gates sharing one checkout-default path), it does not rewrite that path: it publishes its own FAIL verdict to a non-clobbering sibling <summary-file>.integrity-fail.<run-id> plus the logs: bundle (and stdout/stderr) and exits non-zero, deliberately leaving the peer’s block on the pinned path rather than clobbering it. The reader contract that makes this safe: the process exit code is primary, and any pinned-path block MUST be validated by its run-id: line before you trust it — a block whose run-id is not the one you launched (even RESULT: PASS) is a peer’s verdict, not yours. On a run-id mismatch (or a non-zero exit with a foreign block at the path), read the .integrity-fail.<run-id> sibling / logs: bundle for your run’s verdict. A closer polling the summary file should glob "$SUMMARY_FILE".integrity-fail.* and reject any block whose run-id differs from the one it launched.

The path the gate used is also echoed on the summary-file: line inside the block, and a copy is kept in the logs: bundle. The streamed copy is best-effort only.

A fast regression test for this emission path lives at scripts/tests/test_agent_gate_summary.sh (run it directly: bash scripts/tests/test_agent_gate_summary.sh). It exercises scripts/agent-gate.sh --emit-summary-selftest, which prints a representative SUMMARY block through the real emission code without running the 5–8 minute gate. The gate runs this test automatically as the tooling-tests component, so the capture guarantee is enforced on every gate run.

A green gate does not subsume pr-gate-core (#3453)

Section titled “A green gate does not subsume pr-gate-core (#3453)”

The gate and the required CI check overlap; neither contains the other. This is structural, and worth stating because a green SUMMARY reads like a prediction that CI will pass.

  • The gate runs lanes CI cannot. arrow-parity-guard names a #![cfg(feature = "arrow")] integration target that pr-gate’s cargo test -p cqlite-core --lib --all-features compiles no path to (a --lib run reaches no tests/ target); the feature-matrix, binding and parity lanes are local-only in the same way.
  • CI runs a lane the gate does not. That same --lib --all-features invocation executes cqlite-core’s unit suite with the OTLP stack ON, at a feature set no gate component executes.

The size of the gap, measured on main rather than cited from an incident:

cargo test -p cqlite-core --features cli-helpers --lib -- --list -> 3562 tests
cargo test -p cqlite-core --all-features --lib -- --list -> 3782 tests

220 cqlite-core lib tests execute in pr-gate-core and nowhere in the gate of record. #3382’s own fix pin — a_stats_only_name_cannot_create_an_instrument_through_the_emit_path — is one of them, and the gate’s feature set cannot even list it (0 matches vs 1). That is how PR #3382 earned a 31/31 gate PASS without executing the test pinning its own fix. The issue was filed around that single instance; the standing gap is 220 tests wide.

all-features-check closes the compile/lint half of that gap — a type error, or a -D warnings lint, inside a #[cfg(feature = "observability")] item now reds the gate of record. It deliberately does not close the runtime half: it executes none of those 220, so an order-dependent defect of #3382’s shape — a process-wide OnceLock<Instruments> poisoned by whichever test binds the global meter to the no-op provider first, invisible to #[serial_test::serial] grouping — still fails only in CI. (Those tests are gated on observability-testing, not observability.) A full all-features test lane was rejected on cost: tens of minutes on every endgame to duplicate a required check.

So: a red CI check on a green-gate PR is an ordinary event, not evidence the gate malfunctioned.

The gate emits a block between ==== AGENT-GATE SUMMARY ==== markers. The last line is always RESULT: PASS or RESULT: FAIL. Paste this block verbatim in your PR report — prose summaries are not accepted.

Format (as emitted by scripts/agent-gate.sh; ABRIDGED — 13 of the 37 component rows are shown, and the meta lines a run also emits are component-set:, accelerators: and cpu-budget:):

This block previously called itself exact and was not — it showed a subset of rows and had drifted behind the emitted format. “Exact” is a claim this page cannot keep true across every component addition, so it now says what it is. Every field name, and the whole NON-EXHAUSTIVE clause, are verbatim from a real run; only counts and shas are placeholders, as elsewhere here.

==== AGENT-GATE SUMMARY ====
commit: <short-sha> branch: <branch> dirty: yes|no
datasets: <N> Data.db files under <CQLITE_DATASETS_ROOT>
schemas: <N>/<N> canonical .cql readable under <root> (checkout-relative|CQLITE_SCHEMAS_ROOT override)
ci-pins: DATASET_TAG: <tag> DATASET_ASSET: <asset> DATASET_SHA256: <sha>
tree-start: <head-sha12> dirty: yes|no digest: <digest12>
tree-end: <head-sha12> dirty: yes|no digest: <digest12>
tree-integrity: PASS
census: <A>/<N> components AFFIRMED a count; <G> DECLARED-GAP (RECOGNISED); <U> NOT-MEASURED (RECOGNISED); <Z> measured-ZERO (RECOGNISED); <X> not-applicable (component did not PASS); <Y> no-subject (PASSed; the run had nothing to measure); <D> UNDECLARED; <W> unrecognised; <V> row(s) carry a VACUOUS status. NON-EXHAUSTIVE: the gap set is CURATED, so an unaffirmed component is UNMEASURED, never verified (#3625; the remaining gaps are tracked in #3162).
fmt: PASS|FAIL|VACUOUS (<Ns>) [fmt workspace features=n/a] {no census — cargo fmt --all --check emits no per-file tally to count}
clippy: PASS|FAIL|VACUOUS (<Ns>) [clippy workspace(excl 5) --all-features | clippy cqlite-core --features 33:all-compression,arrow,bench-internals,+30 more | ...] {no census — cargo clippy emits a per-crate tally only COLD; a warm run prints Finished alone}
core-tests: PASS|FAIL|VACUOUS (<Ns>) [test cqlite-core --features cli-helpers] {verified: <n> tests passed (across <k> result line(s))}
format-compat: PASS|FAIL|VACUOUS (<Ns>) [test format-compatibility-tests default-features] {verified: 10 tests passed (across 1 result line(s))}
integration-tests: PASS|FAIL|VACUOUS (<Ns>) [test cqlite-integration-tests default-features x2] {verified: <n> tests passed and <k> test binaries built/verified}
write-tests: PASS|FAIL|VACUOUS (<Ns>) [test cqlite-core --features write-support x3] {verified: <n> tests passed (across <k> result line(s))}
cli-tests: PASS|FAIL|VACUOUS (<Ns>) [test cqlite-cli default-features | test cqlite-cli --features write-support] {verified: <n> tests passed (across <k> result line(s))}
minimal-build: PASS|FAIL|VACUOUS (<Ns>) [build cqlite-core --no-default-features --features all-compression | test cqlite-core --no-default-features --features all-compression] {verified: <n> test binaries built/verified}
all-features-check: PASS|FAIL|VACUOUS (<Ns>) [check cqlite-core --all-features | clippy cqlite-core --all-features] {no census — cargo check/clippy passes execute no tests; the subject is a feature set, not a count}
pub-surface: PASS|FAIL|VACUOUS (<Ns>) [no-cargo] {no census — shell/python guard prints no AGENT-GATE-CENSUS contract line yet (#3162)}
dep-duplicates: PASS|SKIP (<Ns>) [via check-dep-duplicates.sh (cargo tree -d --workspace --target all; a metadata probe, compiles nothing): feature set NOT observed] {no census — the guard emits its own MEASURED/verdict lines, not an AGENT-GATE-CENSUS contract line (#3162)}
python-bindings: PASS|SKIP (<Ns>) [via maturin: feature set NOT observed] {verified: <n> pytest tests passed}
tooling-tests: FAIL (<Ns>) [test ws0-corpus-gen default-features | + cargo not observable: cargo may run inside ~60 nested test scripts (child processes)] {no census: component ended FAIL, so there is no PASS to affirm}
smoke: PASS|FAIL|VACUOUS (<Ns>) [build cqlite-cli default-features] {no census — smoke-test-all-tables.sh prints no machine-readable table count (#3162)}
logs: /tmp/agent-gate.<random>
logdir-disposition: REMOVED at exit on PASS; AGENT_GATE_KEEP_LOGS=1 retains
logdir-sweep: <N> REMOVED of <M> aged (>7d) under /tmp (owner ...; examined <E>, removals attempted <A>, cap 1000)
summary-file: <AGENT_GATE_SUMMARY_FILE or $PWD/.agent-gate-summary.txt>
RESULT: PASS
==== END AGENT-GATE SUMMARY ====

If --only was used (PARTIAL run — never counts as gate):

==== AGENT-GATE SUMMARY ====
commit: <short-sha> branch: <branch> dirty: yes|no
datasets: <N> Data.db files under <CQLITE_DATASETS_ROOT>
ci-pins: ...
tree-start: <head-sha12> dirty: yes|no digest: <digest12>
tree-end: <head-sha12> dirty: yes|no digest: <digest12>
tree-integrity: PASS
mode: PARTIAL (--only fmt,clippy) - does NOT count as the gate
census: <A>/<N> components AFFIRMED a count; <G> DECLARED-GAP (RECOGNISED); ... (as above)
fmt: PASS (<Ns>) [fmt workspace features=n/a] {no census — cargo fmt --all --check emits no per-file tally to count}
clippy: PASS (<Ns>) [clippy workspace(excl 5) --all-features | ...] {no census — cargo clippy emits a per-crate tally only COLD; a warm run prints Finished alone}
logs: /tmp/agent-gate.<random>
logdir-disposition: RETAINED: PARTIAL
logdir-sweep: <N> REMOVED of <M> aged (>7d) under /tmp (owner ...; examined <E>, removals attempted <A>, cap 1000)
summary-file: <AGENT_GATE_SUMMARY_FILE or $PWD/.agent-gate-summary.txt>
RESULT: PARTIAL
==== END AGENT-GATE SUMMARY ====

Every component line STATES WHAT IT VERIFIED, and a component that verified nothing cannot PASS (#3625)

Section titled “Every component line STATES WHAT IT VERIFIED, and a component that verified nothing cannot PASS (#3625)”

PASS (0s) is indistinguishable, in a pasted block, from a component that did nothing. A duration is a PROXY for work; a COUNT is the work. So every component line carries a census suffix after its feature matrix, and the block carries one aggregate census: line.

The suffix renderings a reader will actually see:

suffixmeaning
{verified: 3562 tests passed (across 41 result line(s))}an affirmative count — the component’s real subject
{verified: 2 test binaries built/verified}a --no-run lane, whose honest subject is binaries, not tests
{no census — <reason>}a DECLARED GAP: no count is derivable for this component yet, and the reason prints every run
{no census: component ended FAIL, so there is no PASS to affirm}the component did not pass, so there is no PASS to census
{census NOT-MEASURED: <reason>}the census could not be taken (an unreadable log, a suppressed cargo status). Never read as verified, and deliberately non-fatal
{verified NOTHING: <reason>}the subject count was MEASURED and is zero — see VACUOUS below

VACUOUS is a fourth component status beside PASS/FAIL/SKIP. A component whose PASS carries a measured-zero census is recorded as VACUOUS, and it fails the runRESULT is still only PASS/FAIL/PARTIAL, because the aggregation treats PASS and SKIP as the only non-failing component statuses and everything else, VACUOUS included, as a failure.

Two conventions in the aggregate line are load-bearing and are not tidied away: every non-affirmed class prints as N RECOGNISED — never a bare N — and the line declares its own non-exhaustiveness, because the gap set is curated. A component that affirms nothing is UNMEASURED, which is not the same statement as verified.

Why cargo’s warm 0s rows are legitimate, and how to tell: cargo caches COMPILATION, never test EXECUTION. A warm cargo test re-prints test result: ok. N passed, and a warm cargo test --no-run still prints one Executable line per binary. So a 0s row whose suffix carries a count really did re-verify its subject; the duration collapsed because the build was cached. That is the distinction the census exists to make visible.

Every component line NAMES the feature matrix it ran (#3453)

Section titled “Every component line NAMES the feature matrix it ran (#3453)”

Owner ruling, 2026-08-30: “the gate SUMMARY should name the feature matrix each component ran so a pasted block states what it certified.” A bare core-tests: PASS (412s) cannot distinguish a run that certified the OTLP stack from one that never enabled it — which is the whole subject of #3453 (220 cqlite-core --lib tests execute in pr-gate-core’s --all-features lane and nowhere in the gate of record). So every component line, in every mode (full, --lite, --delta), carries a bracketed feature matrix. Read it as <subcommand> <scope> <features>, one entry per distinct invocation, xN when the same set ran N times.

It is DERIVED, not curated (the #3453 block in scripts/agent-gate.sh, kept INLINE because eight hermetic self-tests build a synthetic repo by copying that one file):

  • cargo and env are shell functions in the gate, so every cargo invocation made in the gate’s own shell is described from the real argv about to execute. There is no second copy of a feature list to drift from.
  • The eight components whose cargo calls live inside a single-quoted bash -c body (core-tests’ nextest branch, memory-budget, integration-tests, write-tests, cli-tests, compaction-byte-parity, minimal-build, smoke) hoist their package/features into one variable that is expanded both into the argv and into the recorded matrix. The cargo/env interceptors are deliberately not export -f-ed: exporting an interceptor would make every bash descendant record too, so tooling-tests (which runs nested agent-gate self-tests) would attribute a nested run’s cargo invocations to itself — a false rationale in a gate log is worse than none. Each body instead calls the explicitly named recorder _fm_observe_child (which IS exported, and which intercepts nothing) with the gate’s own _fm_describe_cargo, so there is no second formatter either.

It records EXECUTION, not intent (roborev job 269, blocker 2). Those eight records used to be written by the parent, before the child body ran: cli-tests: FAIL then named both of its feature sets even when Pass 1 — or the fail-closed target derivation above it — died before Pass 2 ever started, and write-tests claimed the same set x3 after failing on the first of three &&-chained passes. A failure summary that claims an invocation which never occurred is affirmatively false, and strictly worse than silence: it is what stops the next person looking. Every record is now appended on the line immediately before its own cargo command, from inside the body, so a short-circuit records nothing later. A body that dies before its first cargo call legitimately leaves an empty sidecar, and that state is named too.

It never renders blank. A component with no observed invocation renders an explicit [UNDECLARED]; one that runs no cargo at all renders [no-cargo]; a SKIP that never reached cargo — or a FAIL before its first cargo call — says exactly that (and names the metadata-probe exclusion, because cargo tree/metadata probes are deliberately not recorded and three components FAIL exactly there). And observation beats declaration: a component declared no-cargo that is observed running cargo shows the observed sets with a !declared-no-cargo marker, so a mis-declaration self-corrects instead of being believed.

THE CLASS DECIDES WHAT MAY BE CLAIMED — four classes, and three rules that came out of one family of findings (roborev job 273).

  1. A component whose cargo runs only in a CHILD PROCESS is never class cargo. The interceptors are unexported by design, so cargo means observable in the gate’s own shell, or self-recorded from a bash -c body. tooling-tests was declared cargo while its only cargo runs inside ~60 nested test scripts — so a PASS read [UNDECLARED] and, worse, a FAIL could claim it “FAILed before its first cargo invocation” after a child cargo build really ran. It is now the fourth class, unobservable:<why>, rendered [cargo not observable: <why>]: it asserts nothing in either direction and takes no SKIP/FAIL note, because “nothing ran” is precisely what that shell cannot know. An in-shell observation rides beside it additively.
  2. An indirect:<driver> component RECORDS whether its driver was REACHED, from an explicit signal — a build-verify rc, or a recorder call on the line immediately before the driver runs — never inferred from the terminal status. python-bindings can die in venv/pip before maturin and node-bindings in npm ci before npm run build, and both used to report [via maturin: …] / [via npm run build (napi): …] for a cargo invocation that never happened. One shared helper pair does the mapping for all of them (_fm_note_driver + _fm_note_maturin_rc, plus the exported child-callable _fm_observe_driver), so a fourth indirect component cannot get the direction wrong by writing its own text. An indirect component with no record renders UNDECLARED naming the driver — a visible recording gap, never a claim.
  3. The misclassification is MECHANICALLY DETECTABLE, which is the part that stops the next round: the census that missed rule 1 read the table instead of exercising it. The guard now RUNS every cargo-class component under --only with a recording shim cargo (29 today, ~16 s for the whole guard) and treats an UNDECLARED annotation as a FAIL — either the component’s cargo runs in a child process, or a record is missing — while a component that cannot be exercised without recursion (tooling-tests runs the guard itself) must be declared non-cargo, also a FAIL.

A driver we cannot see is NAMED, not guessed (roborev job 269, blocker 1). python-bindings, node-bindings, and the --lite scoped-tests python tier — whose maturin develop runs in a child process — render [via <driver>: feature set NOT observed] once their driver is observed to have been reached, and the python tier’s entry is additive: a mixed rust+python diff reads [test cqlite-core --features cli-helpers | via maturin: feature set NOT observed], never one at the expense of the other. It is recorded only for the build-verify exit codes that mean maturin actually ran (a venv/pip failure or an absent cargo/rustc records that the tier never reached maturin). This is deliberately distinct from UNDECLARED: “nobody said” and “known to be indirect, therefore unobservable” are different facts, and only one of them is a defect.

Guard: scripts/tests/test_agent_gate_feature_matrix_annotation.sh (in tooling-tests, hermetic) asserts that every name in COMPONENTS resolves to a declared class — a new component cannot join the set with a blank matrix — that all six per-component emit sites render through the one _fm_summary_line, and, for the bash -c components, that the declared matrix equals the argv that actually executed under a recording PATH-shim cargo, described through the gate’s own _fm_describe_cargo rather than re-derived in the test. It then re-runs that same differential under a failing shim, where each body must name exactly the one invocation it reached — with the short-circuit itself proved by measurement (strictly fewer invocations than the passing run), never assumed. And it drives the real run_scoped_tests for a pure-python and a mixed route, and the real run_python_bindings against a stubbed build-verify child for each rc. RED-verified, one plant per finding: reintroducing a parent-side pre-record, dropping the python-tier record, reverting tooling-tests to class cargo, declaring a cargo-less component cargo, discarding run_python_bindings’ rc, removing (or merely MOVING) node’s in-body recorder, and dropping the note from run_scoped_tests’ record_result-bypassing exit each red a case that NAMES the planted symbol. It also refuses to certify itself vacuously: an EXIT trap fails any run that does not reach its terminal tally (measured — under real /bin/bash 3.2.57 the pre-fix guard ran 1 of 84 cases and exited 0, so a bash-4-only construct degraded it to a silent pass rather than a loud red), plus a verdict floor, plus the associative-array lint over every gate-invoked script in test_agent_gate_summary.sh section 8c.

A long feature list is abbreviated, and the abbreviation declares itself: 33:all-compression,arrow,bench-internals,+30 more, never a silent truncation. Beyond six distinct sets the remainder renders as +K more sets.

A mid-run tree mutation invalidates the run (#2926)

Section titled “A mid-run tree mutation invalidates the run (#2926)”

The gate captures a tree identity at start — HEAD, the dirty flag, and a content-sensitive digest of every uncommitted tracked change plus every untracked, non-ignored file — re-verifies it at every component boundary and once immediately before the terminal emit, and FAILs CLOSED on any mismatch:

tree-integrity: FAIL (tree-mutated-midrun; head <a>→<b>; changed: <paths…> (+N more); detected-after-component: <c>)
RESULT: FAIL

Why it exists: commit:/dirty: used to be stamped by a fresh git rev-parse / git status at emit time, so a worktree edited while the gate ran emitted a block attributing mixed-tree results to the final sha — indistinguishable from a real certification. This is reachable without breaking the one-worker rule (#1930): a lead legitimately runs a closer (gating) and a fixer (editing) that overlap on one worktree.

Contract:

  • A closer MUST read tree-integrity: alongside RESULT: before trusting a summary. RESULT: PASS with anything other than tree-integrity: PASS cannot occur; a block whose tree-start:/tree-end: digests differ is not a certification.
  • The commit: line is derived from the verified terminal capture, never from a fresh git read at emit time. The only sha a block can name is one a validated capture observed — so a HEAD move landing between the capture and the emit can no longer be certified. When no validated capture exists, the line reads commit: unverified … dirty: unverified and the run is already FAIL-closed.
  • The guard covers the full gate, --lite, --delta and --only. Only --list, --python-build-verify, the concurrency stub and the self-test emission modes are exempt (they stamp a synthetic selftest identity).
  • The startup INCOMPLETE sentinel carries tree-start: (and no tree-end:), so even a killed gate records the tree it began on.
  • There is no bypass. No environment variable turns a mutated run green. The one knob, AGENT_GATE_TREE_HASH_CAP_BYTES (default 8 MiB), only caps content hashing of oversized untracked files and is itself stamped as tree-hash-cap:.
  • Exclusions are the repo’s own .gitignore rules plus the run’s own summary file. One named non-fatal class: a Cargo.lock-only difference stamps tree-integrity: PASS (lockfile-settled: …) (the gate runs cargo without --locked, #2962); a lockfile change alongside anything else is fatal.
  • Stated limitation: gitignored inputs — chiefly the fetched test-data/datasets/** SSTable binaries — are outside the digest. Their stability is covered by the existing datasets: and ci-pins: stamps, not by this guard.
  • Recovery: re-run on a stable tree. The FAIL names the changed paths; see docs/development/gate-ops.md.

The agent gate proves a change is correct; the parity CI tiers define what each Cassandra-parity gate promises. The two are read together: see docs/development/parity-ci-tiers.md for the per-tier contract (purpose, accepted evidence.type, skip/failure policy, artifact retention, promotion rules) and the gate-strength classification — smoke vs canonical-semantic vs byte-for-byte — that bounds what a green gate can claim. Smoke alone cannot satisfy a P0 data-loss scenario without a recorded gap. Before publishing a broad public parity claim, run docs/development/parity-release-checklist.md. A fast-PR cross-check (cargo run -p cassandra-parity -- tier-contract-check) keeps the documented tier enum in sync with the code (enums::CI_TIER) and the manifest schema (issue #1022).

The gate reads dataset pins from .github/workflows/sstabledump-parity-gate.yml and includes them in the summary block as ci-pins. Local validation must target the same asset CI uses. Current pins (as of the script source):

DATASET_TAG: datasets-v3
DATASET_ASSET: cassandra5-small-full-v3.4.tar.gz
DATASET_SHA256: 3cae644360e0142a6bb5e96ddab445ff18e3478e7058104842ce1a455fba8a33

See Test data for how fetch-datasets.sh uses these pins and why the SHA256 is the cache key.

core-tests skips one test (test_legacy_format_allows_blob_fallback_with_feature) that requires a feature flag incompatible with cli-helpers. This skip is listed in the script explicitly — it is not a silent omission.

The minimal-build component verifies the library compiles without the query engine (--no-default-features --features all-compression). This catches feature-gate regressions that clippy --all-features won’t find.