The jokes about vibe coding aged poorly A developer who previously predicted AI's rapid takeover of software engineering says the jokes about "vibe coding" have aged poorly, arguing that AI agents now handle most implementation work while programmers shift toward architecture, requirements, and review. To keep AI-generated code maintainable, the developer enforces strict guardrails through pre-commit hooks and CI, combining linters with automatic fixes and static code-smell analysis, and codifies engineering habits into agent rules such as AGENTS.md and SKILLS. The developer reports that projects built this way achieve higher code quality than manually written ones, and that without structural feedback, problems accumulate quickly across large task backlogs. 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? https://maciej-ciemborowicz.eu/articles/what to focus on when programming with gpt , 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 never be able to program well because it would always make mistakes and hallucinate. Meanwhile, the way we build software has changed very quickly. Less and less code is written entirely by hand, while an increasing share of the work is being done by AI tools and agents. Knowing a specific programming language is also becoming less important. What matters much more is the ability to read and evaluate code, understand architecture, and clearly define what actually needs to be built. In that sense, AI has already taken over a large part of the work that programmers used to do manually. The role of the programmer itself is gradually shifting toward that of an architect, a product developer, and, put simply, someone who coordinates and supervises the work of AI agents. Later came the jokes about vibe coding. And those jokes have aged very badly. I am using the term fairly loosely here. I do not mean blindly accepting whatever an agent produces without reading the code or understanding the project. I mean building software where most of the implementation is produced by AI and the programmer focuses mainly on requirements, architecture, constraints, review, and feedback. Sure, you can create an application this way that scales terribly and is difficult to maintain. But this can be addressed to a large extent. Practices around using AGENTS.md appeared, followed later by SKILLS. In 13 Programming Books as AI Agents Rules https://github.com/ciembor/agent-rules-books , I described one way of turning software-engineering habits into rules for coding agents. These rules do not solve the problem by themselves, but they can prevent a large class of recurring problems. 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 larger software projects. By following these rules, I can create projects with much higher code quality than the ones I used to write manually, when I constantly 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 many unnecessarily long blocks and methods. 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, and a code smell reported by a static analyzer does not automatically mean that the code is bad. But many suspicious patterns can be detected mechanically. If one appears, the agent has to at least reconsider the code and either refactor it or make a conscious decision that the warning is acceptable. This happens already at commit time, before these problems have a chance to accumulate. Without this step, they can grow very quickly. I measured this while building a project from a backlog of 80 tasks without any structural feedback for the agent. The most common problem was DuplicateMethodCall . For example, AI can produce something like this: python def report summary report "Average: {report.calculate statistics.average}, median: {report.calculate statistics.median}" end This performs the same potentially expensive operation twice. Depending on what the expression does, repeated calls like this can be merely unnecessary or actually expensive. It is often better to make the intermediate result explicit: python def report summary report statistics = report.calculate statistics "Average: {statistics.average}, median: {statistics.median}" end The point is not that every duplicate method call is a serious problem. The useful part is that the agent gets immediate feedback about something worth looking at instead of repeating the same pattern throughout the project. The second most common problem is TooManyStatements . The model tends to keep adding more steps to an existing method instead of stopping for a moment and thinking about whether the responsibilities should be split. So it can produce something like this: python 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 Depending on the context, it may be clearer to separate these responsibilities: python 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 in a larger application it may make sense to extract separate objects: python def process order order OrderPreparer.new order .call OrderRepository.new.save order OrderFinalizer.new order .call end Of course, splitting a method only to satisfy a static analyzer does not automatically make the code better. It can just as easily create unnecessary indirection. The value of this check is that it forces the agent to stop and reconsider the structure instead of endlessly extending whatever method already happens to exist. The list of problems it produces is much longer, though, and it is worth seeing what it looks like on a chart: Enforcing these checks at commit time therefore does not guarantee good code, but it prevents many simple structural problems from silently accumulating. The next stage is test coverage. A large number of tests obviously does not give us confidence that the tests themselves are correct. The same is true for 100% coverage. I do not treat it as a quality metric, because coverage only tells us that the code was executed, not that the right behavior was actually verified. I treat it as a mechanical constraint that prevents the agent from adding completely untested paths without noticing. It also forces the agent to look at the tests whenever a refactoring changes behavior or makes some branch unreachable. For that reason, requiring 100% coverage is still a useful safeguard, especially when agents are modifying the code repeatedly. 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 same checks also pass outside the local environment. Which, of course, is nothing new. What is new is the economics of this feedback loop. A strict pre-commit hook can be annoying for a human developer, because every failure means stopping, going back to the code, fixing it, and trying again. An agent does not really care. It gets an error, changes the code, runs the check again, and repeats the process until it passes. This makes checks that used to feel overly strict much more practical. Outside of the quality check, most of my supervision is necessary in the context of code organization at the application structure level: layers, directories, files, boundaries, and their organization. SKILLS are obviously very helpful here as well. This is especially important when starting a project and when we have some idea of how large it is going to become. 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. This is also where I think the programmer's role is changing the most. Writing the implementation is becoming the cheap part. The more important part is defining the constraints under which the implementation is created: the architecture, boundaries, tests, static analysis, conventions, and feedback loops. Instead of describing every line of code, we increasingly describe the environment in which the code is allowed to exist. The jokes about vibe coding aged poorly. Not because blindly generated code suddenly became good, but because we learned how to put enough engineering around the agent that blindly trusting it is no longer necessary.