InterviewKata
AI engineering · Module 08c · Level 2: Working with AI

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.

A 3-minute conversation: Sam asks, Priya explains
0:00 / 3:22

1. The idea

Building work. You sketch a garden shed on a napkin and let a friend build it: if it leans, you laugh and rebuild it. For a kitchen extension you give the builder the goal, the budget and "don't touch the load-bearing wall". For a house you hand over architect's drawings. And for a hospital, inspectors test every system against the regulations before anyone moves in.
VibeDescribe, accept, see if it works. Little or no review.
IntentGoal, constraints and success criteria; the agent plans the how.
SpecA reviewed specification the agent implements against (Module 08a).
Test-drivenTests written first; done means they pass, and they can't be edited.
LevelYou writeDone meansCatches mistakesCosts you
Vibe codingA prompt, then more promptsIt looks like it worksLate, if everAlmost nothing up front; a lot later if it ships
Intent briefGoal, constraints, success criteria, non-goalsThe success criteria are metWhen you check the criteriaTen minutes of thinking
Spec-drivenRequirements, design and tasks, reviewedEvery requirement is implementedIn spec review, before code existsAn hour or more per feature
Test-drivenFailing tests (or evals) firstThe tests pass, uneditedOn every run, automaticallyWriting 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

  1. Write the tests from the success criteria, or have the agent draft them and review every one yourself. Confirm they fail.
  2. Commit the tests and protect them: a hook or CI rule rejects any change to them from the agent's branch.
  3. Let the agent iterate: change code, run the tests, read the failures, change code again, until they pass.
  4. Ask for evidence: the command it ran and the output, not "all tests pass".
  5. 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

    SituationLevelEvidence
    Trying an idea, a demo, a personal scriptVibeThe point of Karpathy's term: speed when nothing depends on the result
    Anything near production data, even in an experimentNot vibe: gates and separate environmentsReplit'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 openIntent brief, then the agent's planCheap to write; the plan review catches misunderstandings early
    A production feature across several files in a codebase with conventionsSpec-drivenModule 08a's worked example; specs are reviewed before any code exists
    Large changes where correctness is checkableTest-driven: builds and tests are the judgeGoogle's AI-assisted migrations: tools find the code, the model edits, builds and tests decide, people review (C10)
    Algorithms, parsers, data structuresTest-drivenKent 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.

    LevelWhat to use
    VibeAny agent's chat, or an app builder; keep it away from real data and credentials
    Intent briefPlan mode (Claude Code, Cursor, Copilot, Codex, Antigravity): send the brief, review the plan, then let it build
    Spec-drivenModule 08a: Kiro's spec mode, GitHub Spec Kit, or the portable SDD skill in any agent
    Test-drivenTell 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

    Memory hook: "Napkin, brief, drawings, inspection"The longer the code lives and the worse a mistake would be, the further right you go.
    How this hook works: what “Napkin, brief, drawings, inspection” means

    The story → the concept

    BuildingCoding agent
    Napkin sketch for a shedVibe coding a prototype
    A brief for a kitchen: goal, budget, the wall not to touchIntent brief: goal, constraints, success criteria
    Architect's drawings for a houseA reviewed spec
    Inspection against regulationsTests 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

    Vibe code that shipsA prototype becomes production without anyone deciding it should. Rebuild it at the right level, or at least add tests and review before it ships.
    Letting the agent edit the testsAn agent asked to make tests pass may change the tests. Protect them, and read every test change yourself.
    Tests written by the agent, unreviewedTests that encode the agent's misunderstanding make the misunderstanding pass.
    A spec for a one-line fixRigor costs time. A clear sentence and a test is enough for small, well-understood changes.
    Intent without success criteria"Make checkout better" gives the agent nothing to check against. Say what done looks like.
    Trusting "all tests pass"Ask for the command and its output; agents sometimes report success they didn't verify.

    8. Hands-on lab (roadmap week 8)

    1. Vibe: build a tiny tool from one prompt. Note what you'd have to check before trusting it.
    2. Intent: write an intent brief for a real small feature; ask for the plan first; record what the plan revealed.
    3. 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?
    4. Compare: for one feature, note time spent and defects found at each level.
    5. 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?

    What and why, the limits, and what done looks like; the agent proposes the how.

    The agent's patch makes a failing test pass by changing the test's expected value. What should happen?

    The tests are the specification. Changing them to match the code turns a bug into a passing test.

    You need a script to rename 200 photos once, on your own laptop. Which level?

    Short-lived, easy to check, little at stake. Keep a backup of the photos.

    What's true of the term "intent-driven development"?

    Write-ups describe it loosely; the useful practice is the intent brief.

    In Google's AI-assisted code migrations (C10), what judged the model's edits?

    Test-driven at scale: automated checks first, then human review.

    10. Go deeper

    ResourceTimeWhy
    Best practices for Claude Code20 minGive the agent a check to run, ask for evidence, review against the spec.
    Augmented Coding: Beyond the Vibes (Kent Beck, June 2025)15 minTDD with an agent, and the signs it's going off track.
    TDD, AI agents and coding with Kent Beck (The Pragmatic Engineer)1 hWhy TDD becomes more valuable with agents, including agents deleting tests.
    Intent-driven development (intent-driven.dev)10 minOne of several 2026 descriptions of the term.
    Module 08a: Spec-driven development45 minThe spec level in depth, with EARS requirements and a worked example.