did we keep every behaviour?
Your agent writes the code fast. mechanics is the ledger of what the app is supposed to do — and the gate that fails CI when the two drift apart.
npm i -D @hansenexus/mechanicsAn agent can delete a feature and leave the suite green
It removes the code and the test that covered it in the same commit. Every remaining test passes, because the test that would have failed went with the feature. Line coverage does not drop — the lines are gone. Nothing anywhere notices.
vitest answers do my tests pass? Nothing in the toolchain answers does this app still do everything it used to do? That question needs a record of the behaviours that is kept outside the code, and a check that the two still agree.
Coverage for what your app does, not what your code runs
Line coverage reports which lines executed. mechanics reports which shipped surfaces — routes, API routes, Convex functions, crons, HTTP endpoints, background workers, whatever your adapters declare — no documented behaviour claims. Every one that is left over is a gap with a name and a path, not a percentage.
$ mechanics coverage --app=perch mechanics · perch · 25 behaviours · 38/42 surfaces covered route 10/12 —————————— 83% 2 gaps api-route 4/5 —————————— 80% 1 gap convex-function 16/17 —————————— 94% 1 gap cron 3/3 —————————— 100% http-endpoint 2/2 —————————— 100% worker 2/3 —————————— 100% 1 ignored ⚠ route / ⚠ route /login ⚠ api-route /api/webhooks/stripe ⚠ convex-function monitors.exportCsv tests 16 linked · 1 manual · 8 untested wave 2026-08-redesign open —————————— 10/17 · 3 fail · 3 pending
Real output from examples/perch, a corpus part-way through its life. Four surfaces are unclaimed on purpose — a coverage table with no gaps is a bad advertisement for a tool that finds gaps.
What it is not
mechanics does not run your tests, replace vitest or playwright, or measure line coverage. It sits above them and audits whether what you ship, what you document, and what you test still line up. It has no opinion about how you write the tests themselves.
How it works
- Write a behaviour as one markdown file — frontmatter for the surfaces it claims, labelled acceptance criteria for what it promises.
mechanics buildcompiles a manifest and you commit it. CI diffs the committed manifest against the corpus, so an edited behaviour with a stale manifest fails the build.- Adapters inventory what the app actually ships — the Next.js route tree, the Convex function list, or any set of files you declare by glob.
- Anything unclaimed is a named gap. Set
coverage.enforce: errorwhen the corpus is complete and it stays that way — that is the ratchet.
---
title: Create a monitor for an endpoint
kind: user-facing
status: active
priority: p0
claims:
route: ["/dashboard/monitors/new"]
api-route: ["/api/monitors"]
convex-function: ["monitors.create"]
verify: e2e
---
## Acceptance Criteria
- **AC1** Given a valid HTTPS URL and an
interval, When I submit the form, Then
the monitor appears with status `pending`
and its first check is scheduled within
one interval.
- **AC2** Given a URL already monitored in
this workspace, When I submit, Then the
form rejects it and links to the existing
monitor.| ID | Behaviour | ACs | Tests |
|---|---|---|---|
perch.monitors.create-monitor | Create a monitor for an endpoint p0 | 4 | 1 |
perch.monitors.record-check-result | Record the outcome of a probe p0 | 4 | none |
perch.incidents.resolve-incident | Resolve an incident by hand | 3 | manual |
The claim is the link. A behaviour that claims a route and a Convex function covers both; a route nothing claims shows up in the table above as a gap.
The gate is what stops the corpus rotting
Documentation decays because nothing checks it. The manifest is committed, so CI can diff it — edit a behaviour and forget to rebuild, and the build says so by name. Validation names every unclaimed surface rather than totalling them, because a total is not actionable and a path is.
$ mechanics build --all --check [mechanics] ✗ perch: manifest drift ~ changed perch.monitors.create-monitor [mechanics] ✗ committed manifest(s) are stale. Run `mechanics build --all` and commit the result.
$ mechanics check --app=perch [mechanics] warn perch: unclaimed route "/" — claim it in a mechanic or ignore it [mechanics] warn perch: unclaimed route "/login" — claim it in a mechanic or ignore it [mechanics] warn perch: unclaimed API route "/api/webhooks/stripe" — claim it in a mechanic or ignore it [mechanics] warn perch: unclaimed Convex function "monitors.exportCsv" — claim it in a mechanic or ignore it [mechanics] ✓ perch: 25 mechanics ok (4 warning(s))
The two things it refuses to do
Record a verdict without evidence
pass requires an evidence string, at write time and again at read time. A hand-edited log cannot slip one through.
Let a model mark its own work green
The MCP server is read-only, and a test asserts no write-shaped tool name is advertised. An agent may fix what has one correct answer, and may raise or reject a proposal, but mechanics run accept refuses any actor that is not human — accepting asserts a suggestion was right, which is the same act as marking work green.
Stated plainly, because the alternative is overclaiming: the actor check infers human from the absence of an agent session variable, so a process that unsets it is indistinguishable from a person. It removes the default path and nothing more. The durable boundary is that proposals and their resolutions are committed files somebody reviews.
Any agent, not just one
The pipeline — scan, gaps, propose, fix — runs on whatever the machine actually has. A repo on a locked-down network, a team standardised on a different CLI, and a laptop with a local model and no API budget all want the same loop.
Harness providers
claude, codex, qwen — already agents. Hand one a brief in a worktree and it edits with its own tools.
Model providers
ollama, lmstudio, any OpenAI-compatible endpoint — text in, text out. They answer in a small edit protocol that mechanics validates, applies, and rolls back as one unit if the result does not build.
Four moves are refused whatever the provider: editing a wave file, promoting a draft to active, touching coverage.ignore, and flipping coverage.enforce. Those are not work — they are claims that the work is good.
And when two of them disagree
Decision records live at .docket/decisions/ and name what theyaffect — specs and paths — so a decision is retrievable by the file an agent is about to touch. Why is this like this is unanswerable by search, because the agent would have to already suspect there was a reason.
Two open runs whose decisions overlap get flagged. That is the characteristic multi-agent failure and it is invisible from inside either run, because each one looks locally coherent. A record that points at deleted code fails the build; two agents working the same subsystem is only a warning, because concurrency is legitimate and CI should not punish it.
$ mechanics agents ✓ claude harness claude-cli 2.1.0 ✓ ollama model 3 models · qwen2.5-coder:7b, … · codex harness not on PATH · lmstudio model no endpoint at :1234
One self-contained page, and a full-screen view
mechanics report --html is the same data as a single file — no build step, no server, no external request. Attach it to a PR, or open it from file:// six months later and it still renders.

mechanics tui is the same corpus as a dashboard you leave open: apps and their gaps, issues ordered by how quietly they fail, runs in flight, proposals waiting on you. It watches the corpus and .docket/, so it reacts to an edit rather than to a refresh — and it never writes.
The tarball can be checked, not just trusted
Releases publish from CI over npm Trusted Publishing, so the package carries a signed attestation tying it to this repository and the commit it was built from.repository in a package.json is a claim anyone can make; the attestation is the part that can be verified. There is no publish token to leak or rotate — npm verifies the workflow's OIDC identity directly.
Start here
# single-app repo $ npm i -D @hansenexus/mechanics && npx mechanics init # monorepo: apps/<slug> $ npx mechanics init --app=<slug> # or from inside Claude Code $ /plugin marketplace add hansenexus/mechanics
Init is idempotent — every file is skip-if-exists — and writes the repo config, a corpus skeleton, the CI gate matched to your package manager and detected adapters, the .mcp.json registration, and the committed manifest.--dry-run shows the plan and writes nothing.