Some days you get a week of work done before lunch. Other days you spend four hours in a conversation that goes nowhere, throw the branch away, and write the thing by hand in forty minutes. Same tool, same you, same codebase.
You have probably explained this to yourself as the model having a bad day. It is worth considering the less flattering explanation, which is that the two days were different in a way you can point at.
The thing that separates them
On a good day you knew what “done” looked like before you started. Maybe you did not write it down, but you could have: the function has this signature, these three cases are covered, the existing tests still pass. You would recognise the finished thing on sight, so each round of output got measured against something.
On a bad day you did not have that, and the loop had no way to end. Output arrives, you read it, something feels off, you ask for an adjustment. Nothing in that cycle can terminate it except your patience running out, which is why bad days end with an abandoned branch rather than a decision.
Vibe coding is not a description of how careful you are. It is a description of a loop with no stated exit.
The same task, both ways
Here is the shape of it. A validation rule needs adding to an endpoint.
open loop
"add validation to the pricing endpoint"
... 40 lines back, three of them wrong for reasons you spot on read three
"no, the currency check should happen before the discount"
... rewritten, now the error messages changed
"keep the original error messages"
... now a test fails that was passing
(four rounds later, you take the first version and fix it by hand)
defined
"add validation to the pricing endpoint. Done when:
- rejects negative amounts with the existing InvalidAmount error
- currency validated before discounts are applied
- all existing endpoint tests still pass
- no new dependencies"
... one round, three of four met, the fourth is stated and obvious
"the currency check runs after discounts, move it"
... done
Nothing in the second version is clever prompting. It is four lines of acceptance criteria written before starting instead of discovered one disappointment at a time. The work of writing them is the work you were going to do anyway, just front-loaded to where it can be used.
Why the criteria are the whole trick
Read the two blocks again and notice what the second one does that the first cannot: it makes each round of output checkable by something other than your reaction. Three of four met is a fact. “Something feels off” is not, and you cannot act on it except by asking for a rewrite and hoping.
This is the same move as writing a program instead of asking for reasoning, one level up. There, a specification replaces a request. Here, an exit condition replaces a vibe. Both convert a judgment you were going to make repeatedly, badly and differently each time into one you make once, in advance, on purpose.
Where it goes once it works
Criteria you write twice belong in a file rather than in a message. A short, checked-in definition of what “done” means for a class of task costs one afternoon and stops the loop from reopening every time:
## Done, for an endpoint change
- new validation returns the error type already used in this module
- no new dependency without a note saying why
- existing tests for the endpoint pass unchanged
- the change is one commit and one concern
Four bullets. It is not a process, it is not ceremony, and nobody needs to be trained on it. It exists so that the answer to “is this finished” stops being a matter of how tired you are.
The test for tomorrow
Before the next task, write the one sentence that says how you will know it worked. If you cannot write it, that is not a reason to skip it. That is the finding: you are about to start a loop with no exit, and now at least you know which kind of day it is going to be.
If your team’s output swings wildly and nobody can say why, that’s the work I do.