Engineering notes
How I stopped my AI coding agents from turning my codebase into soup
A working process for keeping a large, multi-month agent-built project honest with itself.
TL;DR: I made documentation a hard requirement for a large agent-built codebase. Every code directory gets a README, stale documentation blocks completion, and standing audits keep the project understandable long after the agent's context disappears.
I have been building a real product with Claude Code, Codex, and whatever agent is having a good day for months now. Not toy projects. A full SvelteKit + Cloudflare platform called VMPire, with D1, KV, Queues, Cron triggers, the whole stack. Dozens of features. Hundreds of files. Team management, a competition registration system, a homegrown chatbot, the works.
And here is the thing nobody warns you about when they show you a slick fifteen-second demo of an agent "building an app." The demo always stops right before the part where the codebase gets big enough that even the AI forgets what it did last Tuesday.
So I did something boring but incredibly effective. I made documentation a hard requirement, not a nice-to-have. And it changed everything about how fast I can move, how confidently I can hand off work, and how little I dread opening a folder I have not touched in three weeks.
The problem nobody puts in the demo video
An AI agent is extremely good at writing code that works right now. It is much less naturally good at writing code that someone, including a future version of itself in a brand new context window with zero memory of today, can safely touch six weeks from now.
Left to its own devices, an agent will happily:
- Build the same feature twice under two different names because it forgot the first one existed.
- Leave a component completely orphaned after replacing it because nothing told it to check.
- Reference a theme, table, or component that was deleted three sessions ago.
- Introduce a subtle race condition because nobody wrote down that the write path is not atomic.
- Quietly skip a test that would have caught the exact bug it just shipped.
None of that is a hallucination in the scary sense. It is what happens when the only memory a coding agent has is the file tree in front of it, and the file tree does not explain itself. Code tells you what it does. It almost never tells you why, or what happens if you poke it the wrong way.
My fix: a README is not optional, it is a failing test
Here is the actual rule, straight from my project's CLAUDE.md:
Every directory that contains code must have its own README.md. A stale README is treated the same as a failing test. It blocks the phase from being marked done.
That one sentence is doing more work for me than any linter I have ever installed.
Every README follows the same shape, every single time:
- What this doesPlain language, like explaining it to a teammate over coffee, not a specification dump.
- How it fits into VMPireWhat depends on this, what this depends on, and which architecture document section it maps to.
- Files in this directoryA one-line table. No mystery files.
- How to change or extend thisThe actual ways someone will try to modify it, including which way is wrong.
- GotchasTiming dependencies, Cloudflare-specific behavior, and everything that is not obvious from reading the code.
That last section, Gotchas, is the one that quietly saves me the most time. It is where the agent writes down the thing it just learned the hard way, so the next agent, or the next me, does not have to relearn it the hard way too.
The rule is not "write docs at the end when you remember." It is "the README gets created the same moment the first file in that folder does, and updated the moment anything in that folder changes meaningfully." Documentation debt never gets to accumulate because it is never allowed to exist in the first place.
A real example, not a hypothetical one
I recently had Claude build out a team management system: create a team, invite members, accept or decline, remove someone, leave a team, and register a whole team for a competition instead of just one person. Real backend, real schema, real UI. The kind of feature that touches five different parts of an app at once.
While it was auditing its own earlier work for a completely different feature, a theme system redesign that trimmed a four-option theme picker down to three, it looked for stale references and found two READMEs still describing a "Rainbow theme" that had been deleted sessions earlier. Nobody asked it to hunt for that specifically. It found it because the discipline of keeping directory docs honest had turned into a reflex, not a one-time chore. That is the point. The system does not rely on me remembering to ask.
Later, during a security pass, it noticed that a registration function was doing a "check if already registered, then insert" without any database-level guarantee against two requests racing each other. That is a genuinely nasty bug category. It only shows up under real concurrent traffic, such as a double click or two open tabs, and is a nightmare to reproduce after the fact. It was caught because the audit discipline treats duplicate point awards as a standing checklist item on every pass.
And then there was the chatbot. I wanted something that could answer real questions about a competition without paying for an external AI API on every message. The build uses intent detection, keyword matching, and a small TF-IDF similarity engine running locally against real competition data, with a hard rule that it must never call an external LLM.
The audit did not just eyeball the code and say it looked fine. It wrote an automated test that scans every file in that module for the literal string fetch or any known AI SDK import. If a future change tries to introduce a paid API call, a test fails immediately instead of someone noticing the bill three weeks later.
That is the actual value here. Not "the AI wrote code fast." Any agent can write code fast. The value is a system that keeps checking its own work and explaining itself to itself, so quality does not quietly decay as the codebase grows.
Why this makes manual fixes easier
Here is the part that matters for me as a human who still needs to open a text editor sometimes.
When I want to fix something myself, I do not need to ask an AI where the thing that handles X lives. I open the folder, read a five-minute README, and I already know:
- What the code is for.
- What it depends on and what depends on it.
- The two ways people usually break it.
- The Cloudflare-specific trap that is not obvious from the code.
- Whether my change is load bearing for three other features or safely isolated.
I am not reverse engineering someone else's, or some agent's, intentions. The intentions are written down in the same commit as the code that embodies them. It is the closest thing I have found to a codebase that ages well instead of aging like unrefrigerated milk.
It also means I can drop an agent into the middle of a large existing project without babysitting it through a tour of the codebase. It reads the README in the folder it is about to touch, the same way I would, and gets useful in minutes instead of hallucinating a plausible-sounding guess about how something works.
Tests are not a phase, they are a reflex
Here is a habit that took me a while to trust, and now I cannot imagine working without it: every time functionality changes, both a unit test and an integration test get written right then. Not at the end of the week. Not "before we ship."
The split is simple, and I took it straight from my own project rules:
- Unit tests live in
/tests/unit/and cover pure functions with no database, network, or external dependency. Validators, similarity scoring, form parsers, and similar logic belong here. - Integration tests live in
/tests/integration/and hit a real local database or queue spun up for the test run. "The function works in isolation" and "the function works against real data with real constraints" are two different claims.
A few real examples make the distinction clearer.
When I had the agent build the local chatbot's similarity matching, it did not just ship the matching logic. It wrote unit tests for the tokenizer, scoring function, and intent detector. It also wrote a separate test that scans the entire module for external network calls, so nobody can accidentally introduce a paid API later. Four test files, one small feature, written in the same pass as the code.
When it found the registration race condition, fixing the code was the easy part. Proving the fix works required a test using Promise.all to fire two registration attempts at the same time, then assert that only one wins and only one point award is created. A sequential test would never reproduce that bug. You have to shape the test like the bug.
When a feature touches five systems at once, such as registering a team for a competition, each piece passing its own tests is not enough. One more test walks the full path end to end: create the competition, publish it, form a team, register it, verify the points, verify the leaderboard, archive the competition, and confirm the data survives. That test exists to catch bugs at the seams between already-tested pieces.
The numbers make the habit obvious in hindsight. In one working session, the suite grew from a few hundred tests to well over 350 because every new piece of functionality brought its own tests with it. Nothing gets marked done with "looks right to me." It gets marked done with passing tests and a typecheck with zero errors.
That is the difference between an agent that writes code and an agent that ships working software. The code was never the hard part. Proving it keeps working, especially when two features quietly step on each other, is the actual job.
The paper trail: one report per chunk of work, one index for all of them
Tests prove the code works today. They do not tell a human six weeks from now why something was built, what was checked, and what was deliberately left for later. That is what the report layer is for.
Every finished chunk of work gets a markdown file in /tests/reports/, following the same shape every time:
- A short summary of what changed.
- A table of every unit and integration test that ran, with its result and a note for any failure.
- A known issues and follow-ups section for work deliberately deferred, not silently dropped.
- A plain yes-or-no sign-off on whether it is safe to build the next thing on top.
No exceptions. An empty or skipped report counts as an unfinished phase, just like a failing test.
Each report gets one line in a running SUMMARY.md. Scroll through it and the build history is visible on one screen:
| Work item | Status | Date |
|---|---|---|
| Team + Participation-Type Schema (Foundation) | PASS | 2026-09-02 |
| Team Management System (Service Layer) | PASS | 2026-09-02 |
| Internal Registration Rebuild (Step 1) | PASS | 2026-09-02 |
| Admin Competition Fields + Participation-Type Display | PASS | 2026-09-02 |
| Kepler: Local NLP Competition Assistant (Steps 4-11) | PASS | 2026-09-02 |
| Security / Data-Integrity Audit (Step 19) | PASS | 2026-09-02 |
This is not a changelog written for marketing. It is the literal order in which things were built, with each line pointing to a report and the test table behind it.
Git log tells you what changed in diff form. It does not tell you why a race-condition fix happened, what was tested before the feature was called done, or which known gap was left on purpose. The report turns that reconstruction job into a five-minute read.
It also closes a loop that documentation alone cannot. A README tells you how code is supposed to work. A report tells you that, on a specific date, someone ran the tests and confirmed it did. I want both kinds of trust.
Explains the code permanently and captures the why.
Proves a specific claim now and every time the suite runs.
Records what changed, what passed, and what remains.
Feature built, tests written, outcome documented. All three happen in the same sitting. None is optional, and none is left for later, because later is exactly when it stops happening.
Why this scales
The real test of any workflow is not whether it works on a small project. It is whether it survives twenty more features on top of the ten already shipped. A few more habits make this hold up beyond the docs, tests, and reports.
Phases are explicitly ordered, and the agent is told to stop and ask rather than guess. When something is ambiguous against the architecture docs, it does not invent scope on the fly. That single instruction has prevented more scope creep and silent wrong turns than any code review I have done.
Audits are a standing habit, not a one-time event. Functionality, full user lifecycle, UI and UX polish, security and data integrity. The same buckets are revisited every time a major piece of work lands. "It worked when I tested it in isolation" and "it still works once three other features touch the same table" are very different claims. Only one matters in production.
Nothing gets merged on vibes. Work happens on a development branch, gets its own test report, and only lands on main once I have actually looked at it. The agent does the work. I still hold the pen on what ships.
The unglamorous truth
None of this is a clever prompt trick. There is no magic incantation that makes an AI agent "just get" your codebase. What works is closer to being a strict manager: force the documentation to exist when the code is written, force a test to exist when behavior changes, force a report to exist when a chunk of work is called done, and never let "it works" substitute for "it is tested and written down why."
It is not exciting to write about. Nobody is making a viral demo out of "and then it updated a README and wrote a report." But it is the difference between an AI-built codebase that turns into something you are quietly afraid to touch, and one where I can hand a fresh agent, or my own tired brain at 11pm, a single folder and trust that its documentation, tests, and report will tell us the truth about itself.
If you want to steal this
You do not need my exact project or stack for this to work. The parts that transfer work best as one system, not three separate tips:
- Treat a missing or outdated README as a failing test, not a suggestion. It explains the why, which code alone never does.
- Write the unit test and integration test in the same sitting as the behavior change. Shape the test after the bug, especially for concurrency or money-like operations that must never fire twice.
- Close every chunk of work with a written report: what changed, what was tested, what was deliberately deferred, and whether it is safe to build on. Append it to a running summary index.
- Build standing audit passes for security, functionality, real user flows, and UI polish instead of hoping problems announce themselves.
- Never let the agent merge its own homework. Someone still has to read the report and inspect the diff.
Turns out the best AI coding workflow I have found is mostly good documentation discipline, a reflexive testing habit, and an honest paper trail. All of it is enforced by a very literal-minded collaborator who never gets bored of writing READMEs and never merges anything without me looking first.