pushkin 0.2.1

Schema-first enforcement harness that gates AI coding agents' file writes against project contracts
# 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

```sh
cargo install pushkin
```

Or from source:

```sh
cargo install --path crates/pushkin-cli
```

No Rust toolchain? The same binary ships on PyPI as `pushkin-cli` (macOS and
Linux wheels, nothing but the executable inside):

```sh
uv tool install pushkin-cli     # or: pipx install pushkin-cli / pip install pushkin-cli
```

Use the installed binary for hooks, not `uvx pushkin-cli` — `uvx` resolves an
environment on every call, which the warm-path latency gate does not budget for.

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

```sh
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.

```yaml
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](https://arxiv.org/abs/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.toml` — **one
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.

```sh
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 (`cargo install pushkin`, or `uv tool install pushkin-cli` with no
Rust toolchain either), 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.1` 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`.