Docs

Gates

Ring 0 push checks

The 14 checks that scan the whole repository and run for the first time when you push, before anything leaves your machine. Each runs again in CI on the pull request.

14 checks
Ring 0
Pre-push
Full scan

How the push hook runs

The managed pre-push hook (.vinaya/hooks/pre-push) runs four steps in order:

  1. 1

    Format

    Biome, over the files changed since the remote base.

  2. 2

    Record what is being pushed

    It reads git's pre-push input into VINAYA_PUSH_REFS and exports it.

  3. 3

    Run the Vinaya checks

    vinaya check --all --local, with VINAYA_HOST=hook set. This is the gate.

  4. 4

    Typecheck and test

    bunx turbo typecheck --affected, then the test files the import graph selects, plus any in prePush.alwaysRun.

The gate passes --local, which skips every check that declares requiresOpenPr. It passes neither --skip-full nor --diff-only, so it runs every Ring 0 check:

  • the 11 scope: 'diff' checks that already ran at commit (see Ring 0 commit checks);
  • the 14 scope: 'full' checks below, which the commit hook skips (skip reason full-scope, pre-commit) and which run for the first time here.

main-branch-refusal is one of the 11 commit checks. It is listed on this page too, because it refuses a push as well as a commit.

--diff-only never skips a scope: 'full' check, so all 14 run again in CI.

Where they run again

RingWhereTrigger
Ring 0pre-push hook only (skipped at commit)git push
Ring 1vinaya-checks.yml, job vinaya check --all --diff-onlypull_request types opened, reopened, edited (not synchronize)
Ring 2None of these checks. Direct-main-push detection and the dead-branch audit are separate jobs, not checks. See Ring 2 audits.—

None of these 14 declares validates, so none runs on the pull-request write path.

Checks

coherence

A task branch, its tranche, its Issue and the forge must tell one story, so drift between them is refused before more work lands on it.

RefusesDrift in the current task branch's tranche, read from the forge: closed-without-merge, archived-without-provenance, auto-close-misfire, phantom Issue reference, orphan task, #TBD in an active tranche, dispatched on unmet dependencies, missing rationale field, Surface excluding a bound doc, Surface overlap, milestone drift, tranche not archived, dangling or duplicate .vinaya/doc-owners entries, forge read unavailable. Runs the sub-checks A1, A3, T1, T2, T3, D1, R1, L1 and L3.
RunsRing 0: push. Ring 1: CI.
ReadsAEG_REPO, BRANCH, GITHUB_TOKEN, GH_TOKEN (all optional; they fall back to git remote, git rev-parse and gh auth token). Live forge facts, including open Issues by label. .vinaya/projects.md for R1. Full scan, timeout 30 s.
OutcomesExit 0 off a task/<tranche>/... branch. Exit 1 when owner and repository cannot be resolved or forge facts are unavailable (severity:infra). Exit 1 for each failed sub-check. A warning when .vinaya/projects.md has no rows.
ImplementsVinaya-specific
SettingsgateCutovers.objectivesSinceIssue (R1); principals (trust anchor, A1); .vinaya/projects.md; checks.coherence

dispatch-readiness

A task may only be worked when its Issue, dependencies and conflicts allow it, so a push from a task that is not ready is refused.

RefusesA task whose Issue is missing, whose rationale fails, whose dependency is unresolvable, unmerged or unreadable, whose conflicting task is open or in flight, whose task number is claimed by more than one open Issue, or that is absent from the task list derived from the forge. With PREMISE_FILE, also a failed Premise: pin in that file.
RunsRing 0: push. Ring 1: CI. The dispatch loop also runs it directly.
ReadsAEG_REPO, BRANCH, GITHUB_TOKEN, GH_TOKEN, PREMISE_FILE (all optional). Optional <tranche> <n> arguments. The live forge. Full scan, timeout 30 s.
OutcomesExit 0 with a warning off a task branch with no arguments. Exit 1 on any blocker or severity:infra failure. Exit 0 when ready.
ImplementsVinaya-specific
Settingsprincipals (trust anchor); checks.dispatch-readiness

registry-gates

The gate registry, the hooks, the workflows and the doctrine must describe one set of gates, so any disagreement among them is refused.

RefusesG1: a row's implementation that does not resolve, or a hook script the doctrine names that is not shipped. G2: a hook or CLI file no row names, or a leftover scaffold placeholder. G3: a file that crosses to GitHub that no Ring 0 row names. G4: a #NNN cited in aeg-root/enforcement.md that does not resolve on the forge. G5: a contract producer or consumer that is not a real role, or a role missing performs or refuses_when. G6: a product row with no entry in the core check registry. G7: a file that writes to GitHub or is a blocking gate, that does not reach a Log producer and is not exempt.
RunsRing 0: push. Ring 1: CI.
ReadsAEG_REPO, GITHUB_TOKEN, GH_TOKEN (optional). aeg-root/enforcement.md, role and contract frontmatter, hook files, CLI sources; gh for G4. Full scan, timeout 30 s.
OutcomesExit 0 with one warning (registry-gates.dormant) when aeg-root/enforcement.md is absent. Otherwise exit 1 if any of G1–G7 fails; each finding's check field is registry-gates.G<n>. G4 fails with severity:infra when the repository or gh is unreachable.
ImplementsVinaya-specific
Settingschecks.registry-gates

branch-topology

A task branch's name names a task, so a push is refused when no such task exists on the forge.

RefusesA task/<tranche>/<n> branch whose task id has no row in that tranche's topology as derived from the forge, or whose tranche cannot be found.
RunsRing 0: push. Ring 1: CI.
ReadsBRANCH, AEG_REPO, GITHUB_TOKEN, GH_TOKEN (optional). The tranche, read from the live forge. Full scan, timeout 30 s.
OutcomesExit 0 off a task branch or on a matching row. Exit 1 on a refusal or a severity:infra failure (owner and repository, or the forge, unreachable).
ImplementsVinaya-specific
Settingschecks.branch-topology

dead-branch-push

Work on a branch whose pull request is already merged or closed is finished, and pushing there would reopen it.

RefusesA push to a branch whose most recent pull request is MERGED or CLOSED.
RunsRing 0: push. Ring 1: CI.
ReadsBRANCH, GITHUB_TOKEN, GH_TOKEN (optional). gh pr list --head <branch> --state all --limit 1. Full scan, timeout 30 s.
OutcomesExit 1 on MERGED or CLOSED. Exit 0 on OPEN, NONE, or UNKNOWN (forge unreachable: it fails open).
ImplementsTerminal completeness
Settingschecks.dead-branch-push

first-push-dispatch

The first push of a task branch is the moment work starts, so it must come from a task that is ready to dispatch.

RefusesThe first push (no pull request yet) of a task/<tranche>/<n> branch whose dispatch readiness is NOT_READY.
RunsRing 0: push. Ring 1: CI, where a pull request exists, so it allows.
ReadsBRANCH, AEG_REPO, GITHUB_TOKEN, GH_TOKEN (optional). gh pr list --head, the live forge. Full scan, timeout 30 s.
OutcomesExit 0 off a task branch, when a pull request exists, or on UNKNOWN (it fails open). Exit 1 on NOT_READY.
ImplementsVinaya-specific
Settingsprincipals (trust anchor); checks.first-push-dispatch

reader-resolvable-prose

Prose a reader cannot resolve, such as a forge number or an undefined coined term, is useless to anyone without the author's context.

RefusesBlocking (error): a tranche-slug citation in product code (proseGates.productPaths, with a built-in default), spec-class citations (a tranche, a task, an Issue or pull-request number, or an outside document), and custom rules set to error; source-comment citations when proseGates.sourceComments.severity is error. Report-only (warning): unresolvable references and undefined coined terms in the doctrine (ships) and reader-facing classes.
RunsRing 0: push. Ring 1: CI.
ReadsGIT_INDEX_FILE, VINAYA_COMMIT_HOOK. Sweeps the whole doctrine tree, <doctrineRoot>/glossary.md, the legacy-slug directory, spec paths, product paths and source-comment globs. Reports only findings on changed lines, or all of them when no diff resolves. Full scan, timeout 30 s. It declares include, which has no effect on a full-scope check.
OutcomesExit 2 (error status) when vinaya.config.json is malformed or no doctrine root resolves. Exit 1 when a reportable finding is blocking. Exit 0 otherwise.
ImplementsContract validity
SettingsproseGates.doctrineRoot, readerFacingPrefix, readerFacingSuffix, legacySlugDir, productPaths, specPaths, specGrandfather, sourceComments.globs, sourceComments.allowlist, sourceComments.severity, rules; decisionRecords.path; checks.reader-resolvable-prose

retired-vocabulary

Terms Vinaya has retired must not come back into the doctrine, where they would describe a method that no longer exists.

RefusesA match of a built-in retired-term pattern (or of a custom rule in its group) in a doctrine .md file, on a changed line.
RunsRing 0: push. Ring 1: CI.
ReadsGIT_INDEX_FILE, VINAYA_COMMIT_HOOK. Sweeps <doctrineRoot>/** and reports only findings on changed lines (all of them when no diff resolves). Full scan, timeout 30 s.
OutcomesExit 2 on a malformed config or an unresolvable doctrine root. Exit 1 on a blocking finding. Exit 0 otherwise.
ImplementsVinaya-specific
SettingsproseGates.doctrineRoot, proseGates.rules; checks.retired-vocabulary

doctrine-portability

Shipped doctrine is read in other repositories, so it must not cite paths that exist only in the repository it was written in.

RefusesNothing; it is report-only. Reports doctrine citations of paths that exist only in the authoring repository, when they are new against BASE_SHA and on changed lines.
RunsRing 0: push. Ring 1: CI.
ReadsBASE_SHA (default origin/main), GIT_INDEX_FILE, VINAYA_COMMIT_HOOK. Runs the rule on the working tree and on the base ref (git ls-tree, git show) and keeps only new findings. Full scan, timeout 30 s.
OutcomesAlways exit 0. Findings are warning.
ImplementsVinaya-specific
SettingsproseGates.doctrineRoot (default aeg-root); checks.doctrine-portability

doctrine-no-procedures

Doctrine describes rules, not runbooks, and a command sequence in doctrine goes stale as soon as the commands change.

RefusesA fenced block in a doctrine .md file that strings two or more shell-command lines together, outside the AEG:VENDOR-EXAMPLE anchors or a templates/ directory.
RunsRing 0: push. Ring 1: CI.
ReadsNo environment variables. Walks every .md under the doctrine root. Not filtered by the diff. Full scan, timeout 30 s.
OutcomesExit 1 with error findings; exit 0 with none.
ImplementsVinaya-specific
SettingsproseGates.doctrineRoot (default aeg-root); checks.doctrine-no-procedures

doc-claims

An AEG:CLAIM marker binds a sentence to text in a cited file, and when the file stops holding that text, the sentence is no longer true.

RefusesA malformed AEG:CLAIM marker, or a binding (contains:, absent:, sha256:) its cited file no longer satisfies. A cited file git does not track is reported as unreadable.
RunsRing 0: push. Ring 1: CI.
ReadsNo environment variables. git ls-files over aeg-root/*.md, apps/*.ts, apps/*.md, packages/*.ts, packages/*.md. Not filtered by the diff. Full scan, timeout 30 s.
OutcomesExit 1 with error findings; exit 0 with none.
ImplementsEvidence freshness
Settingschecks.doc-claims

workspace-escape

A constructed file path that reaches into another workspace package is invisible to the dependency graph and breaks silently when that package moves.

RefusesNothing; it is report-only. Reports a constructed filesystem reference (readFileSync, readFile, new URL('…', import.meta.url)) in .ts or .tsx sources under apps/ or packages/ (tests excluded) that leaves its own package or resolves to nothing, on a changed line.
RunsRing 0: push. Ring 1: CI.
ReadsGIT_INDEX_FILE, VINAYA_COMMIT_HOOK. Walks apps/ and packages/; reports only findings on changed lines (all of them when no diff resolves). Full scan, timeout 30 s.
OutcomesAlways exit 0. Findings are warning.
ImplementsContract validity
Settingschecks.workspace-escape

quoted-command

A doc that quotes a command or config line as present fact goes stale when the quoted file changes, so marked quotes are compared with their file.

RefusesNothing; it is report-only. Reports a span inside an AEG:QUOTES-FILE:START:<path> / AEG:QUOTES-FILE:END marker pair that the cited file no longer contains word for word. Unmarked text is never judged.
RunsRing 0: push (skipped at commit, full-scope, pre-commit). Ring 1: CI.
ReadsNo environment variables. Sweeps every .md under the doctrine root, and the reader-facing pages when configured, then reads each cited file from the working tree. Not filtered by the diff. Full scan, timeout 15 s.
OutcomesAlways exit 0. Findings are warning.
ImplementsEvidence freshness
SettingsproseGates.doctrineRoot, proseGates.readerFacingPrefix, proseGates.readerFacingSuffix; checks.quoted-command

main-branch-refusal

Work belongs on its own branch and reaches the default branch only by pull request, so a commit or a push on the default branch is refused.

RefusesA commit or a push on the default branch: HEAD is the default branch (refs/remotes/origin/HEAD), unless VINAYA_PUSH_REFS shows a tag-only push.
RunsRing 0: commit and push (listed here too; it runs first at commit). Ring 1: CI, where HEAD is detached and it always passes.
ReadsVINAYA_PUSH_REFS (optional, set by the push hook). git symbolic-ref only, no network. The diff, timeout 15 s.
OutcomesExit 1 with an error on the default branch. Exit 0 with a warning when the default branch cannot be determined (it fails open). Exit 0 otherwise.
ImplementsEffect safety
Settingschecks.main-branch-refusal

token-collection-wired

When usage collection is wired but cannot reach its transcript, token reports would silently go empty, so that broken wiring is refused.

RefusesA metering setup that found a wiring point but could not use it: pointer-unusable, transcript-unreadable, transcript-empty. It passes no-transcript-resolved (nothing wired, or a pointer from another session).
RunsRing 0: push. Ring 1: CI.
ReadsCLAUDE_PROJECT_DIR, CLAUDE_CODE_SESSION_ID (optional), TMPDIR (runner baseline). The $TMPDIR transcript pointer file and the transcript it names, read only when each is a regular file owned by this user. No network. Full scan, timeout 15 s.
OutcomesExit 1 with an error on broken wiring; exit 0 otherwise.
ImplementsTraceability
Settingstokens (how usage is collected); checks.token-collection-wired

What gets logged

Every vinaya check run writes gate events:

EventWhenFields
gate · summaryOnce per runran, passed, failed, skipped, failed_checks, duration_ms
gate · checkedOnce per check that did not pass or skipcheck, check_version, policy_version, input_fingerprint, outcome, reason, duration_ms

See Events for every field.

Configuration

KeyEffect on these checks
checksAn entry keyed with a core check name replaces that check, including with a no-op. A namespaced key (owner/name) adds a check; a scope: 'full' one runs here, at push. A config that cannot be resolved refuses the whole run.
ringsNeither flag affects these checks.
prePushprePush.alwaysRun adds test files to the hook's test step, after the checks. It does not change which checks run.
gateCutoversUsed by coherence (R1).
proseGatesUsed by reader-resolvable-prose, retired-vocabulary, doctrine-portability, doctrine-no-procedures, quoted-command.
principalsRead from the default branch (trust anchor) by coherence, dispatch-readiness, first-push-dispatch.

What's next