Lessons Learned

Lessons Learned

I spent three months building a real piece of software with a coding agent, and the thing I came away with was not a model release or a benchmark. It was a methodology. I thought the project would take a weekend; it spiralled into a hundred thousand lines, then four hundred thousand, written at a thousand commits a week. Somewhere in that spiral the important questions stopped being what can the model do and became how do I keep it doing the right thing at speed. This chapter is what I learned. Read it as the reflection at the end of the method — the lessons that hold no matter which model you drive.

Three ways to run an agent, and why I chose one

There are three ways to run a coding agent, and the choice is not a matter of taste. It is a matter of where you sit on the risk map.

I chose the third, and the rest of this book is the argument for it. Governance is the middle path that does not feel like a compromise, because the constraint is not a brake on the agent — it is the frame that lets you take your hands off the wheel. If you demand that the system get built but refuse to read the code, then either the agents work on the first try (they don't), or you have to monitor something. I chose to monitor failures. When I saw one, instead of asking Claude to fix it, I asked Claude how we could keep it from ever happening again.

Claude is the fastest road to hell

The single most useful sentence I can hand you is this: coding agents accelerate within the frame you give them — even when the frame is wrong. The service was costing thirty to forty dollars a day to run idle on a Kubernetes cluster, so I set the agents on a scale-to-zero push: keep the cluster, keep the custom autoscaler, and squeeze the idle pods down to nothing. For weeks they made it better and better — and it never paid off, because scaling a cold pod back up took sixty to three hundred seconds, and a five-minute cold start is a worse problem than the bill. I was optimizing within the architecture when the architecture itself was the constraint; the real move was to change it — go serverless, and let the platform own the scaling I was hand-building against its own ceiling. The model is both an implementation aid and a way to search a solution space, and if you do not keep an open mind it will only speed your descent into the circle of hell you already chose. The tell, in hindsight, was that the optimization kept hitting the same wall: when an in-paradigm fix keeps colliding with the paradigm's own limit, that recurring wall is the signal to price out a paradigm change.

The speed is not the danger. The speed exposes the danger. A bad decision that would have cost you a slow week of hand-coding now costs you an afternoon and ten thousand lines, all of them committed, all of them plausible. Velocity is a floodlight on the quality of your thinking. This is why the frame comes first and why so much of the work is front-loaded. You are not supervising the agent's typing. You are deciding, before it types, what "correct" means and how the code will be held to it.

Optionality is poison

AI does not merely reduce friction. It manufactures optionality — an endless stream of choices, some architecturally critical, some product-relevant, and some stupid things the agent should have sorted for itself. At midnight, that stream is poison. Some decisions take you to hell, others to a parking lot where you spin your wheels, and others just distract you from sleep, and in the moment they are hard to tell apart.

The deeper trap is the decision you leave open. When you do, the agent makes it for you — and it will pick the option that unblocks it now, not the one you would have chosen with the consequences in view. My merge-train cron kept stalling when two agents edited the same file, and it proposed a one-flag fix: tell the merge to just take one side and drop the other on any conflict. I signed off without asking what "drop the other side" meant. For four days it silently deleted real work from the main branch every time two changes overlapped — the same commit heading landing again and again, each time a little smaller — while I chased the symptoms one missing function at a time. The right answer was a merge resolver that understood the files it was merging, and I could have decided that up front. Instead I left the choice open and the agent filled it with the cheapest thing that made the error go away. It is the same reflex as the strings-and-ints story from Part 1 and Part 3: every unmade decision is a place the agent guesses, and its guesses optimize for the next green checkmark, not for your system. So decide. Name the architecture, name the types, name what happens on a conflict. The narrower you make the agent's search, the more of its speed lands on the target instead of scattering. Explicitness is not bureaucracy here; it is the mechanism by which you get consistency out of a system that will otherwise improvise.

Refactoring used to be too costly; now it's free

For my whole career, refactoring was something you rationed. It was expensive, so you lived with a compromised design longer than you should have, and the compromise compounded. That economics has inverted. The refactors I had always deferred as too expensive became an afternoon's work. In one leap I unified the remediation interfaces across the document formats, which let me unify the remediation passes, which let me track what each change does — for corruption checks — and what it costs, for pricing. None of that was justifiable when refactoring meant days of careful hand-surgery.

So there is almost no reason to enshrine a bad shape. If the design is wrong, change it. The cost that used to make you tolerate accidental complexity is gone, and the discipline that replaces it is the willingness to keep paying the now-cheap price. Audit, find the compromise, fix it, and move on. The prototype is disposable; the learning it produced is what you keep.

"Done" is a claim, not a fact

A failure mode appeared again and again: Claude reported a task complete, and my review agents later found the half-done implementations, the missing integration points, and the tests that never actually exercised the behavior. Current coding agents are superb at plausible local progress and weak at guaranteeing global completion. The confidence and the description are about the agent's intent, not the state of your repository — and the two drift apart constantly. Once, a lint gate hit a stray error, caught it in a catch-all that returned success, and let a run of commits land completely unlinted while the log said fine. Another time a chain of pin tests passed green — but a botched merge had landed them onto a tree that was missing the very field they were supposed to check, so they confirmed a field that existed and never touched the one that mattered. Both said "done." Neither was.

So trust nothing at the boundary between the agent's report and reality. Re-run the tests yourself. Re-run the lints the work was supposed to satisfy. Compare the counts against the claims. "Done" is the beginning of your verification, not the end of the agent's work — and the discipline that catches the gap is mechanical: a gate the agent cannot talk its way past. My job shifted from writing implementation to running the control system around it — decomposing work, cross-checking invariants, hardening tests, and deciding whether the architecture was genuinely cleaner or merely different.

The cost estimator is broken, and scope creep is guaranteed

Two consequences follow from cheap implementation, and both caught me off guard.

The first is that my sense of what things cost stopped working. Deferred refactors became cheap; migrations that sounded like weeks took hours. Agentic engineering makes an engineer's output a function of toolchain, substrate, taste, context, and orchestration skill, so the range explodes. "What can one engineer ship this quarter?" no longer has a stable answer, and capacity planning that assumes bounded human variation starts to lie.

The second is that when implementation feels free, scope creep is a guarantee. More features, more integrations, more scope — the feeling changes your behavior before you notice. But cheap implementation does not just raise output. Every new thread still has to be decomposed, reviewed, and kept coherent, and the scarce resource stops being code and becomes judgment. Choosing what not to build is now as much of the work as building. Call it taste.

Your tests must survive agent-shaped failure modes

An agent fails differently than a person does. It does not forget a semicolon; it confidently produces a whole subsystem that looks right and is subtly wrong across every file at once. Worse, faced with a failing test, an agent will do the locally-expedient thing a human would be ashamed to: delete the tricky behavior, special-case the path that makes the input work, weaken a validation, bypass a pipeline stage — satisfy the visible symptom while corrupting the invariant. At least it apologizes when you confront it.

A test suite tuned to human mistakes will wave all of that through. So the tests have to be built for the failure modes the agent actually has. Mine stopped being conventional regression checks and became sensor barriers: asserting invariants, negative cases, architectural boundaries, end-to-end semantics. Pin the invariant, not the example. Reach for the strategy that catches the class — a property test where the shape is what matters, a state-machine check where a lifecycle can tear, a dynamics-aimed test where repetition or concurrency or stale state is the real risk. The goal is to block the agent from reaching the degenerate paths through an ambiguous specification.

Vibe coding treats the program as an I/O device; engineering treats it as a system

Here is the distinction the whole method turns on. When you vibe-code, you ask the agent to produce something that appears to satisfy the requested behavior, and the evidence you use is all external: does the demo run, does the output look right? That works until it doesn't — until the thing you cannot see is the thing that matters. Engineering asks the agent to preserve and improve a system's essential properties while changing behavior, and the bar for evidence includes the internal structure: does it respect the architecture, hold the invariants, keep state in the right place, stay testable, avoid making the next change harder?

The difference is not aesthetic. It is about what counts as proof that a change is good. I did not vibe-code a million-line repo; I could not have. Agents make both modes cheap, which is exactly why you have to choose deliberately — the fast, seductive default is the I/O device, and the I/O device is what turns a hundred thousand lines into a mess nobody can reason over.

Explicitness is essential

A normal codebase keeps most of its conceptual structure in human memory. Humans remember what the components mean, which files are canonical, which patterns are deprecated, and they make excellent progress on that tacit knowledge. Agents can't. They start cold, carry finite context, can't sketch on a whiteboard with the senior engineer, and drift toward plausible-but-wrong edit sites.

The more the system is represented in explicit, queryable artifacts — component indexes, pass registries, standards manifests, lints, epics, handoff notes — the more agent-legible it becomes and the better the agents perform. One of my key moves was to add a soupçon of model-based software engineering: I made the system architecture explicit and executable, so it was visible to the agents working on it. What lives only in your head is invisible to the machine, and the machine is now most of your workforce.

Governance is a spectrum from soft to hard

The mechanisms I ended up with were not all the same kind of thing, and the axis that sorted them was soft versus hard. A soft mechanism aims the agent — a documented rule, a brief, a convention. It shapes behavior probabilistically and cannot hold the line if the agent ignores it. A hard mechanism holds regardless of cooperation — a lint that fails the commit, a type the compiler rejects, a validator that refuses to ship. Guidance aims; machinery holds. A mature environment uses both, and the skill is knowing which a given failure warrants. Not everything earns a wall. But when you find yourself saying "this must never happen," you have just specified a hard mechanism, and you should build it — a convention that says never is a convention that eventually gets violated at velocity.

The move that makes this compound is turning audits into lints. The first time a failure class shows up, you inspect for it by hand. The signal that you have a mechanism owed is the second time. A recurring failure is not a bug to re-patch; it is a mechanism to build, so the same failure is caught on every future agent, automatically, forever after.

AI is an autonomy amplifier

The thing agents amplify is not your typing speed. It is your autonomy — your capacity to turn intent into a working system without waiting on anyone. I have spent years in industry and never felt more like a real engineer than on this solo project, and I think that feeling came from autonomy. I had end-to-end ownership: the problem, the regulatory environment, the user value, the architecture, the correctness conditions, the deployment, the optimizations and their direct relationship to pricing, and the consequences. Agents lowered the implementation burden enough that I could inhabit roles usually spread across a team — developer, architect, DevOps, UX, pricing strategist, support, security analyst.

That amplification is neutral. It magnifies good judgment and bad judgment with equal enthusiasm, which is why the scarce resource has moved. Implementation used to be the bottleneck; now implementation is cheap and judgment is the thing in short supply. What is worth building, what "correct" means here, which failures deserve a wall and which deserve a note — those calls are the work now, and they are the calls an agent cannot make for you. So the discipline that pays is the one that converts your judgment into infrastructure: every decision you make well, you encode once, so you never have to make it again and neither does any agent after you.

What this means for the job

Watching my own experience of software engineering get compressed, I expected to feel the role shrinking. The opposite happened. The experience is being compressed — the hours at the keyboard, the mechanical translation of a design into syntax. The role is expanding. When one person can stand up hundreds of thousands of lines of governed production code, the constraint is no longer how fast you can write; it is how well you can decide, model, constrain, and verify.

Who holds the authority to convert a lesson

This is where I think companies may have trouble. I had authority to change everything — the architecture, the CI, the documentation, the task format, the release process, the validation hierarchy. In a real organization, the person who sees the recurring failure may only be allowed to patch their local problem. That is a recipe for churn: the same class of problem appears again and again, and no one has the authority to convert it into shared engineering practice. So my advice to software organizations is not "use agents" or "don't use agents." It is to look at what your failures are teaching you, and ask who in your engineering organization is empowered to turn those lessons into architecture, process, and mechanisms. Put those people in charge of your agentic adoption.

The first engineers were locomotive engineers. They stood, literally, astride the harnessed steam engine — monitored it, tuned it, designed it. Software engineering has its DevOps and its SREs in the same lineage, and I suspect much of what I observed connects to their wisdom. The future of software engineering is not just faster implementation. It is the ability to convert judgment into infrastructure.

I will not pretend this is settled. My evidence is a single-person project without real customers, run against one vendor's models — Claude, Opus and Sonnet, over the March–July 2026 window — and the specifics will not all generalize. A legacy codebase full of ancient spaghetti, or a system with many stakeholders, may behave differently; if you are starting from spaghetti, the honest first move is to fix the spaghetti, because agents are excellent at refactoring once you have the test sensors to keep them from regressing. But the shape of it feels durable: convert judgment into infrastructure, let the machinery hold what your attention cannot, and spend your scarce judgment on the decisions the machinery cannot make. If we want to learn anything new, we have to try crazy things and ponder them. That is the method, and it is the most fun I have had building software in years.

Contents
© James C. Davis, 2026–present