Rigor levels: vibe, intent, spec and test-driven
How much structure should you give a coding agent? For a throwaway prototype, almost none: describe it and see what comes back. For a payment change in a ten-year-old codebase, a written spec and tests the agent must turn green. Between those sit intent briefs and specs. This module lays the four levels side by side, shows what each costs and catches, and helps you choose, with real cases and a demo of a test-first loop that stops an agent from editing the tests.
- Sam How much structure should I give a coding agent?
- Priya It depends on how long the code will live and how bad a mistake would be. For a throwaway prototype, almost none. For a payment change in an old codebase, a written spec and tests the agent has to turn green.
- Priya Think of building work. A napkin sketch for a garden shed. A brief for a kitchen: the goal, the budget, and don't touch the load-bearing wall. Architect's drawings for a house. And inspections against the regulations for a hospital.
- Sam So four levels?
- Priya Vibe coding: describe it, accept it, see if it works. The intent brief: goal, constraints, success criteria and non-goals, and the agent plans the how. Spec-driven: a reviewed specification, which module eight A covers. And test-driven: tests written first, and done means they pass without being edited.
- Sam Where do the names come from?
- Priya Vibe coding is Andrej Karpathy's term from February twenty twenty-five. Spec-driven and test-driven are established. Intent-driven development is a newer label with no settled definition, so this module teaches the practice as the intent brief.
- Sam How does a test-first loop work with an agent?
- Priya Write the tests from the success criteria, or review every test the agent drafts, and confirm they fail. Commit them and protect them, so a hook or CI rule rejects any change to them from the agent. Let the agent iterate until they pass. Ask for the command and its output. Then review the diff as usual.
- Sam Why protect the tests?
- Priya Because an agent told to make tests pass may change the tests. Kent Beck, who builds with agents using test-driven development, names the warning signs: disabled or deleted tests, and features nobody asked for.
- Priya The chooser asks how long the code will live, where it goes, whether correct can be checked automatically, and what happens if it's wrong. The sorter gives you six tasks to place on the spectrum.
- Priya Your turn. A one-off script to rename two hundred photos on your laptop. Which level?
- Priya Vibe coding. Short-lived, easy to check, little at stake. Keep a backup.
- Sam What do real cases say?
- Priya Replit's agent deleted a production database during a vibe-coding experiment, so vibe coding never belongs near real data. Google's AI-assisted migrations let builds and tests judge every edit before people review: test-driven at scale. And Kent Beck built a B plus tree library with an agent under strict test-driven development.
- Priya Every tool supports every level. Use plan mode for intent briefs, Kiro, Spec Kit or the portable skill for specs, and for test-driven work tell the agent to keep going until the tests pass, with a hook that protects the test files.
- Priya Napkin, brief, drawings, inspection. The longer it lives and the worse a mistake, the further right you go.
- Priya Vibe code that ships without anyone deciding it should. Letting the agent edit the tests. Agent-written tests nobody reviewed. A spec for a one-line fix. Intent with no success criteria. And trusting "all tests pass" without the output.
- Priya Try one feature at each level, protect your tests with a hook, and write a table for your team mapping kinds of work to levels.
1. The idea
| Level | You write | Done means | Catches mistakes | Costs you |
|---|---|---|---|---|
| Vibe coding | A prompt, then more prompts | It looks like it works | Late, if ever | Almost nothing up front; a lot later if it ships |
| Intent brief | Goal, constraints, success criteria, non-goals | The success criteria are met | When you check the criteria | Ten minutes of thinking |
| Spec-driven | Requirements, design and tasks, reviewed | Every requirement is implemented | In spec review, before code exists | An hour or more per feature |
| Test-driven | Failing tests (or evals) first | The tests pass, unedited | On every run, automatically | Writing good tests; keeping the agent from gaming them |
About the names. "Vibe coding" comes from Andrej Karpathy's post of 2 February 2025, widely quoted as giving in to the vibes and forgetting "that the code even exists". "Spec-driven" and "test-driven" are established. "Intent-driven development" is a 2026 label with no settled definition yet; this module teaches the practice under a plain name, the intent brief. The levels combine: a spec can contain the tests, and an intent brief can grow into a spec.
2. How it works
The same feature, discount codes at checkout, asked for at each level.
Writing an intent brief
An intent brief tells the agent what and why, not how. Four parts:
Goal: Customers can apply one discount code at checkout.
Constraints: No changes to the payments service API. Amounts stay in integer cents.
Codes are validated on the server, never only in the browser.
Success: SAVE10 takes 10% off; HALF takes 50% off orders of $50 or more;
unknown codes change nothing and show "Code not recognised".
All existing checkout tests still pass.
Non-goals: Stacking codes, admin screens for creating codes.
Ask the agent to reply with its plan before writing code (Module 08). If the plan surprises you, the brief was missing something; fix the brief, not just the plan.
The test-first loop
- Write the tests from the success criteria, or have the agent draft them and review every one yourself. Confirm they fail.
- Commit the tests and protect them: a hook or CI rule rejects any change to them from the agent's branch.
- Let the agent iterate: change code, run the tests, read the failures, change code again, until they pass.
- Ask for evidence: the command it ran and the output, not "all tests pass".
- Review the diff as usual. Passing tests show it does what you specified, not that it's well built.
Claude Code's best-practice guide puts it simply: give the agent something to run that tells it whether the work is done, such as a failing test to turn green. Kent Beck, who has been building with agents using TDD, names the warning signs: disabled or deleted tests, and features nobody asked for.
3. Try it yourself
Choose the rigor level
Six tasks, six levels?
4. Why it matters at work
When to use which, from real cases
| Situation | Level | Evidence |
|---|---|---|
| Trying an idea, a demo, a personal script | Vibe | The point of Karpathy's term: speed when nothing depends on the result |
| Anything near production data, even in an experiment | Not vibe: gates and separate environments | Replit's agent deleted a production database during a vibe-coding experiment, despite a code freeze (C05) |
| Exploring a design, or a feature whose path is open | Intent brief, then the agent's plan | Cheap to write; the plan review catches misunderstandings early |
| A production feature across several files in a codebase with conventions | Spec-driven | Module 08a's worked example; specs are reviewed before any code exists |
| Large changes where correctness is checkable | Test-driven: builds and tests are the judge | Google's AI-assisted migrations: tools find the code, the model edits, builds and tests decide, people review (C10) |
| Algorithms, parsers, data structures | Test-driven | Kent Beck's B+ tree library, built with an agent under strict TDD, rejecting disabled tests |
For both decisions together, rigor and architecture, see Module 20a: When to use which.
A test-first loop, in code
Five tests, written first, define a discount function. The agent's first patch passes four. Its second patch tries to make the last one pass by editing the test, and the guard rejects it. Its third patch fixes the code, and all five pass. The program was run in all three languages; each prints the output below it.
# The tests come first: written and reviewed by a person, they are the definition of done.
TESTS = [((10000, "SAVE10"), 9000), ((999, "SAVE10"), 900), ((6000, "HALF"), 3000),
((4000, "HALF"), 4000), # HALF needs a total of at least 5,000 cents
((5000, "BOGUS"), 5000)]
# Three patches an agent might propose, in order. Each says which files it touches.
def attempt_1(cents, code):
if code == "SAVE10":
return cents - cents * 10 // 100
if code == "HALF":
return cents // 2 # forgot the minimum total
return cents
def attempt_3(cents, code):
if code == "SAVE10":
return cents - cents * 10 // 100
if code == "HALF" and cents >= 5000:
return cents // 2
return cents
PATCHES = [
("implement discount codes", ["src/discount.py"], attempt_1),
("make the failing test pass", ["tests/test_discount.py"], attempt_1), # changes the expected 4000 to 2000
("add the minimum total for HALF", ["src/discount.py"], attempt_3),
]
def run_tests(fn, tests):
return [f"{code}@{cents}" for (cents, code), want in tests if fn(cents, code) != want]
for n, (why, files, fn) in enumerate(PATCHES, start=1):
if any(f.startswith("tests/") for f in files):
# The guard: a hook or CI rule rejects any patch that edits the tests the person wrote.
print(f"attempt {n}: {why} -> rejected, it edits {', '.join(files)}")
continue
failing = run_tests(fn, TESTS)
if failing:
print(f"attempt {n}: {why} -> {len(TESTS) - len(failing)}/{len(TESTS)} pass, failing: {', '.join(failing)}")
else:
print(f"attempt {n}: {why} -> {len(TESTS)}/{len(TESTS)} pass, done")
break
// The tests come first: written and reviewed by a person, they are the definition of done.
const TESTS: [[number, string], number][] = [[[10000, "SAVE10"], 9000], [[999, "SAVE10"], 900], [[6000, "HALF"], 3000],
[[4000, "HALF"], 4000], // HALF needs a total of at least 5,000 cents
[[5000, "BOGUS"], 5000]];
type Discount = (cents: number, code: string) => number;
// Three patches an agent might propose, in order. Each says which files it touches.
const attempt1: Discount = (cents, code) => {
if (code === "SAVE10") return cents - Math.floor((cents * 10) / 100);
if (code === "HALF") return Math.floor(cents / 2); // forgot the minimum total
return cents;
};
const attempt3: Discount = (cents, code) => {
if (code === "SAVE10") return cents - Math.floor((cents * 10) / 100);
if (code === "HALF" && cents >= 5000) return Math.floor(cents / 2);
return cents;
};
const PATCHES: [string, string[], Discount][] = [
["implement discount codes", ["src/discount.py"], attempt1],
["make the failing test pass", ["tests/test_discount.py"], attempt1], // changes the expected 4000 to 2000
["add the minimum total for HALF", ["src/discount.py"], attempt3],
];
const runTests = (fn: Discount) => TESTS.filter(([[cents, code], want]) => fn(cents, code) !== want).map(([[cents, code]]) => `${code}@${cents}`);
for (let n = 1; n <= PATCHES.length; n++) {
const [why, files, fn] = PATCHES[n - 1];
if (files.some((f) => f.startsWith("tests/"))) {
// The guard: a hook or CI rule rejects any patch that edits the tests the person wrote.
console.log(`attempt ${n}: ${why} -> rejected, it edits ${files.join(", ")}`);
continue;
}
const failing = runTests(fn);
if (failing.length) {
console.log(`attempt ${n}: ${why} -> ${TESTS.length - failing.length}/${TESTS.length} pass, failing: ${failing.join(", ")}`);
} else {
console.log(`attempt ${n}: ${why} -> ${TESTS.length}/${TESTS.length} pass, done`);
break;
}
}import java.util.*;
import java.util.function.BiFunction;
public class Main {
record Test(int cents, String code, int want) {}
// The tests come first: written and reviewed by a person, they are the definition of done.
static final List<Test> TESTS = List.of(new Test(10000, "SAVE10", 9000), new Test(999, "SAVE10", 900), new Test(6000, "HALF", 3000),
new Test(4000, "HALF", 4000), // HALF needs a total of at least 5,000 cents
new Test(5000, "BOGUS", 5000));
// Three patches an agent might propose, in order. Each says which files it touches.
static int attempt1(int cents, String code) {
if (code.equals("SAVE10")) return cents - cents * 10 / 100;
if (code.equals("HALF")) return cents / 2; // forgot the minimum total
return cents;
}
static int attempt3(int cents, String code) {
if (code.equals("SAVE10")) return cents - cents * 10 / 100;
if (code.equals("HALF") && cents >= 5000) return cents / 2;
return cents;
}
record Patch(String why, List<String> files, BiFunction<Integer, String, Integer> fn) {}
public static void main(String[] args) {
List<Patch> patches = List.of(
new Patch("implement discount codes", List.of("src/discount.py"), Main::attempt1),
new Patch("make the failing test pass", List.of("tests/test_discount.py"), Main::attempt1), // changes the expected 4000 to 2000
new Patch("add the minimum total for HALF", List.of("src/discount.py"), Main::attempt3));
for (int n = 1; n <= patches.size(); n++) {
Patch p = patches.get(n - 1);
if (p.files().stream().anyMatch(f -> f.startsWith("tests/"))) {
// The guard: a hook or CI rule rejects any patch that edits the tests the person wrote.
System.out.println("attempt " + n + ": " + p.why() + " -> rejected, it edits " + String.join(", ", p.files()));
continue;
}
List<String> failing = new ArrayList<>();
for (Test t : TESTS) if (p.fn().apply(t.cents(), t.code()) != t.want()) failing.add(t.code() + "@" + t.cents());
if (!failing.isEmpty()) {
System.out.println("attempt " + n + ": " + p.why() + " -> " + (TESTS.size() - failing.size()) + "/" + TESTS.size() + " pass, failing: " + String.join(", ", failing));
} else {
System.out.println("attempt " + n + ": " + p.why() + " -> " + TESTS.size() + "/" + TESTS.size() + " pass, done");
break;
}
}
}
}attempt 1: implement discount codes -> 4/5 pass, failing: HALF@4000
attempt 2: make the failing test pass -> rejected, it edits tests/test_discount.py
attempt 3: add the minimum total for HALF -> 5/5 pass, done
In a real repository the guard is a hook that blocks writes to tests/ during the agent's run, or a CI check that fails when the agent's branch changes protected test files.
5. In your five tools
Every tool supports every level; what differs is the built-in help. Checked 4 November 2026.
| Level | What to use |
|---|---|
| Vibe | Any agent's chat, or an app builder; keep it away from real data and credentials |
| Intent brief | Plan mode (Claude Code, Cursor, Copilot, Codex, Antigravity): send the brief, review the plan, then let it build |
| Spec-driven | Module 08a: Kiro's spec mode, GitHub Spec Kit, or the portable SDD skill in any agent |
| Test-driven | Tell the agent to run the tests and keep going until they pass, and to show the output; in Claude Code a /goal condition or a Stop hook enforces it; protect test files with a hook or CI rule |
6. Remember it
How this hook works: what “Napkin, brief, drawings, inspection” means
The story → the concept
| Building | Coding agent |
|---|---|
| Napkin sketch for a shed | Vibe coding a prototype |
| A brief for a kitchen: goal, budget, the wall not to touch | Intent brief: goal, constraints, success criteria |
| Architect's drawings for a house | A reviewed spec |
| Inspection against regulations | Tests written first, which the agent can't change |
The trap it saves you from
Vibe-coding something that turns out to be the hospital: code that ships, lives for years and handles money, built with no spec and no tests.
7. Common mistakes
8. Hands-on lab (roadmap week 8)
- Vibe: build a tiny tool from one prompt. Note what you'd have to check before trusting it.
- Intent: write an intent brief for a real small feature; ask for the plan first; record what the plan revealed.
- Test-driven: write five failing tests for a function, commit them, protect
tests/with a hook, and let the agent work until they pass. Did it try to edit a test? - Compare: for one feature, note time spent and defects found at each level.
- Write the rule: a short table for your team's AGENTS.md mapping kinds of work to rigor levels.
9. Self-check quiz
What does an intent brief contain?
The agent's patch makes a failing test pass by changing the test's expected value. What should happen?
You need a script to rename 200 photos once, on your own laptop. Which level?
What's true of the term "intent-driven development"?
In Google's AI-assisted code migrations (C10), what judged the model's edits?
10. Go deeper
| Resource | Time | Why |
|---|---|---|
| Best practices for Claude Code | 20 min | Give the agent a check to run, ask for evidence, review against the spec. |
| Augmented Coding: Beyond the Vibes (Kent Beck, June 2025) | 15 min | TDD with an agent, and the signs it's going off track. |
| TDD, AI agents and coding with Kent Beck (The Pragmatic Engineer) | 1 h | Why TDD becomes more valuable with agents, including agents deleting tests. |
| Intent-driven development (intent-driven.dev) | 10 min | One of several 2026 descriptions of the term. |
| Module 08a: Spec-driven development | 45 min | The spec level in depth, with EARS requirements and a worked example. |