Status Is Downstream of Proof: The Architecture of Anvil
An agent reported a test it never wrote. That gap is the whole reason Anvil exists, now open source as anvil-state on PyPI. Here is the architecture, and the one part still unsolved.
Sekou M. Doumbouya
Listen to this article
The views expressed here are my own and do not represent those of any current or former employer.
An agent told me it was done. The task was small, a parser fix with a test attached, and the agent came back fast and confident: implemented, tested, complete. I marked it off. An hour later I ran the suite myself and the test it claimed to have written did not exist. Not failing. Not present. The model had reported success the way models report everything, fluently, and I had taken the report at face value because the tool I was coordinating through gave me nothing better to take.
That is the moment that built Anvil, which I am releasing today. The lineage is short. It started as a durable record I called fakoli-state, one more name drawn from the same West African well as the rest of my projects, living inside my plugin monorepo. I pulled it out, renamed it Anvil, and gave it its own life as a standalone, runtime-neutral thing. It is on PyPI now under the name anvil-state, because the bare name was already taken, which is its own small lesson about what you do and do not get to own.
uv tool install anvil-state
# puts both `anvil` and `anvil-mcp` on your PATH
I should say up front that I am aware this is the fourth time I have written at length about what is, structurally, a SQLite file in a hidden directory. I have made my peace with that. The file is the only part of this whole stack I am sure is worth owning, and this post is the argument for why.
Status Is Downstream of Proof
Let me be blunt about the thing the whole project turns on. When an agent says “done,” it is making a claim. A claim is a sentence, generated by the same machinery that generates everything else the model says, and it carries exactly as much weight as the sentence before it, which is to say none you can bank on. Evidence is different in kind. A claim is “it works.” Evidence is an exit code: the test that ran, the command that returned zero, the artifact that exists on disk whether or not anyone narrates it into being.
The tools we coordinate agents through were not built to tell those two apart. GitHub Issues stores free-form text in a body. The markdown-todo conventions store a checkbox. Chat stores an “I’ll take this” message and forgets it the moment the session resets. In every one of them, status outranks evidence: something declares a thing true, and the system records the declaration. That is exactly backwards for an autonomous or weakly-supervised runner, where the declarer is the least trustworthy component in the loop. Chat history is not a database. It does not survive a session reset, a model swap, or a change of runtime, and it never verified anything to begin with.
Anvil inverts the order. Status is downstream of proof. A claim becomes a real lock instead of a polite announcement, and completion is gated: the system will not record work as accepted without attached evidence that matches what the task said it required. The rest of the architecture exists to serve that one inversion, so the rest of this post is really just five mechanisms and one honest confession about what they do not yet reach.
A Claim Is a Leased Row, Not a Label
Start with the claim, because that is where most tools are weakest. In Anvil a claim is not a label on an issue or a name in a chat line. It is a row in a database, with a lease.
When an agent claims a task, it writes a Claim with an expiry and a heartbeat. The lease defaults to sixty minutes, configurable. While the agent works, it renews. If it stops renewing, because the process died or the session reset or the model wandered off, the lease goes stale, and here is the part that matters: stale claims are reaped on every mutating call. Not on a cron, not on a sweep someone has to remember to run. Every single time anyone mutates state, Anvil first auto-releases anything past expiry, records the release reason as stale, and preserves the original claimant in the audit event. Dead agents do not hold locks. The work flows back to the queue without a human noticing it was ever stuck.
That is the middle of a four-layer stack. Underneath it, SQLite in WAL mode with BEGIN IMMEDIATE serializes the writers, so two agents racing for the same task resolve at the storage layer instead of in a hope. Above it sits a coarser guard: a ConflictGroup names a set of tasks whose expected files overlap, and anvil next refuses to hand out a task whose group already has an active claim, so two agents do not even get routed toward a collision. And at the very edge, a PreToolUse hook warns at the editor surface before an Edit or Write fires when the actor holds no covering claim. It is non-blocking by contract, a tap on the shoulder rather than a locked door, but it closes the gap between “claimed the task” and “actually touched the file.”
This is the part of my old life leaking through. I spent twenty years watching humans coordinate, and the failure mode is always the same: two people editing the same module because nobody owned it. File ownership is code ownership. A lease is just ownership with an expiry, so the dead can hand back what they were holding.
Three Gates, and the One That Refuses the Lie
Coordination keeps agents from colliding. It does nothing about the lie. For that, Anvil routes every task through a small state machine, eleven statuses from proposed to done, and plants three gates along the path. Each is a pure function that either lets a transition through or raises with a structured reason.
The readiness_gate fires when a task moves from drafted to reviewed: it refuses unless the task has both acceptance criteria and verification commands, both non-empty. You cannot promote a task that has not said how it will be checked. The prd_status_gate fires from ready to claimed: no agent claims work under a requirements doc that is still a draft. And the one this whole post is about, the evidence_gate, fires from needs_review to accepted. It walks every item in the task’s required_evidence and refuses the transition unless each one appears in the evidence the agent actually submitted. No matching proof, no acceptance. The agent’s confidence is not an input.
# An agent finishes, then has to show its work:
anvil submit T012 \
--commands "pytest tests/test_parser.py::test_fix" \
--files-changed "src/parser.py, tests/test_parser.py"
# Only now can a reviewer move it forward, and the gate
# checks that the required_evidence actually appears:
anvil apply T012 --approve
That is the inversion made mechanical. The task declares up front what proof completion requires, the agent must produce that proof, and the gate, not the agent, decides whether it counts.
Because every one of those transitions is pure, a function from (entity, context) to a new entity via model_copy, persistence is somebody else’s job. That purity buys the audit property I care about most: every mutation appends to an events.jsonl mirror, and replaying that log from an empty database reconstructs the canonical SQLite state exactly. I should be honest here. A native anvil replay command is a roadmap item, not something that ships today. The replay guarantee is the design property; the convenience button is not built yet. Backup today is copying .anvil/ wholesale, and committing the event log to git gives you a distributed audit trail recoverable from any clone. The contract is real. The button is pending.
The Bones
For the people who want to see the skeleton: the state lives in about twenty-five Pydantic v2 classes, configured extra="forbid" and validate_assignment=True, so an unknown field is an error and every assignment is validated at the moment it happens, not eventually. The entities are the ones you would expect once you accept the premise: Project, PRD, Requirement, Feature, Task, Claim, Evidence, Decision, Review, Event, and the sync bookkeeping. The eleven-status task lifecycle is the spine, and the three gates live on its edges.
Two rules hold the layering together. The CLI is the one and only mutator: hooks shell out to it, skills and agents call it, the MCP server goes through the same engine functions. Nothing writes to .anvil/ behind the engine’s back. And the MCP server exposes capabilities, twenty-four stdio tools any MCP client can call, while the plugin layer, the eight skills and five agents and four hooks, encodes discipline. That distinction is load-bearing. A tool can claim a task. It cannot decide when claiming is the right move, which specialist should execute, or what evidence the work requires. Capabilities are cheap. Judgment is the part you have to author.
The piece that makes the queue actually work is the work packet. anvil packet T012 renders exactly what an agent needs to execute, the intent, the acceptance criteria, the scope, and the explicit non-goals, projected from canonical state rather than dumped as a whole issue thread the agent has to summarize. This is the part of the original vision I am most attached to. When you parallelize this kind of work, you do not want to feed agents one task at a time and babysit each one. You want a queue they pop from. But a queue is only safe if each task arrives scoped to fit the runner that grabs it, and that is what the six scoring dimensions are for: complexity, parallelizability, context load, blast radius, review risk, and agent suitability. Anvil scores eligibility. It never names a model. The runner pulls a task it is suited for, and a tight packet lets even a fast local model skip exploration and just execute, because the worldview it would otherwise need has been written down for it. Anvil owns eligibility. The harness owns assignment. The two stay separable, which is the entire reason you can swap the fleet without touching the record.
The multi-PRD support in the release I am shipping today exists because I hit that wall myself. I was using Anvil to build Anvil, fixing a bug, and realized I needed two plans live in one project at once. The tool surfaced its own next requirement by being used, which is the most honest kind of roadmap there is.
The Honest Part: The Gate Is Advisory, and the Proof Does Not Travel Yet
Here is where I stop selling and tell you what is not finished, because the gap is the most interesting part.
The evidence gate is advisory by default. It refuses the transition inside Anvil, on my machine, in my repo. What it does not do, not yet, is emit a typed, signed, portable proof artifact, the kind of thing you could hand to someone who does not trust your machine and have them verify the work without re-running it. Anvil is single-host SQLite under a .anvil/ directory. The proof lives where the work happened, and the moment it leaves that host it is back to being your word about what your machine told you.
So the genuinely unoccupied position, a portable, signed, replayable proof bound to task identity and the claim that produced it, verifiable off the host, is a bet I still have to execute. It is not a moat I hold. Portable proof formats exist already, separately. Enforced evidence gates exist already, separately. The platforms are absorbing verification as I write this. What nobody has cleanly is the fusion: proof that is portable and signed and bound to the claim that made it, in a record no vendor owns. That is the wedge, and it is contested, not won. The day I start describing it as won is the day I have stopped being useful to you.
This is also why I have stopped pitching durable state as the differentiator. Anvil is not your durable execution engine, and if you need one, Temporal is right there. On that pillar Anvil is behind, not ahead: Temporal’s event history with replay, cross-host checkpointing, Dolt-versioned stores, the control planes shipping inside OpenAI and GitHub and Claude Code, all of them exceed my single-host SQLite and a JSONL file. The honest strategy is to concede durable execution and harden the one thing that is cheap to stay ahead on, the lease-and-claim single-winner coordination, while the verification bet is the thing that could become a real moat if I land it.
The one stance I will not concede is neutrality, because it is the whole point. State lives in your repo, with no hosted backend, no account, and no telemetry. The platforms cannot match that, and not because they have not gotten to it. A vendor-neutral, no-account record that helps you run their competitor’s models alongside theirs is not a gap in their roadmap. It is a conflict of interest in their business model. A self-hosted shared backend, your own Postgres on your own network, is in bounds. A hosted cloud backend with accounts is the exact terrain that gets absorbed, so it stays out. The load-bearing line is multi-vendor, local-first, no-account. The instant the value also works fine single-vendor inside one platform’s account, the platform wins it, and I will have built sand again.
I did build sand once. My own orchestration layer, fakoli-flow, got obviated by a first-party feature, Claude Code’s Dynamic Workflows, within months. The crew of named agents around it survived, because naming a specialist in a flow still earns its keep, but the orchestration did not. What kept earning its keep through all of that churn was never the conductor. It was being able to walk into a session and not rebuild context from nothing, to open a database that already held exactly the task to start on. That is the thing the platforms kept failing to make portable, so that is the thing I kept. Anvil is not a coding agent, work packets in and evidence out. It is not an issue tracker, though it can sync to GitHub Issues as an opt-in target. It is not a model router. It is the record underneath whatever you are running this week.
Build the Layer That Outranks the Report
The release today is anvil-state v0.3.0: multi-PRD revisable partitioned state, still labeled beta and pre-1.0, because the core loop works but the surfaces may still move before I call it done. It rode an arc from a capacity-coordination MVP, to a standard installable package, to the Claude Agent SDK as its default LLM provider, which fits the project’s real economics. Anvil is capacity-bound, not per-token-cost bound, so a flat-rate subscription is exactly the right shape to lean on. The cross-runtime half of this story, what happened when I moved the whole setup to a different harness and watched only the ledger survive the move, is a sibling post. This one is the architecture underneath that ledger, and the reason it exists at all started with buying a GPU to run my own agents and discovering I could not trust what they reported.
But the package is not the point. The point is the inversion. For most of software history the person reporting the work and the person doing the work were the same person, and “I’m done” was a reasonable thing to record, because a human’s reputation was on the line behind it. Agents broke that. The reporter is now the cheapest, most fluent, least accountable component in the system, and it will tell you the test passed with the same confidence whether the test exists or not.
So you stop recording reports. You record proof, and you make the proof outrank the report by construction, so the system cannot store a completion that no evidence backs. I have not finished making that proof portable, and I have told you exactly where that bet stands. But the principle holds whether or not I land the hard part: when the thing doing the work can also narrate the work, never let the narration be the record. Make the record something the narrator cannot fake. Build the layer that outranks the report.
Co-authored with AI, based on the author's working sessions, dictations, and notes.
Explore the source
fakoli/anvil
This article discusses an open-source project. Star it, fork it, or open an issue.