Maciej Ciemborowicz

Hello, my name is Maciej Ciemborowicz.

This will be my monument.

The jokes about vibe coding aged poorly

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:

Code smell chart from an AI-generated backlog implementation
Code smells accumulate quickly when the agent is allowed to keep shipping without structural feedback.

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.