The jokes about vibe coding aged poorly.
Three years ago, I wrote a short article after I managed to build a terminal Spotify client using nothing but Ctrl+C and Ctrl+V. Back then, in What To Focus On When Programming With Gpt4?, I predicted what would happen in the near future, and so far those predictions have turned out to be fairly accurate. At the time, I often encountered skepticism and opinions that AI would not replace programmers. It basically already has. Nobody in their right mind writes code by hand anymore. We have reached a point where knowing the language itself no longer matters. Agents know it better.
Later came the jokes about vibe coding. And those jokes have aged very badly. Sure, you can create an application that scales terribly and is difficult to maintain. But this is relatively easy to address. Practices around using AGENTS.md appeared, followed later by SKILLS. In 13 Programming Books as AI Agents Rules, I described one way of turning software-engineering habits into rules for coding agents. And to a large extent, these rules solve the problem. You can use them to instruct the agent how it should organize the code, which patterns it should use, what layers the project should have, and how much emphasis we want to put on bounded contexts, which, in my opinion, are currently crucial in software projects. By following these rules, you can create a project with much higher code quality than the ones I used to write manually, when I had to balance implementation speed against code quality.
For a vibe-coded project to have high code quality, however, it is worth focusing on a few things that are rarely discussed. You can put very strong guardrails around an agent by imposing strict rules on how it works and forcing it into a certain engineering discipline. I do this through a demanding quality check, not only in CI, but already in a pre-commit hook.
The quality check consists of several steps. First, I run a linter. At this stage alone, you can get rid of not
only ugly code, but also long blocks of code. This is very cheap in terms of CPU usage and execution time. I also
run the linter with automatic fixes enabled, which takes some work away from the agent and makes the process
deterministic. The next step is checking for code smells. Not everything can be measured, but many code smells can.
If there are code smells in the code, the agent has to refactor what it produced already at commit time. This
prevents their number from accumulating. Without this step, they can grow very quickly. I measured this while
building a project from a backlog of 80 tasks without any control over the agent. The most common problems were
DuplicateMethodCall. For example:
def full_name(user)
name = user.profile.name
"#{name} (#{name.length})"
end
It produces something like this:
def full_name(user)
"#{user.profile.name} (#{user.profile.name.length})"
end
This is not optimal and, when accessing the value requires a database query, it can lead to unnecessary queries.
The second most common problem is TooManyStatements. The model keeps adding more steps to an existing
method instead of stopping for a moment and thinking about how to separate responsibilities. So it can produce
something like this:
def process_order(order)
validate(order)
calculate_total(order)
apply_discount(order)
save_order(order)
send_email(order)
log_order(order)
update_metrics(order)
end
It is better to write:
def process_order(order)
prepare_order(order)
persist_order(order)
finalize_order(order)
end
def prepare_order(order)
validate(order)
calculate_total(order)
apply_discount(order)
end
def persist_order(order)
save_order(order)
end
def finalize_order(order)
send_email(order)
log_order(order)
update_metrics(order)
end
And even better to extract separate objects:
def process_order(order)
OrderPreparer.new(order).call
OrderRepository.new.save(order)
OrderFinalizer.new(order).call
end
The list of problems it produces is much longer, though, and it is worth seeing what it looks like on a chart:
Enforcing the avoidance of code smells at commit time therefore solves many problems.
The next stage is test coverage. A large number of tests obviously does not give us 100% confidence that the tests themselves are correct. But it does guarantee that the agent has to at least pay attention to them and think about what happened when one of them fails after a piece of code changes. Requiring 100% coverage is therefore a useful safeguard. It also helps with periodic refactoring.
At the end of the pre-commit process, all tests are run. Later, the process is repeated in CI, which gives us confidence that the tests also pass in an environment close to production. Which, of course, is nothing new.
Outside of the quality check, supervision is really only necessary in the context of code organization at the application structure level: layers, directories, files, and their organization. SKILLS are obviously very helpful here as well. This is crucial when starting a project and when we know how large it is going to be. Based on that, we choose an architecture appropriate for the project, and if the project grows more than we initially expected, we supervise the refactoring.
The jokes about vibe coding aged poorly, as long as you know how to vibe code and use common sense.