Skip to main content

CI gates

Convention without enforcement rots. Every rule in AGENTS.md that an agent could plausibly violate has a paired CI gate that fails the PR if it slips. This page is the single source of truth for what those gates are, where they live, and how to bypass them safely.

Most of the mechanical gates (rows 1, 3–10, 14, 16) live as steps inside the single Quality Gates job of .github/workflows/ci.yml — but Quality Gates is one job among several parallel top-level jobs in that same workflow file, not the whole pipeline. Lint · Typecheck, Build, Test · CI, E2E Smoke and Verify Migration Back-Compat are each their own job (see rows 2, 11–13, 19), and Verify Release Migrations / Verify Release KB Curation are release-PR-only jobs. The source-PR jobs run on every PR targeting develop or an epic/* integration branch (§ Epic integration branches); the migration back-compat gate additionally covers hotfix PRs to main. A release/* PR is exempt from that gate only when it contains the current origin/develop wave and has no additional migration delta.

The gates​

#GateScriptWhat it enforcesBypass marker
1Formatnpx prettier --checkRepo-wide Prettier compliance.— (fix it: prettier --write)
2Migration tracking (Build job, not Quality Gates)npx tsx scripts/check-migrations.tsEvery Prisma migration in */migrations/ has a tracking row in the right schema_migrations table. Guards against the 2026-05-10 dual-tracking-table footgun. Runs as the "Verify migrations" step after npx turbo run build in the Build job.—
3Audit coverage lint(in lint step)Security-sensitive mutations use auditCritical(); routine ones use auditAction().— (per-callsite // audit-coverage: skip <reason> comment)
4OpenAPI freshnessnpm run check:openapiCommitted apps/project-tracker/openapi.json matches the Zod schemas. Run npx turbo run generate-openapi --filter=@constellation/project-tracker to regenerate.—
5Changeset presentnpx tsx scripts/check-changeset.tsEvery PR with user-visible impact adds a .changeset/*.md fragment.[skip changeset]
6PR requirementsnpx tsx scripts/check-pr-requirements.ts(a) Title/body cites a ticket; (b) feat: PRs add/modify a spec; (c) added/modified .ai/specs/*.md matches the spec format (INF-107).[skip ticket], [skip spec], [skip spec-format]
7AC traceability (INF-110)npx tsx scripts/check-ac-traceability.tsEvery numbered AC in a changed .ai/specs/*.md resolves to a code anchor (→ file:symbol), file anchor (→ path/to/file.md), UI marker (→ <!-- ui: ... -->), or explicit waiver (→ <!-- deferred: ... -->).[skip ac-traceability]
8Docs-sync (INF-111, warn-mode v1)npx tsx scripts/check-docs-sync.tsBehaviour-changing source files (routes, services, schema, event contracts) have a paired apps/docs/docs/** edit. Warn-only in v1 — flip to hard-fail with DOCS_SYNC_ENFORCE=1 after a tuning window.[skip docs]
9Scripts unit testsnpm run test:scriptsVitest run against scripts/**/*.test.ts (the parsers / validators / CI gates). Runs in two scopes since INF-510 — see the path-scoping note below.—
10Route wrappingnpm run check:routesEvery route in catalog / directory / wiki is wrapped with authedRoute / authedRouteWithParams.Per-route // @route-wrap: skip <reason>
10bAgent-guidance docs (INF-179)npm run check:agent-docsAGENTS.md ↔ .claude/skills / .claude/agents alignment: path references resolve, the task router and skill/agent files stay cross-referenced (no orphans either way), every skill/agent carries name + description frontmatter, and every agent definition's instructions agree with its tools: grants — a named MCP tool or pt command must exist and be granted, a granted MCP tool must be named by a step, and a pt example is read in one supported notation (SPEC-inf-483 § Supported notation) with anything outside it reported rather than guessed at; the starter-kit agent templates get only this last rule (INF-483).[skip agent-docs]
11Lint + typecheck (own Lint · Typecheck job)npx turbo run lint typecheckESLint + TypeScript strict across the monorepo.—
12aBuild (own Build job)npx turbo run buildFull monorepo build, plus the migration-tracking check (row 2) as a follow-up step.—
12bTest (own Test · CI job)npx turbo run test:ciUnit + integration + property tests, run in parallel with Build against a real Postgres + MinIO.—
13E2E smoke (own E2E Smoke job)Playwright saved-filters.smoke.spec.tsSaved-filters POST → GET → DELETE round-trip against a seeded next dev (PT-26 regression gate).—
14Wiki MCP boundarynpx tsx scripts/check-wiki-mcp-boundary.tsWiki MCP tools stay within the documented boundary.—
15Verify release migrations(release PRs only)Both staging + production DBs have every migration applied before the release PR opens. See .claude/skills/release-and-migrations.— (rerun after applying)
16Review threads resolvednpx tsx scripts/check-review-threads.tsNo unresolved review comment thread on the PR (mirrors branch protection's required_conversation_resolution flag as a visible CI status). Author-agnostic — any unresolved thread blocks, including a reviewer's own. Only re-runs on pull_request activity, so resolving threads without a new commit needs gh run rerun --failed to refresh the status.[skip thread-check]
17Verify release KB curation (PLT-317)scripts/check-release-kb-curation.ts (release PRs only)The release PR body carries the own-line marker KB curation pass: <YYYY-MM-DD> — <who ran it; outcome summary>, with the date matching the release branch date — proof the per-release wiki-curator judgement pass (PLT-301) actually ran.— (no marker bypass; run the pass)
18Dependency CVE gate (INF-396)npm run check:dependency-auditFails on a high/critical advisory against a production dependency, keyed on the GHSA id rather than the package (npm reports a package's MAXIMUM severity, so a by-package waiver would silently accept its future advisories). Reads the committed lockfile and reports every resolved copy. Its scope is pinned on the command line — one --workspace= per workspace, enumerated from the lockfile — so no .npmrc layer can narrow what is audited; a lockfile yielding no workspaces is an error, never an empty scope. PR-scoped to diffs touching a manifest, a lockfile, the waiver record, the checker or its workflows; dependency-audit-nightly.yml re-audits develop daily. Unfixable advisories go in scripts/dependency-audit-waivers.json with a GHSA id, rationale, review date and the exact resolved copies reviewed.Per-advisory waiver record
19Migration back-compat (PT-927)npm run check:migration-compatMigrations apply before the code deploys, so every migration must work against the outgoing build. Shape-triggered, not blanket: it fires only on a migration not yet on main that reads an app.* GUC the deployed build does not set, reaches NOT NULL with no syntactically usable DEFAULT, or drops/renames a name the old build may still use. "Does the deployed build set it?" is answered from the curated registry scripts/deployed-app-gucs.json, whose every entry names the commit that shipped its setter — verified to be an ancestor of origin/main, so listing a GUC in the same PR as the migration that reads it does not help. Each flagged file then needs one own-line Migration back-compat: <path> — <why the outgoing build is safe> in the PR body — path-scoped, so a later migration inherits nothing. Covers every source PR, as well as hotfix PRs to main; a release-named PR is skipped only when it contains current origin/develop and no migration delta beyond that wave. The shapes it cannot decide (a new trigger or CHECK constraint, a changed column meaning, a backfill correct only under the new code, or execution-dependent default semantics) are printed on every run and stay a review responsibility.[skip migration-compat]
20Cross-model evidence (INF-348 / INF-371 / INF-617)npm run check:codex-evidenceA source PR to develop or an epic/* branch, and a release/* → main PR, must carry cross-model evidence: Codex review: for the native Codex loop, or strict Claude review: for Codex-led ordinary develop-bound PRs (forms below) — in a PR comment by the PR author or a non-bot OWNER / MEMBER / COLLABORATOR (INF-485), or in the body. Enforced: a real calendar date neither in the future nor earlier than the PR; both counts attached to their field names (2 rounds, 5 findings — a bare number could be a ticket id); and the head SHA the review covered, an abbreviation of 7+ characters matched against the PR head. The SHA binding (INF-371) is what makes a green check mean the pass covered this code rather than a pass was claimed.[skip codex-review]
21Migration number collisions (PT-925)npm run check:migration-collisionsNo two migrations in one directory share a leading number. Covers all eight migration directories and both layouts, mirroring each module runner's own predicate — including that the directory layout (NNN_name/migration.sql) is understood by project-tracker's runner alone, so such an entry elsewhere is reported as never-applied rather than counted. A letter suffix is a distinct version, never a collision (029b sorts between 029 and 030 by design, PLT-653). The 23 pre-existing shipped collisions are grandfathered by exact file set, not by number, so a third file at an already-grandfathered number still fails; an entry matching no on-disk collision, or naming an unconfigured directory, is itself an error. Runs in Quality Gates and as gate 19 of the pre-push local tier — the collision exists only in the union of two branches, so a purely local run cannot see it.None — renumber the unmerged migration

Bypass-marker matching​

Skip markers are matched strictly so a PR description that documents a marker doesn't accidentally trigger the bypass:

  • In the title — any substring match counts (titles are short and intentional).
  • In the body — the marker must appear on its own line, trimmed. Inline mentions, backticked formatting (`[skip X]`), and prose like "use [skip X] to bypass" do NOT trigger the bypass.

Use markers sparingly and only when the rule genuinely does not apply. Every bypass should carry a written reason next to it so reviewers can judge intent.

The newest gates in detail​

AC traceability — check-ac-traceability.ts (INF-110)​

Reads every .ai/specs/*.md in the PR diff, parses the ## Acceptance criteria section, and verifies each numbered AC ends with one of four anchor forms:

Anchor formMeaningVerification
→ path/to/file.ts:symbolCode symbol is present in the diffGreps the file (at HEAD) for symbol definitions / exports / property heads.
→ path/to/file.md (no colon)Non-code file is present in the diffChecks the file appears in the PR's changed-file set. Use for AGENTS.md edits, agent / skill / workflow files, docs pages.
→ <!-- ui: <route> -->UI-only behaviourGated on Playwright smoke instead.
→ <!-- deferred: <reason> -->Explicit waiverSurfaces in PR body as a risk.

The script runs locally too: npx tsx scripts/check-ac-traceability.ts <spec-path> .... Exit code is 0 (clean) or 1 (failures).

Why the gate exists. The post-impl-reviewer subagent does AC mapping manually; this gate makes the mapping mechanical so even a PR opened without invoking the reviewer cannot silently miss an AC. INF-110.

Cross-model evidence — check-codex-evidence.ts (INF-348 / INF-371 / INF-617)​

The job remains Verify Codex Evidence. It accepts native Codex review and the Codex-implements / Claude-reviews ordinary source-PR flow. It cannot prove which model ran or the review coverage; it makes the claim explicit and attributable, so a false marker is a deliberate false statement rather than a silent omission.

The existing Codex form names the commit the review covered (INF-371):

Codex review: 2026-07-29 — 2 rounds; 5 findings, 4 fixed; converged — 4f2c9ab
  • A marker citing a SHA that is not the PR head always fails, with no phase-in. Naming the head is sufficient — the summary is free prose, so it may also carry a previous head or a fixing commit.
  • A re-run is needed when the head moves, and only then. Refuting a finding or editing the PR body changes no code and leaves the marker valid; any code change invalidates it, which is why a round's fixes should be batched into one push.
  • Phase-in. A marker citing no SHA is grandfathered only where both the PR and the marker predate 2026-08-24. The test reads the PR's own dates, never the wall clock, so re-running the gate later always returns what it returned before. A grandfathered marker is not head-bound: later pushes neither invalidate it nor are covered by it, so a green check on such a PR does not mean the latest commits were reviewed.
  • Record it as a PR comment (INF-485). The gate reads the marker from the PR's issue comments as well as its body. A comment counts when its author is the PR author or a repository OWNER / MEMBER / COLLABORATOR and is not a bot account — the population that could already have edited the body, minus machine output that may quote a marker without attesting one. A comment fires no pull_request event, so recording a round cancels no in-flight CI run and starts no multi-LLM re-review, which a body edit did. The body stays readable for PRs open during the transition; the SHA rule applies to both sources, and the phase-in only to body markers — a comment marker is never grandfathered, whatever date it carries.
  • Order when recording it. If the commit is still local, post the comment citing it, then push — the push's run passes. If the reviewed head is already pushed and the round is clean, post the comment and re-run the red job (gh run rerun <run-id> --failed); the job reads the comments live. If the round found something, push first so the findings can be anchored.

Full workflow: codex-review.

Claude subscription form (INF-617 — Codex-led ordinary source PRs)​

This form applies only to ordinary source PRs targeting develop; release, back-merge, epic/*- and main-targeted PRs retain the native Codex/baton contract, because the Claude launcher reviews develop-bound PRs only. Codex retains implementation and fixes; Claude reviews through the installed subscription-authenticated CLI. The local launcher emits this strict marker only after a validated converged run:

Claude review: YYYY-MM-DD — N rounds; N findings, N fixed, N refuted; implementer: codex; converged — FULL_HEAD_SHA

Eligibility uses the event head/base commits and complete fetched origin/main history. A head already on main or importing main ancestors absent from the event base is ineligible, regardless of its branch name. Every head/main merge-base is checked. Missing, shallow or unreadable history disables Claude acceptance; native Codex/baton evidence and the existing skip contract remain available. The evidence job starts with a depth-one checkout. Only an otherwise eligible PR with an own-line Claude candidate in the body or a trusted comment acquires complete event head/base, checkout and main ancestry, requesting commits without historical file contents. Fenced/untrusted candidates do not trigger the fetch. A failed refresh disables Claude even if old local history remains usable. Context-free parser calls default to disallowing Claude unless their caller explicitly establishes eligibility.

The date and trusted-comment rules above apply. This grammar additionally requires positive rounds, nonnegative integer counts, fixed + refuted = findings, at least two rounds when findings exist, exactly implementer: codex, the explicit affirmative outcome and a matching 7–40-character head SHA. The launcher emits the full 40-character SHA. There is no historical no-SHA waiver. Missing fields, negative outcomes, wrong roles, contradictory counts and stale heads cannot supply valid Claude evidence. A valid initial review with no findings can converge in one round; that initial round includes two independent complete passes.

The Codex implementation lifecycle is the canonical procedure for tickets, specs, subscription authentication, validated JSON context, macOS enforcement, tests, timed review passes, saved-receipt confirmation and evidence publication. The launcher requires a claude setup-token subscription credential in CLAUDE_CODE_OAUTH_TOKEN, with an isolated home and temporary runtime; it grants no caller credential files. Its macOS sandbox denies host reads and writes by default. The literal filesystem root is a verified startup exception: its top-level entry names can be listed, but their contents and recursive access are not granted. Narrowing only / to metadata access made both supported Node20 and native Claude2.1.153 abort before startup. Mixed parent listings and unrelated host-file reads remain denied. The initial diff uses the captured live develop tip's merge-base with HEAD and must be nonempty; the guidance base must belong to develop and be no older than that canonical merge-base. Version-2 receipts retain recomputable lineage. Version-1 receipts and the earlier unpublished version-2 shape with previousDigest require a fresh initial review. Local receipts and cumulative dispositions are editable and unauthenticated; structure/lineage checks cannot prove historical review or detect edited statuses. An unchanged-tree confirmation, including an empty commit, may have an empty delta and permits only open or refuted, with exact prior-closed repeats at the same HEAD retained under their existing IDs. New-head repeats remain visible for regression triage; this deliberate choice can prevent automatic convergence. Incorporating develop and moving the merge-base requires a fresh initial review without --previous, using current guidance. SIGHUP/SIGINT/SIGTERM and first-pass failures terminate all review process groups and verify they are absent or zombie-only before cleanup. Final-kill verification has a five-second deadline independent of output-pipe closure; live/unknown state or incomplete stream drainage fails with a cleanup diagnostic, retains snapshot/runtime scratch and produces no evidence. Sibling cleanup failures remain visible alongside the original failure. Bounded client output supplies safe diagnostic categories based on structured service codes, never model prose. A trusted Node supervisor anchors each group, alone owns its signals, and terminates it on launcher IPC loss, including SIGKILL. Native children inherit no liveness channel and retain the existing sandbox. After abrupt death, scratch may remain for inspection; verify process termination before deleting it. The supervisor stays directly executable by Node 20 using Node built-ins and one pure repository-owned CJS start contract. The supervisor and Zod start schema use the same validator and byte/deadline limits; response/control types remain shared through JSDoc. Strict check:scripts-types, both modes of format:check, and the advertised npm run format repair cover CJS. Producer-drift tests compile their mutations together once; the ordinary typecheck gate validates unmodified code. The Claude review macOS boundary CI job runs on every PR targeting develop, main or an epic/* branch, using npm run test:claude-review-macos from the CI-only checklist. Title/body-only edits skip the separate Claude review macOS edit caller, allocating no runner and creating no boundary check. Job names are literal: GitHub may leave a skipped job's name expression unevaluated. Branch retargets and edits without recognizable title/body change data call the same-commit local boundary workflow, producing Claude review macOS edit / Claude review macOS boundary. Only the boundary workflow owns concurrency, so metadata skips cannot cancel or replace verification. Direct lifecycle and manual runs keep the original boundary name. Tests assert both workflow files; verify emitted names with GitHub's API. It runs real Seatbelt tests with a fake CLI and no subscription secrets. Portable Git and parser tests still run on Linux; their results do not establish macOS enforcement. The runner writes reports locally; the implementing session posts findings and the generated own-line marker. Local receipts and CI markers are attestations, not cryptographic proof. Existing CI reviewers, thread gates and human merge approval still apply.

Baton form (INF-452 — experimental, opt-in)​

The Codex review: marker line may carry the reserved token baton plus labelled fields when a PR used the opt-in baton pass (reviewer fixes bounded confirmed findings; the implementer confirms the delta):

Codex review: 2026-08-11 — 2 rounds; 5 findings; baton: implementer: claude, reviewer: codex, fixer: codex, confirmer: claude; reviewed 4f2c9ab, fixed 82ac31d, confirmed 9c01dde; converged — 9c01dde

Once the token appears the line fails closed unless it is complete: all four actors and all three SHAs exactly once (initial reviewer is accepted for reviewer); fixer ≠ confirmer and implementer ≠ reviewer after Unicode-aware normalization; a non-zero findings count (a clean review has nothing to hand off); and confirmed abbreviating the PR head exactly as the classic citation does. reviewed/fixed are recorded attestation only — the baton parser does not validate their ancestry or review execution. New failure reasons: baton-missing-field, baton-same-actor, baton-confirmed-not-head. A malformed baton line blocks even a conforming classic line beside it; a stale-but-parseable one is superseded like any stale classic marker. Because the token is reserved, an honest classic summary must not use the word baton. This attests roles — it does not prove who controlled either CLI. Classic Codex markers retain their existing grammar. The baton remains an explicit opt-in for role swapping, separate from the ordinary Claude route.

Docs-sync — check-docs-sync.ts (INF-111)​

Maps behaviour-changing source-file patterns to required docs surfaces:

Source patternRequired docs surface
apps/catalog/src/app/.../route.tsapps/docs/docs/modules/catalog/
apps/catalog/src/server/services/apps/docs/docs/modules/catalog/
apps/directory/src/app/.../route.tsapps/docs/docs/modules/directory/
apps/directory/src/server/services/apps/docs/docs/modules/directory/
apps/wiki/src/app/.../route.tsapps/docs/docs/modules/wiki/
apps/project-tracker/src/app/api/.../route.tsapps/docs/docs/modules/project-tracker/ OR openapi.json regen
packages/platform/*/src/.../index.tsapps/docs/docs/packages/ or apps/docs/docs/architecture/
packages/contracts/src/events/apps/docs/docs/reference/events.md
prisma/schema.prisma or */migrations/apps/docs/docs/modules/ or apps/docs/docs/architecture/

If a PR touches a mapped source pattern without also touching a matching docs surface, the gate flags it. Warn-mode in v1 — exits 0 with warnings so we can tune the mapping before forcing hard-fails. Flip to hard-fail by setting DOCS_SYNC_ENFORCE=1 (per-PR via env, or globally once the false-positive rate is acceptable).

Why warn-mode. The mapping is intentionally narrow on the source side and broad on the docs side. Tuning needs real PR data — better to ship the gate as a warning, gather feedback, and harden than to ship hard-fails and have everyone start using [skip docs] reflexively.

No .kiro/ directory — check-pr-requirements.ts sub-check (d) (INF-312)​

The Kiro IDE wrote spec bundles into .kiro/specs/; it was retired (INF-119) and its history archived under .ai/specs/archive/kiro/ (INF-312). Sub-check (d) of the PR-requirements gate fails any PR that (re)introduces a .kiro/ directory at any depth — the failure mode that actually happened, where a second .kiro/ root grew unnoticed under apps/project-tracker/. Write new specs as .ai/specs/SPEC-<key>-<slug>.md.

Two properties worth noting:

  • Universal. Unlike checks (a)–(c), which are source-PR rules exempt on release/hotfix heads and on bases other than develop and epic/*, sub-check (d) runs on every PR — including release/hotfix PRs to main — because reintroducing .kiro/ is wrong everywhere.
  • git-status based, not a filesystem probe. It reads git's own post-state accounting (git diff --name-only --diff-filter=d -M -z: additions, modifications, and rename targets; deletions and rename-aways excluded), then does a pure string match for a .kiro/ path segment. That is deliberately not an fs existence check — an fs probe leaks against exactly the paths this must catch (a dangling symlink, which existsSync follows to a missing target; a non-UTF-8 filename byte, which lossily decodes to U+FFFD). A string match over git's post-state has neither blind spot, and lets INF-312's own diff (which only deletes/renames .kiro/ away) pass.

Bypass: [skip kiro-guard] (a deliberate revert that re-creates .kiro/ is correctly flagged and must carry this marker).

The pre-PR self-review hook (INF-114) — not a CI gate, but enforced​

.claude/hooks/pre-pr-self-review.sh runs on the agent's local machine, not in CI. It intercepts gh pr create Bash calls and refuses to let the call through unless:

  • The PR body contains a <details><summary>Self-review</summary>...</details> block, AND
  • The **Verdict:** line inside is not blocked.

Bypass: [skip self-review] (with the standard title-substring / body-own-line semantics).

The hook is documented in detail on the Subagents page. The pairing matters: developer subagents invoke post-impl-reviewer, embed its verdict bar, and the hook then validates that they actually did before letting the PR open.

Epic integration branches (INF-665)​

An epic/<name> branch collects one epic's task PRs before a single PR carries the whole epic into develop. A PR into it is a source PR and gets every gate this repository runs on a PR to develop: the workflows admit epic/** wherever they admit develop, and the gate scripts share one predicate, isSourcePrBase in scripts/lib/pr-base.ts, whose workflow twins scripts/lib/pr-base.test.ts keeps in step. SonarCloud and the Vercel previews are configured outside the repository and are not covered here. Two things still differ from develop:

  • No push CI. A merge into an epic branch starts no run of its own. Each PR's CI tests its merge result, and the final PR into develop runs every gate again.
  • Codex evidence only. The Claude-review form of cross-model evidence stays develop-only, so an epic PR records a native Codex review: marker.

Which checks GitHub requires before a merge is a repository ruleset setting, not code. A ruleset can only require a check that runs, which is what this section provides, and it lists only some of them. So whoever merges into an epic branch must still treat every check being green, and every review thread being resolved, as merge conditions: check-review-threads fails open when it cannot read the threads and relies on a branch-level rule to enforce.

Branch protection: the enforcement layer (INF-272)​

Every gate in the table above only runs. Whether it can actually block a merge is a separate GitHub setting — "required status checks" on the develop and main branch-protection rules — and the two can silently drift apart. INF-272 found exactly that: Quality Gates and E2E Smoke were both advisory at the merge button (not in the required-checks list), and none of the real-Postgres integration suites (wiki / catalog / pt / directory / events / platform-db, rows folded into Build · Test) were required at all, despite this page and AGENTS.md describing them as gating.

.github/branch-protection/ is the checked-in contract — develop.json / main.json, each a valid GitHub "Update branch protection" request body — plus a README.md covering the open decisions (required approving review count, CODEOWNERS enforcement, strict/up-to-date-branch requirement). A weekly scheduled job (.github/workflows/branch-protection-drift.yml) compares the live GitHub settings against that contract and fails loudly on divergence — a drift detector, not a per-PR gate, mirroring the existing schema-parity.yml convention. Applying the contract to live settings is a deliberate, human-run step (scripts/apply-branch-protection.sh) — no agent applies a branch-protection change directly.

Running the full pipeline locally — npm run prepush (INF-354)​

One command runs the local tier of the checklist:

npm run prepush

It parses AGENTS.md § Pre-Push Checklist and executes the numbered list it finds — there is no second copy of the inventory to drift (the runner and the check:agent-docs reconciliation read the same lines through one parser, scripts/lib/prepush-gates.ts). It runs all gates even after a failure and ends with a summary naming each gate and its result; pass --fail-fast to stop at the first failure, --list to print the gates without running them.

Two tiers (INF-421)​

The checklist carries a local tier — the numbered list, which npm run prepush executes — and a ### CI-only gates tier, which it deliberately skips. The CI-only entries are enforced on the PR instead, and check:agent-docs still reconciles them against the workflows, so a gate cannot quietly stop running in both places.

Three gates sit in the CI-only tier: npx turbo run test:ci, npx turbo run build, and npm run test:scripts. The rule for earning that tier is cost-weighted, not hit-rate-weighted — a gate moves there when running it locally is expensive or unreliable and CI enforces it, so no coverage is lost and only the moment of discovery moves.

One gate in that tier is additionally path-scoped under ADR-029: npm run test:scripts runs --scope default on every PR, while --scope baton — scripts/claude-pr-review.test.ts, the INF-452 review-baton suite — runs only when a PR touches the adapter surface, plus unconditionally on every push to develop/main. Be exact about what that backstop buys: it is detection, not prevention. A push workflow starts after the ref has moved, so a change the filter failed to match lands first and reddens the branch. A path scope therefore spends latency and the assurance that a protected branch is green at the moment of merge — which is why ADR-029 attaches four conditions (a push backstop, a machine-checked trigger, fail-open on unknown scope, and a measured justification) rather than allowing it generally. A cheap fails-closed guard that has never fired is a guard working, not idling, and stays local. Rationale and the measured evidence: ADR-027.

The local tier's cost depends on what your edit invalidates, because lint and typecheck hash $TURBO_DEFAULT$. Measured on a quiet machine for the then-18-gate list (PT-925 later added gate 19, check:migration-collisions, at ~0.7s — the figures below predate it and were not re-measured):

your working treetotal gate timewall
unchanged (Turborepo cache hit)46.0s49s
one app edited (20 of 100 lint+typecheck tasks)180.0s3m00s
a platform package edited (61 of 100)271.9s4m32s
fresh worktree, cold cache—≈4m50s

Before INF-421 the list was 19 gates with a 17-minute observed median. Gate 2 (npx turbo run lint typecheck) is the whole of that variation — the other seventeen gates total ~85s in every case. One further cost is not in these numbers and will dominate if present: the INF-393 machine-wide lock, which serializes gate runs so a concurrent session's run is queued ahead of yours.

The stamp and the two hooks​

On green, the runner writes a success stamp keyed to your tree state — HEAD, the resolved origin/develop, working-tree status and diff, untracked file contents, package-lock.json, and npm's installed-tree marker. Change anything (edit, commit, fetch that moves develop, re-install) and the stamp goes stale; re-run npm run prepush. A failed run clears any existing stamp, and a run clears it before the first gate too, so the stamp always means "a completed green run at this state".

Two hooks compare that stamp (they never run gates — the comparison is ~250 ms):

HookCoversInstalled by
.claude/hooks/pre-push-gate.shagent sessions (Bash-tool pushes)tracked .claude/settings.json — nothing to install
.git/hooks/pre-pushhumans, editors, every other toolnpm run hooks:install, or automatically via prepare on any plain npm install

If you install with npm ci --ignore-scripts, prepare never runs — run npm run hooks:install once per clone (per worktree if your core.hooksPath is a relative path; the installer detects which case applies and says so).

Bypassing the hooks — not the checklist​

Constitution §10 still requires every gate to pass before a push, and a red gate is blocked however spurious it looks. A bypass skips the stamp comparison — for a verdict that exists but cannot be stamped (e.g. a background git fetch invalidated it, or a --dry-run that publishes nothing) — and using one is an attributable act to name in the PR:

To skipUse
the git hookgit push --no-verify
the Claude Bash hook only[skip prepush] anywhere in the command
bothCONSTELLATION_SKIP_PREPUSH_GATE=1, or --no-verify (honoured by both)

If a class of push genuinely should not need the checklist, that is a change to §10 and needs an ADR — not a bypass habit.

Gates the aggregate deliberately does not run​

The real-Postgres integration suites and the Playwright smoke tests need an environment a bare checkout lacks; they stay CI-enforced. Run them yourself when your change touches migrations, RLS, routes, or a GUI path — see AGENTS.md § Suites CI enforces. The PR-context gates can still be exercised individually:

npx tsx scripts/check-ac-traceability.ts # CI mode — needs GITHUB_BASE_REF=develop
npx tsx scripts/check-ac-traceability.ts .ai/specs/SPEC-foo.md # CLI mode
npx tsx scripts/check-docs-sync.ts # warn-mode
DOCS_SYNC_ENFORCE=1 npx tsx scripts/check-docs-sync.ts # hard-fail mode

See also​

  • Subagents — the reviewing subagents that emit the verdict bar these gates consume.
  • Workflow — the end-to-end flow that runs these gates.
  • Architecture rules — the underlying hard rules each gate enforces.
  • Constitution — the invariants the rules derive from.