Deterministic gates
Test-edit denial, evidence-checking commit lint, decision gate, drift checks — all configured by one flat factory.yaml. Hooks stay byte-identical across adopters; upgrades are file copies. Hook reference →
Gates that block instead of ask. Roles that can't grade their own work. A commit lint that bans the word "verified" unless a command and its output back it up. We pulled this out of the factory we build our own product with — it's what stuck after an agent told us a change was verified when the check had never run.
# Actual behavior of scripts/hooks/test-edit-denial.sh and scripts/selftest/run.sh — not a mock.
Across four review rounds of real agent-written code, one pattern held:
consecutive review rounds before the pattern was undeniable
of false "verified" claims were checks that had never been run
checks that actually ran and lied
# INSTRUCTIONS.md - never edit test files - always verify before claiming done compliance: whatever the model remembers today
$ ./scripts/hooks/test-edit-denial.sh DENIED: implementer role cannot edit test files. exit code: 2 compliance: not optional
The underlying idea — computational controls beat inferential ones — comes from Birgitte Böckeler at Thoughtworks. We kept applying it until nothing important was left as prose. Read the full Verification Contract →
A check you've only seen passing proves nothing. Every gate ships with this proof in CI — and the installer runs it on your machine before it will say "done."
To be clear: this is how we build, not how you should. There are other serious takes on agent reliability worth your time. This is just the one we can vouch for, since we live in it daily.
The left column is the common default across today's agent tooling — instruction files, trusted-because-green checks, one session doing everything. It exists for good reasons. The right is what survived our failures.
exit 2 is a fact.WROTE / RAN / OBSERVED — no evidence, no "verified." The commit lint enforces it.Test-edit denial, evidence-checking commit lint, decision gate, drift checks — all configured by one flat factory.yaml. Hooks stay byte-identical across adopters; upgrades are file copies. Hook reference →
Defined once, generated per harness, separated by permissions the hooks enforce — not by politeness. One keeps a cited wiki of your codebase — Karpathy's LLM-wiki pattern, with the provenance enforced rather than hoped for.
The suite violates each rule on purpose and asserts the gate fires. factory-init won't claim success until it passes on your repo.
| Language pack | Blessed stack | Maturity |
|---|---|---|
| Go | Ginkgo + Gomega · golangci-lint · gosec · govulncheck · gremlins | battle-tested |
| TypeScript | Vitest · Biome · tsc · Stryker · OSV-Scanner | experimental |
| Java / Spring Boot | JUnit 5 + AssertJ + Testcontainers · Spotless · Error Prone · SpotBugs · OSV-Scanner · PIT | experimental |
battle-tested = a production service shipped under it. experimental = no proven adopter yet. Labels change on real adoption — not before. The core — commit gates, push gates, role separation, the verification contract — is language-agnostic and works with no pack at all. A pack only arms the test-file pattern and check command for your language, so Python, Rust, or Ruby projects get everything except those two.
Configuration is one flat file — no nesting, no schema to learn:
Bare, the installer only fetches — a pinned copy into one folder, running nothing it downloaded. Add init and it also sets up the current repo. The word is your consent; the plain command never touches your project.
Needs git, a coding harness (opencode, Claude Code, or Codex), and your language's tools. factory-init runs prereq-check first and tells you what's missing.
installer fetches a pinned copy — and executes none of it
factory-init asks a few questions, writes factory.yaml, installs the gates
init breaks a rule on purpose, watches the gate fire, and only then says "done"
The gates are plain shell scripts, so you can call them from any CI — or from the git pre-push hook, which needs no CI at all. One caveat: the ready-made workflow we ship is GitHub Actions. On GitLab CI, Jenkins, CircleCI, and the like, you invoke the same scripts from your own pipeline config — a handful of lines, but that part is yours to write.
Yes. Every gate is independent. Adopt them one at a time — the adoption guide covers going incremental in a brownfield repo.
./factory upgrade refreshes the framework — the hooks, scripts, and gates — in place. It never touches your factory.yaml, your code, or your customized files; it just reports which of those changed upstream so you decide. Then ./factory doctor confirms every gate is still armed and fires its own proof. Nothing is committed for you.
The canonical config lives in opencode.json, but a sync script generates equivalent Claude Code and Codex configs. The enforcement layer is plain shell — it doesn't care which harness drives.
The core works anyway. Commit gates, push gates, role separation, and the verification contract are language-agnostic. A pack only arms the test-file pattern and check command — set those two in factory.yaml by hand and you have the full thing.
The gates are the fast local rail — a hook that exits in milliseconds. They run where you'd otherwise wait on a human reviewer to notice the same mistake. That trade goes the other way.
The fastest way to evaluate this template is to try to violate it. That is also the point.