pushkin 0.2.0

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts
pushkin-0.2.0 is not a library.

pushkin

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts, deterministically. An agent write that violates a contract, touches protected surface, or edits a committed test suite is denied at the hook — with the reason, the fix hint, and an auditable event trail. Humans stay un-gated where it matters; everything fails open loudly, never silently.

Documentation map

document what it covers
DESIGN-FINDINGS.md canonical spec, the research it rests on, and the F-Ledger
docs/TESTING.md testing design and infrastructure — the layers, the red→green protocol, read-only suites, fixtures, the floor and its accounting, CI, and the evidence for why it is shaped this way
docs/TESTING-RUST-FOR-PYTHON-ENGINEERS.md the toolchain and test model taught from a Python baseline; start here if cargo and clippy are new
docs/RESEARCH-SOURCES.md every external figure this repo cites, with URLs and verification dates — and an honest list of the claims that still have no source
AGENT-INSTRUCTIONS.md builder conduct — the N-rules and the pass protocol
docs/STANDING-RULES.md rules that apply to every pass (V2, V3, V5, N10/§4.1)
docs/CONTINUATION-GUIDE.md area-by-area orientation for someone picking the repo up
docs/ROADMAP.md current state, open findings, what is queued
PHASE-LOG.md append-only record of every pass
PUSHKIN-AGENT-INTEGRATION.md per-agent hook mechanics for all five adapters

Install

cargo install pushkin

Or from source:

cargo install --path crates/pushkin-cli

Requires the binary on PATH (hooks resolve pushkin from PATH by design — nothing bakes an absolute path). pushkin doctor names it if resolution would fail.

After any change to gate behaviour, reinstall. A binary older than the manifest schema fails every manifest-reading verb at exit 1 before any gate logic runs, and a hook exiting 1 is non-blocking — so version skew silently disables the write-time gate while doctor still reports healthy. Tracked as F71; see docs/ROADMAP.md.

Quick start

cd your-repo                  # carrying a pushkin.toml manifest
pushkin init                  # Claude Code hooks + versioned consent
pushkin init --agent git      # native pre-commit shim (no other tools needed)
pushkin doctor                # verify the install; --repair fixes what it owns
pushkin floor                 # run the repo's declared mechanical floor

Which manifest governs

Resolution is pinned to the repository, not to your shell's working directory:

  1. PUSHKIN_MANIFEST, if set — that file, wherever it is. If it names a file that cannot be read, pushkin fails with a named error; it never falls back to the repo's manifest behind your back.
  2. Otherwise <git rev-parse --show-toplevel>/pushkin.toml — the repository root's manifest, whatever directory you invoke from.
  3. Otherwise ./pushkin.toml, and pushkin says so on stderr.

Case 3 is the honest edge: where git is missing or the tree is not a repository, pushkin cannot tell whether a root exists, so it uses the working directory and tells you it did. In that situation a pushkin.toml in a subdirectory still governs — pushkin doctor names every nested manifest it finds, and carries a standing info line when git is unavailable.

Every verdict names the manifest it was decided under: repo-relative in the agent-facing prose, absolute in --json and in the pushkin floor header.

Adapters: --agent claude | codex | auggie | hermes | opencode, plus two pre-commit floors and a managed instructions block (--agent agents-md). --remove-agent <name> uninstalls only what pushkin owns.

The pre-commit floors

Two interchangeable floors run pushkin check --staged --json before every commit; both carry the same guard: fail open only on positively probed absence (no binary, or no pushkin.toml), with a loud actionable notice — a teammate who never installed pushkin is not blocked by a hook they never opted into, and a check that RUNS is never second-guessed.

  • pushkin init --agent lefthook — merges a marker-bracketed block into lefthook.yml, preserving every foreign command byte-for-byte. Any prior pushkin marker generation upgrades in place; uninstall leaves no litter.
  • pushkin init --agent git — writes .git/hooks/pre-commit directly, no hook-manager dependency. Detects core.hooksPath overrides and foreign hooks and prints the integration snippet instead of clobbering.

Team enforcement: CI is the backstop

The hooks above bind only machines that opted in. A teammate who never installed pushkin (or lefthook) commits ungated — the fail-open floor is deliberate, so local hooks alone cannot enforce the gate team-wide. The backstop is CI: run the gate as a required status check with the pushkin-gate composite action this repo ships (.github/actions/pushkin-gate), and unprovisioned pushes are caught at the pull request instead of landing silently.

name: pushkin-gate
on: [pull_request]
jobs:
  gate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0            # affected mode needs history (F19)
      - uses: acoletti/pushkin/.github/actions/pushkin-gate@<TAG-OR-SHA>
        with:
          mode: affected            # or `check` for the full repo
          base-ref: origin/main
          pushkin-ref: <TAG-OR-SHA> # pin; a moving ref is non-reproducible

affected gates only the diff against base-ref; check sweeps every mapped file. Optional db-drift / db-rls inputs add the schema gates (see the action's own docs for their prerequisites). Make the job a required check on the default branch and local provisioning becomes a latency optimization, not the enforcement boundary.

What the gate enforces

  • Contract conformance at mapped boundaries (contract.boundary.*).
  • Protected paths (pushkin.protected_path): the manifest, schemas, CI, the floors — agent-denied at write time, unwaivable by construction. Staged protected changes draw a loud advisory at the floor, never a block (the floor runs for humans too).
  • Committed tests are read-only (pushkin.read_only_path): files in git HEAD under read_only_paths globs deny agent edits; NEW test files and uncommitted iteration flow freely. Unwaivable.
  • Suppression comments (pushkin.suppression.new) per the manifest.
  • Waivers (pushkin waive): scoped, expiring, decision-logged — for rules that allow them; doctor lints stale grants.

Read-only test surfaces are not a stylistic preference. Public measurement of frontier models finds test modification is one of four systematic cheating strategies, and that "Read-only access provides a middle ground: it restores legitimate performance while preventing test modification attempts" where hiding the tests degrades performance on the original benchmark (ImpossibleBench, arXiv:2510.20270). docs/TESTING.md §2 collects that evidence alongside this repo's own recorded incidents; docs/RESEARCH-SOURCES.md carries the full citations.

The mechanical floor

pushkin floor runs the [floor] table declared in pushkin.tomlone committed list of commands that pushkin floor, the Makefile, scripts/floor.sh and CI all read, so "CI runs the same checks you do" is a fact rather than a promise someone has to remember.

make floor                    # the canonical green — cite this, not a cargo count
pushkin floor --skip bench    # named omission; prints EXCLUDED, NOT a full floor

Three properties worth knowing:

  • Ignored tests are accounted for. A command declaring reconcile_ignored must have its ignored count matched exactly by a command declaring covers_ignored_of, or the floor is RED. Self-coverage is rejected at parse time, and a coverer excluded by --skip covers nothing. This exists because a cited floor number once silently omitted two #[ignore]d benchmarks — one of which was red at the time.
  • The exit contract is inverted relative to ESLint/ruff, and disclosed in --help: 0 clean, 2 findings, 1 the gate itself could not run.
  • Network-dependent verdicts are disclosed. inputs = "network" on a command (e.g. cargo deny check, which consults the RustSec advisory DB) prints a note that the verdict is not a pure function of the commit.

docs/TESTING.md §6–§7 documents the table, the accounting and the exit codes in full.

The schema-only tier (no JS runtime)

Gate-time enforcement is pure Rust. A repo that commits its canonical schemas needs no bun, node, or JS toolchain to be gated: install the binary, run pushkin init, done. Bun is a compile-time tool only — pushkin compile regenerates canonical schemas and bindings from Zod sources, and repos that author schemas directly (or vendor them) never need it. If you never run compile, you never need a JS runtime.

Doctor

pushkin doctor verifies every installed surface (adapters, both floors, binding epochs) and exits 1 on findings; --repair regenerates only what pushkin owns — foreign configs are reported, never rewritten, and advisory findings (like an unresolvable binary) never trigger a rewrite. Repair exit code: 0 when every repair that could run succeeded; deferral messages name the manual remedy without failing the run.

Release status

pushkin is published on crates.io — cargo install pushkin installs the latest release. All four crates version in lockstep with exact internal pins (=0.2.0 at the current workspace version), and Cargo.lock is a release artifact: cargo install --locked reproduces the exact dependency graph a release was tested against.

Published versions are immutable. A correction to shipped content — including this README, which renders on the crate page from the published tarball — requires a new patch release; it cannot be fixed by editing develop.

Versioning policy:

  • Patch (0.1.x) — fixes that do not change emitted output: docs, CI, the gate action, internal refactors.
  • Minor (0.x.0) — new schema capability or changed emitter output. Any change under crates/pushkin-compiler/src/** is epoch-sensitive and bumps schema_epoch in pushkin.toml, so it is at least a minor.
  • Tags — every published version carries an annotated vX.Y.Z tag on the exact commit whose tree was published.

Contributing

Tests are written first, committed before the implementation, and read-only once committed. make floor must be green at every commit. The full protocol, the fixture patterns, and the reasoning are in docs/TESTING.md; if Rust is new to you, read docs/TESTING-RUST-FOR-PYTHON-ENGINEERS.md first.

Phase 0 reference implementation

The original Bun/TS spike (Claude Code gate) still lives in src/ and tests/: make test && make lint. The Rust workspace is the product; make floor runs its full declared mechanical floor — currently seven commands: fmt, clippy, test, bench, deny, tsc, bun-test.