By Robert Ioffe
A real tool you can run today, and the method it belongs to. The long version is the book, From Vibe Coding to Spec Engineering; this is the short one.
Coding agents are good enough now that the bottleneck has moved. It is no longer “can the agent write this?” — it is “did the agent write what I meant, and how would I know?”
When you build from a chat prompt, every gap in the prompt gets filled with a guess. The agent picks an exit code, a rounding rule, a default, an error message, a thing to do when the input is empty. Most guesses are fine. Some are wrong. All of them are invisible: they live in the code, not in anything you can read, and you meet them one at a time, later, as bugs. When you ask the agent whether the code does what you asked, it reads its own code and tells you yes. That answer costs nothing to produce and proves nothing.
The pattern gets a name here because it is a pattern, not a personal failing: vibe coding is building against intent that was never written down, and then trusting an account of the result that was written by the thing being evaluated.
The alternative is old and unglamorous: write the specification first, and make it the source of truth. What is new is who the specification is for. It is written for an agent to build from and for a verifier — human or machine — to check against. That changes what “precise enough” means. The bar we use:
A specification is precise enough when two competent implementers would build materially equivalent systems from it, and a verifier could tell whether either one conforms.
Everything in the method follows from that bar:
R-nn, contracts C-nn, invariants
I-nnn, constraints K-nn, edge cases
E-nn, acceptance tests T-nn. An ID is the
handle that lets code, tests, reviews, and reports point at the same
sentence.2 and prints
<message> to stderr”. If you cannot write the test,
you have not written the requirement.confirm status,
waiting for the human.The human’s job moves up a level. You decide what the system must do, you ratify or overturn the defaulted decisions, and you read evidence. The agent writes, reviews, and builds against the document — and proves it did.
The method is encoded as three skills — small instruction files an agent loads on request — and the whole loop is a handful of prompts:
<the application>:
<brief>.” Read §0 — is that the system you
meant? — and §12, the decisions it made on your behalf.READY /
NOT READY verdict. A spec that reads clearly to a person is
routinely Level 2 for an agent, because the agent cannot ask.READY. Usually one
more pass.<the change>.” Then 2–4 again. The spec stays
the source of truth; the code follows it.Thirteen fixed sections, so that agents and tools know where to look: intent and non-goals, actors, requirements, behavior and state, contracts, interfaces, invariants, constraints, edge cases, tests, dependencies, the traceability matrix, and the decisions to confirm. Formulas are LaTeX, diagrams are mermaid, and the rows are normative while the pictures illustrate. A row from speccheck’s own specification:
| **R-11** | A judge verdict MUST only ever lower an ID's status (`PASSING` → `WEAKLY_PASSING`);
the judge MUST NOT change any status other than `PASSING` and MUST NOT create, remove, or
re-attribute citations. | §0 principle |and the edge case that pins what happens when a model misbehaves:
| **E-16** | Judge returns `ASSERTS` with no evidence, or evidence outside the span / in
another file | `UNKNOWN`, `coerced: true`, rationale `judge: ungrounded`. The raw answer is
available only at DEBUG. |Both are checkable. Both are cited — by the code that realizes them and the tests that prove them — and that is the whole trick.
Phase three of building from a spec is the audit: re-read the specification and, for every ID, point at the evidence that the implementation realizes it. Humans doing that are slow and get tired. Agents doing it are fast and confabulate. speccheck makes it mechanical.
Given a SPEC.md, a source tree, a test tree, and a JUnit
XML results file, it:
~~R-07~~ is retired);UNCITED, UNTESTED,
UNVERIFIED, FAILING, SKIPPED, or
PASSING — every one backed by a file:line you
can grep;speccheck: CONFORMING - 170/170 passing (100.0%), 0 failing, 0 skipped, 0 weak, 0 unverified, \
0 untested, 0 uncited; 0 dangling, 0 stale; judge=mock
With --strict, that line is a CI gate: exit
0 only when every ID is PASSING and nothing is
dangling or stale.
Then there is the judge. A test can cite R-11, run the
code, and never assert anything about it — and the deterministic kernel
cannot tell. So --judge llm sends each passing (test, ID)
pair to a model with one question: does this test assert the
behavior this ID describes, or does it merely execute code near it?
The design rule is the one the whole tool is built on:
The model may only ever make the news worse. Every status is computed deterministically from evidence you can grep; the judge is permitted to downgrade a
PASSINGtoWEAKLY_PASSINGwith cited line numbers, never to upgrade anything.
A verdict without valid evidence is discarded as
UNKNOWN. A green report is therefore exactly as trustworthy
as grep plus your test runner, and a yellow one carries a
reason you can click on. The judge speaks the OpenAI-compatible
chat/completions shape, so it runs against a local Ollama
model for free, or against OpenRouter, Anthropic’s compatibility
endpoint, or anything similar for cents.
speccheck is written to its own SPEC.md, and it is the
worked example of the method. The numbers in the summary line above are
its self-application: 170 IDs, 74 tests, every one PASSING,
and — this is the part I find most convincing — when the LLM judge was
first pointed at the tool’s own suite, it found five tests that proved
their IDs only by implication. The tests were strengthened; the code was
not touched. That is the judge doing precisely its job.
The gate slots into the loop at step 4 and runs twice, in order:
uv run python -m pytest tests -q --junitxml=junit.xml
# Phase A — deterministic, offline
speccheck check --spec SPEC.md --src src --tests tests --results junit.xml \
--judge mock --strict --out build/speccheck
# Phase B — LLM judge, only once A is clean
# Ollama, or any compatible endpoint
export SPECCHECK_JUDGE_URL=http://localhost:11434/v1/chat/completions
export SPECCHECK_JUDGE_MODEL=qwen3:8b
export SPECCHECK_JUDGE_API_KEY=ollama
speccheck check --spec SPEC.md --src src --tests tests --results junit.xml \
--judge llm --strict --out build/speccheck-llmEach non-passing status names the fix. UNCITED means the
agent silently dropped a requirement — go build it, test-first.
UNTESTED means the code is there and no test proves it.
UNVERIFIED means the test never ran.
WEAKLY_PASSING means the test runs the behavior without
asserting it, and the report’s §8 has the model’s rationale, line by
line. The spec-build skill knows all of this and will not call the work
done until both phases exit 0 — and it records both summary
lines in the build report, so the evidence travels with the code.
On a real project the rhythm is: spec, review, fix, build, gate; then
for every change, spec first, gate last. The agent does the typing. You
read SPEC.md §12, the review’s remediation plan, and the
gate line. That is a job a person can actually do at the speed agents
now work.
Everything is in one repository, MIT for the code and CC BY 4.0 for the documents:
git clone https://github.com/rioffe/speccheck && cd speccheck
# skills for Claude Code / Pi / Oh My Pi, spec2pdf.sh, the speccheck CLI, a local judge
./install.sh --interactive
speccheck --self-checkThen pick something you were about to vibe-code anyway and say: “Use the spec-writing skill to write SPEC.md for …” Read §12. Review it. Build it. Run the gate. If the tool tells you something your agent didn’t — and it will — open an issue and tell me what.
The book goes into the why and the how in depth: the review dimensions, the status algorithm, the judge contract, the failure modes we hit building this and what they taught us. For now the point is simpler: the tool exists, it checks itself, and you can run it this afternoon.