You Cannot Debug Your Way to a System
I watched a 2 hour vibe coding video last week. The developer built the entire thing without once reading the code.
That is not a criticism. It is a description of what the work has become, and he was good at it.
Watch what he was actually doing for those 2 hours. He was choosing which tool handled which part. He was reading the instruction stream between the agent and the model. He was wiring in the security components, connecting services that were never designed to talk to each other, and stitching the pieces into something that ran.
That is system integration. It is real engineering work, and it is difficult. He never checked a line of syntax, and for that job he did not need to.
This is the new practice of software development. Arguing that it is not development is arguing about a practice that has already evolved.
What the Agent Actually Bought Me
I have been coding with an agent for over a year. I am the sole developer on my team, so the agent is not an experiment. It is how my tasks get done.
The system is not small. Hundreds of modules and functions, a relational database carrying graph relationships across the data, and a retrieval layer that started as basic RAG and evolved into graph RAG once the naive version stopped returning results I could trust.
The agent saved me real time. At the syntax level the gain is not marginal. It writes the function, handles the boilerplate, remembers the library signature I would have gone looking for. I do not read every line it produces either. I pick where I review, and I pick based on what the code can break, not on how much of it there is.
So on the surface, my practice looks like the video. Instruct, receive, move on.
The difference sits underneath.
Where I Do Not Hand Over
I design the architecture. I write the functional specification. Every decision about how the system is shaped, what sits in which layer, what talks to what, and what the data has to guarantee stays with me.
That is not caution. It is the result of watching what happens when I let go of it.
In my experience over the past year, close to 99% of the time when I asked the agent to make a system level decision, it has delivered 1 of 2 mistakes. It designs the wrong thing, or it designs far more than the problem needs.
The second failure is the expensive one, because it does not look like a failure.
Over-Engineering Does Not Announce Itself
Ask an agent to solve a problem and it reaches for the most complete pattern it has seen. An abstraction layer for a case that has one variant. A configuration system for a value that will never change. Error handling for conditions the architecture already makes impossible. 3 helper functions where the logic belonged in one place.
Each piece is defensible on its own. That is why it survives review. Nothing in it is wrong, so nothing gets removed.
Then it compounds. The abstraction that had one use case now has one use case and a maintenance cost. The next feature gets built on top of it, because it is there and it looks intentional. 6 weeks later part of that structure serves nothing at all, and nobody can say when it stopped being needed.
That is dead code. In a system you have to keep evolving, dead code is not neutral. It is a cost applied to every future change.
The agent will not catch this. It has no memory of why the structure exists and no stake in what it costs later.
Two Ways to Reach Working Code
Which brings me back to the video, and the thing I keep returning to. In 2 hours, the vibe coder never pushed back once.
The loop was: instruct, run, look at the interface, find the error, point the agent at the error, run again. The application got built by fixing what broke, in the order it broke.
It works. The result displayed in the UI shows everything looks fine.
My method is the opposite one. I would rather prevent the error than find it. The specification exists before the instruction. I decide what the layer boundary is, what the data has to guarantee, what a function is allowed to touch, and the agent writes to that.
An error-driven loop catches everything the runtime reports, and that is not a small category. But a design failure does not throw an error. Over-engineering runs fine. A redundant abstraction runs fine. Dead code sits in the codebase perfectly, forever, doing nothing at all. A layer boundary in the wrong place will pass every test until the day you need to change something on the other side of it.
None of that reaches the interface. So a loop driven by what the interface reports cannot see it. Not because the developer is careless, but because the method has no channel for it.
Regeneration Is Not a Fix
A position follows from this that I hear often now. If AI writes code this fast, why troubleshoot at all. Throw it away and ask for it again.
For a contained piece of work with nothing depending on it, that is reasonable. Cheaper to regenerate than to read.
As a general method it costs the thing that is hardest to recover. Every regeneration is a fresh design decision, and the agent is the one making it. Ask twice and you get 2 plausible structures that do not agree with each other. Build the next feature on whichever one arrived most recently, and the system stops being something you designed. It becomes a sequence of accepted defaults.
An Idea Is Not a Mental Model
Both failures trace back to the same missing thing. What directs the work is not an instruction. It is the mental model sitting behind it.
An idea is what you want to exist. A mental model is how the thing works: which components carry which responsibility, where the data moves, what each tool is actually good at, and what breaks when one of those assumptions is wrong. Ideas are cheap and everyone has them. A mental model is built from experience, from know-how, and from specific knowledge about specific tools.
Two examples from my own work, one small and one structural.
A user specifies a location, FR for France, on a website published in English. Should the output come back in French or in English? Nothing in the input answers that. The answer depends on what the work is for, and a French query and an English query in the same market do not come from the same population. Choosing one changes the finding, not just the wording. An agent will pick one, quietly, and it has no basis for knowing the choice carries a consequence at all.
The second is the retrieval layer. Moving from naive RAG to a graph model was not a preference for a better result. Everyone wants a better result. Naive retrieval matches on similarity. It returns the chunks that resemble the query, and it cannot follow a relationship from one record to another, because it has no concept of a relationship. Recognizing that as the limit required knowing how relational data behaves and what it means to traverse rather than match.
I either had that knowledge already or I had to go and get it. There was no third option where the agent supplied it for me, because the agent cannot tell me which limit I am hitting when I have not framed the problem well enough to ask.
This is why free-form idea-driven development does not hold. Describe what you want and ask the agent to build it, and it will build something. It always builds something, and it will look right. But every decision inside it was made by a model working from patterns, not from your understanding of the problem, because you did not have one to give it.
The Line Worth Drawing
The argument about whether developers should still read code picks a visible habit and treats it as the measure. It is the wrong argument.
The line that matters is whether you own the system design or the agent does.
Read no syntax and hold the architecture, and the agent is the fastest colleague you will ever work with. Hand over the architecture and you are not directing a system. You are accumulating one, decision by decision, none of which you made.
The code was never the hard part. It was just the part that used to be visible.


