The Vibe Coding Method: AI Writes, You Read
By the end of this chapter, you should be able to:
- Take a one-line request — "add a command to jackdaw that lists the models in the config" — and turn it on the spot into a spec with goal, constraints, and acceptance all spelled out, naming the failure each element guards against;
- After the AI says "done", know which four cargo commands to run, in what order, and why the model saying so doesn't count;
- Face a several-hundred-line diff and know which chunks you can let pass, which four spots you must read line by line, and why the commands won't catch those four for you.
The contract: the AI owns the how, you own the what
Chapter 2 described an agent as a loop: messages in, model answers, results fed back, repeat. Your collaboration with the AI is the same loop: you send a prompt, it returns a diff, you paste command output back, it keeps fixing — your request is the user message, the diff is the assistant message, the diagnostics you paste back are the results fed into the loop. One mismatch matters: in the agent loop, the model decides which tool to call; here, you decide which commands to run and when to stop. The bottleneck of this loop is not the model — it's every verdict you hand down.
The contract, in three sentences: the AI is responsible for how the code is written; you are responsible for what it does and what counts as correct; the compiler and the tests decide who is right. Vibe-coding failures follow one path: vague request → confident, wrong code → you can't read it → all that's left is to believe or not believe. The contract replaces "believe or not" with "verify or not".
Spec first: goal, constraints, acceptance
Running example: add a small feature to jackdaw, this gateway — list every model in its config. A bad spec is one sentence:
Add a command to jackdaw that lists the models.
The AI will hand back code that compiles. Whether it's what you wanted is luck. Spec-first turns luck into terms. Three elements:
- Goal: the behavior change, one sentence a user would understand — "running
jackdaw models listprints every model in the config, inprovider/modelform"; - Constraints: what it must not touch and must follow — "read-only on the config file; no new dependencies; output sorted alphabetically";
- Acceptance: what "done" means — "run against the repo's example config, output is exactly 5 lines;
cargo testis green".
Put one countable fact into the acceptance (how many lines, which exit code): without reading all the code, you can still catch "clearly wrong" on the spot.
Small steps: one verifiable change at a time
Don't throw the whole spec at the AI and ask for the finished product. Split it:
- Add the subcommand skeleton that does nothing; get it to compile;
- Collect model names from the config;
- Sort, print, add the test.
After each step, run the next section's arbitration; move on only when it's green. Why refuse to do it in one shot? You can't review a huge diff, and rolling it back hurts; with three steps, a mistake costs one third of a redo. How small is small enough? Rule of thumb: one acceptance item maps to one change, one commit.
Four arbiters: check, test, clippy, fmt
"I'm done" from the AI carries no weight; four Cargo subcommands do:
cargo check # does it compile: are the types and names right
cargo test # is the behavior right: does it meet the acceptance terms
cargo clippy # is it idiomatic: is there a more conventional way
cargo fmt # is the formatting uniformHow to read this: each command answers exactly one question and stays in its lane — clippy doesn't judge business logic, test doesn't care about formatting. "Arbitration" means: when you and the AI argue over whether the output is really sorted, you don't compare eloquence — you write a test that asserts the order and let test deliver the verdict; when you argue over whether the code is idiomatic, clippy does. Paste diagnostics back to the AI verbatim; not understanding every one of them yet doesn't stop you from using them to arbitrate. But "this step passed" must come from command output, never from the model's say-so. (Each command in detail: chapter 8.)
Asking for readable code: comments, naming, minimal diffs
Readability isn't begged for; it goes into the spec. Three clauses worth using:
- Comment density: one "why" comment per 10–15 lines of code — intent, not a translation of the code;
- Naming: verb-object function names;
list_modelsbeatsprocess; - Minimal diff: touch only the lines the goal requires; no drive-by refactors of unrelated code.
The last clause gets violated most. Here is a sample diff (for teaching only, not real jackdaw source; you likely don't know the syntax yet — on a first pass, look only at structure and behavior; the details unfold across chapters 4–7):
fn print_all(models: &[String]) {
- for m in models {
- println!("{}", m);
+ for model in models {
+ println!("{}", model);
}
}
+
+fn lookup(models: &Vec<String>, name: &str) -> Option<usize> {
+ let owned: Vec<String> = models.clone();
+ owned.iter().position(|m| m == name)
+}How to read it: start with the paired deletions and additions at the top — the only change is renaming m to model, unrelated to "sort and print"; that's smuggled-in churn you send back. Then look at the new lookup: it receives a read-only list, yet its first line copies the whole list before searching — a full clone just to read. That line is exactly the "must understand it yourself" spot from the next section. The rename, meanwhile, produces zero value and eats your review attention.
Where you must understand it yourself
The four arbiters block most risk, but four kinds of places are beyond them — there, you read line by line:
- Ownership transfer: who takes the data, where a copy happens.
.clone()is always a deliberate decision — whether it's worth it, the compiler won't judge. (Chapter 5.) - Concurrency: two pieces of code touching the same data, where interleaving changes behavior — and tests tend to miss exactly that. (Chapter 7.)
- unsafe: a block that explicitly steps around Rust's memory-safety guarantees; the compiler has dropped its warranty, and human review is the last gate. jackdaw has zero lines of unsafe in the whole project — if you see it, stop.
- A swallowed error:
let _ = write_line(&mut out, line);How to read it: let _ = discards the return value of write_line entirely. If that value is an error, nothing here handles or reports it, and the layer above still believes all is well. Errors should propagate upward (chapter 6), not evaporate on the spot; searching a diff for let _ = and .ok() — both error-swallowing shapes — is a fixed move when you review AI code.
Outside these four spots, a first pass over a diff may skip details: check function signatures and "why" comments against the acceptance terms, then move on. You don't need to understand everything now — you need to know where you'll have to come back.
Summary
Spec-first turns a vague request into terms you can arbitrate; small steps keep every change cheap to verify; check, test, clippy, and fmt are arbiters nobody can sweet-talk. Ownership transfer, concurrency, unsafe, and swallowed errors sit beyond them — those you read yourself. Next chapter: Rust itself, starting with variables, functions, and expressions.
Self-Check
- Expand "add failover to the gateway" into a three-element spec — at least one item per element.
- One AI commit changes routing logic, renames 20 variables, and turns one function into unsafe. How many steps do you take with it, and on what grounds?
cargo testis all green. Why can't that replace reading the linelet _ = write_line(...)yourself?
Answers
- Goal: when an upstream request fails, the gateway retries with the next model in the list, invisible to the caller; constraints: only swap the upstream, don't touch the request body, no new dependencies; acceptance: when the first upstream refuses, the response comes from the second upstream's success, and the "entire list failed" branch has test coverage.
- Three steps: first demand three separate diffs — review the routing change against the acceptance terms; the renames are unrelated churn, return them under the "minimal diff" clause; unsafe gets its own line-by-line human review, because command arbitration doesn't cover unsafe — it's one of the four must-read spots.
- Tests only verify the paths that actually execute. After
let _ =discards the error, the program runs to completion "successfully" while carrying a broken state; a test asserting the happy path stays green. The evidence of a swallowed error is never in the test output — it's in that line of code.