Stop vibe coding in a fix-it loop
You know the loop. The app mostly works, so you run it, something looks wrong, and you type fix it. The model fixes it. You run it again. Now something else is wrong — maybe the thing it fixed two turns ago. You type fix that too. Forty minutes later you have a working header, a broken checkout, no idea which change did what, and a chat scrollback you would not wish on a code reviewer.
Every person who has built something real with an AI coding tool hits this wall. The usual diagnosis is that the model is not smart enough yet. It is almost never the model. It is that you are asking one conversation to do four different jobs at once, and only one of them is coding.
Why "fix it" stops scaling
Thinking of something, deciding whether it is worth doing, working out how to do it, and doing it are four separate activities. In a fix-it loop they collapse into one prompt box, and the collapse costs you more than it looks:
- The model optimizes for the last thing you said. It has no way to know that the thing you mentioned in passing nine turns ago was load-bearing and the thing you just said was a half-formed idea.
- Nothing defines done. Without a written finish line, "done" becomes "the error went away," which is how you end up with a caught exception where a fix should be.
- The thinking lives in scrollback. Close the session and it is gone. Open a new one and you re-explain the project from scratch, slightly differently, and get a slightly different architecture.
- You cannot review a decision that was never written down. By the time you see it, it is a diff, and reviewing a diff is a far worse place to notice that the whole approach is wrong.
- Duplicate and contradictory work goes unnoticed. Two sessions solve the same problem two ways and both land.
Treat your first thoughts as a rough draft
Here is the uncomfortable part. Your ideas, at the moment you have them, are not good yet. They arrive while you are looking at something annoying, they are phrased as complaints, half of them are the same idea wearing different clothes, and a few are actively bad ideas that feel urgent because you just got irritated by them.
That is fine — that is what first thoughts are. The mistake is handing them straight to a model that will dutifully implement all of them, including the bad ones, in the order you happened to think of them.
So put a process between the thought and the execution. It does not have to be heavy:
- 01Capture wide, and do not filter.
Every idea, annoyance and bug goes into the list the moment you notice it. Filtering while capturing is how good ideas get lost and how you end up derailing the thing you are currently building.
- 02Dedupe.
Half your list is the same three problems described from different angles. Merging them is usually the moment the actual underlying problem becomes visible — and it is often not what any of the individual items said.
- 03Edit each survivor into something specific.
"Search is bad" is not a task. "Search returns nothing when the query has an apostrophe" is. If you cannot make it specific, you do not understand it yet, and neither will the model.
- 04Order by dependency, then by value.
Some things are impossible until something else exists. Get that ordering right on paper and you avoid the rewrite where the model builds against a foundation you were about to replace.
- 05Write down what done means.
One or two lines of acceptance criteria per item. This is the highest-leverage sentence you will write all day.
A prompt is not a plan
The difference between prompting and planning is not length. It is what the artifact can do for you once it exists.
| Ad-hoc prompting | A written issue | |
|---|---|---|
| Where the thinking lives | Chat scrollback | The tracker |
| When "done" gets defined | After you see the diff | Before work starts |
| Survives a fresh session | —no | ✓yes |
| Reviewable before any code exists | —no | ✓yes |
| Two efforts quietly solving the same thing | Common | Caught at dedupe |
| What you hand the next person | A repo and a shrug | The reasoning, linked to the change |
Acceptance criteria is the load-bearing row. Everything else is convenience; that one changes the model's behavior. Given a finish line it can check itself against, an agent will keep working until it gets there. Given "fix it," it will stop at the first state that no longer throws — and those two stopping points are very far apart.
Then let it run
Once the plan is written and you have actually read it, the execution side changes character completely. You are no longer supervising a conversation. You are handing over a self-contained unit of work — one issue, with its context, its constraints and its finish line — and letting the model plan, build, and open a pull request against it.
The review gets easier too, because you are no longer asking "is this good code?" in the abstract. You are asking one answerable question: does this do what the issue said? You wrote that issue while thinking clearly, before you were annoyed and eight turns deep.
And when it comes back wrong, you have something the fix-it loop never gives you: a fixed reference point. Either the code missed the issue, or the issue was wrong. Both are fixable. "It still feels broken" is not.
What this looked like on this site
The site you are reading was built this way, and its own history is a reasonable demonstration. The proposal system behind it — database, auth, REST API, password-gated viewer, email, tracking, CLI, admin dashboard — went from nothing to shipped in an afternoon, and the git history shows why that was possible.
- 17:29:11 – 17:29:57Eleven issues, filed in 46 seconds
Infra, schema, auth, API, viewer, email, tracking, CLI, admin, CI — the whole backlog, in dependency order, arriving as one batch. Nothing lands in a repo that fast by accident: the thinking had already happened, and this was the transcription of it.
- 17:58Issue #12 gets written properly
Not a sentence. A stated dependency, the exact file paths, a TypeScript signature for the registry it needed, a checklist of every component with its prop shape, and an Acceptance section naming the finish line.
- 18:22#12 closes
Twenty-four minutes after it was filed, merged as pull request #20. The execution was fast because every decision in it had already been made and reviewed.
- SinceThe habit stuck
Most commits on main still arrive through a numbered pull request that closes a written issue — the numbering is past seventy now. The plan is in the tracker, the reasoning is linked to the change, and the chat scrollback is disposable.
Twenty-four minutes from filed to merged is the number people usually react to, but it is the least interesting one. The interesting number is the twenty-nine minutes between the batch of eleven and issue #12 — the time spent writing down what should be built, before anything was.
None of this makes the model smarter. It makes your instructions survive contact with a long session, which turns out to be most of the gap between a demo that mostly works and software you would put a customer on. The fix-it loop has a ceiling, and it is lower than people expect. You can step off it this afternoon, for the price of writing things down first.
One week, free. Real work planned, shipped and merged into your repo by Friday — and the code is yours either way.
Start with a conversation →