In July 2026, Robert C. Martin wrote that he no longer reads the code produced by his coding agents.
The statement drew plenty of attention. The rest of his post matters more for understanding how he works. Martin surrounds the agents with unit tests, Gherkin acceptance tests, QA procedures, mutation testing, coverage checks, and quality metrics. Code earns his confidence by passing through that system.
He had made a similar point a few months earlier. Instead of inspecting the implementation, he looks at test coverage, dependency structure, cyclomatic complexity, module size, and mutation-testing results. When people interpreted this as giving up review altogether, he clarified: "I review plenty — just not the code."
His comments describe a practical problem in AI-heavy software development. Coding agents can produce changes faster than people can read them. Once the volume of AI-generated code exceeds the team's review capacity, line-by-line inspection cannot remain the only source of confidence.
We have been dealing with the same problem at vm0 for the past eight months. Most of the implementation in our repository is written by agents — vibe coding, in the popular shorthand — while six engineers remain accountable for what that code does in production.
The short version
vm0 is a vibe-coded codebase: agents write most of the implementation, and six engineers own what it does in production. Eight months in, the repository holds 1,329,170 lines and merged 630 pull requests in a single week. Five mechanisms carry the quality burden that line-by-line review can no longer carry alone.
- Converged environments. Engineers and agents work in the same dev container, and every pull request can get its own database branch. A command that worked for an agent works for a person.
- Executable constraints. Strict TypeScript, roughly 135 typed API contract modules, Oxlint, ESLint, and architectural rules, with CI accepting no warnings. A rule that can be checked mechanically is not left to review.
- Boundary-focused tests. A testing trophy rather than a test pyramid: integration tests through public module boundaries, real PostgreSQL and real migrations, mocks only at external systems. Test code is about 36 percent of the repository.
- Complete feedback loops. Agents start the application and database, run migrations, and drive a real browser, then report the commands they ran, the path they tested, and screenshots. The median pull request merges in about 53 minutes.
- Continuous cleanup. Knip removes dead code deterministically, daily workflows clear the AI slop no linter can name, and a pattern that recurs often enough is promoted into a lint rule or the type system.
What vibe coding and agentic coding actually mean
Vibe coding is prompting an LLM for code, running what it produces, asking for changes, and paying no attention to the generated code itself. Martin Fowler's definition is deliberately narrow, and he highlights the phrase "forget that the code even exists." It suits prototypes, disposable software, and small tools with limited consequences.
Agentic coding, which Fowler calls agentic programming, is what teams do when they expect to maintain the result. An agent reads the repository, edits files, runs tests, and works independently for an extended period, while people keep ownership of structure and behavior and review the evidence a run produces: test results, quality signals, previews, and production behavior.
The difference is not how much of the code a model writes. It is where human attention goes.
| Vibe coding | Agentic coding | |
|---|---|---|
| Agent autonomy | Prompt, run, prompt again | Reads the repository, edits, tests, iterates |
| Who reads the code | Nobody | People read what carries risk |
| What is verified | Whether the output looks right | Types, contracts, tests, previews, production signals |
| Best fit | Prototypes and throwaway tools | Software with a maintenance horizon |
| Typical failure | Code nobody understands | Verification too weak for the volume |
Agents that edit a repository are one branch of a wider shift; we looked at the non-developer side of it separately.
We often call vm0 a vibe-coded project because that phrase has become common shorthand for software written primarily by AI. In Fowler's terms it is closer to agentic coding. Our engineers type less of the implementation than they used to, while continuing to own the architecture, maintenance cost, and production behavior.
As code generation became faster, more of that responsibility moved into the development environment, type system, test suite, and automated maintenance workflows.
Eight months of growth in a vibe-coded codebase
The vm0 repository was created in November 2025. At the time of our latest weekly engineering scale report, it was roughly eight and a half months old.
The repository contained:
| Metric | Count |
|---|---|
| Non-empty logical lines | 1,329,170 |
| Production lines | 850,913 |
| Test lines | 478,257 |
| Source files | 5,549 |
| Test files | 1,327 |
| Packages | 44 |
| Commits on the main branch | 14,005 |
Those counts come from our weekly engineering scale report for the week of July 20–26, 2026, measured directly from the monorepo. Test code made up about 36 percent of the measured codebase. That is a share of code volume, not a test-coverage figure, but it gives some sense of how much implementation has accumulated around verification.
Across three recent full weeks, six engineers made 541, 640, and 631 commits respectively. During the week of July 20–26, GitHub recorded 630 merged pull requests. Six engineers authored 556 of them, while release automation authored the remaining 74.
The median time from opening a pull request to merging it was about 53 minutes. The 90th percentile was approximately 7.4 hours.
At this rate of change, human review remains useful but cannot carry the entire quality system. We need independent signals at several points in the lifecycle of a change.
Our current approach has five recurring themes: converging environments, executable constraints, boundary-focused tests, complete feedback loops, and continuous cleanup.
Converging the development environment with dev containers
Many hard-to-reproduce failures come from state that never appears in a pull request.
A developer may have an undocumented environment variable, a globally installed tool, an old configuration file, or a database that has been running for months. People grow accustomed to these details. Agents usually cannot see them.
We gradually stopped treating the host machine as the standard development environment. Engineers and agents work inside a dev container. The development image includes the project toolchain, PostgreSQL, pgvector, Chromium, and browser-automation utilities.
CI runs versioned toolchain images built from the same multi-stage Dockerfile family. The development and CI images serve different purposes, and production has its own deployment shape. The useful property is convergence: tool versions, dependencies, and runtime assumptions are explicit and versioned.
This gives us a straightforward expectation. Commands that an agent runs should be reproducible by an engineer in the same development container. Checks that pass during development should run under a closely related toolchain in CI.
We apply a similar idea to data. Every pull-request preview can have its own database branch and run real migrations and seed steps. An agent can create data, change it, and repeat destructive tests without borrowing a developer's existing database or inheriting state from another pull request.
Environment changes travel through the repository. Tool upgrades, browser versions, and database extensions are reviewed and propagated in the same way as application code.
Making engineering standards executable in types and lint rules
Written standards help people understand design decisions. They do less to guarantee that every change follows those decisions, especially during long agent sessions.
We put stable rules into the type system, linters, and CI whenever the rule can be expressed reliably.
vm0 uses strict TypeScript settings, including strict and noUncheckedIndexedAccess. Some packages enable additional checks for unused values and implicit returns.
The API uses a schema-first typed REST contract layer built with tRPC's type machinery and Zod schemas. Drizzle connects database access to TypeScript types. At the time of this review, the repository contained roughly 135 contract modules.
These choices do not catch incorrect product decisions. They do surface interface drift early. When a field or response changes, related callers tend to fail during static analysis. The resulting error is usually specific enough for an agent to use in its next iteration.
Linting covers a second group of rules. The platform runs Oxlint, type-aware checks, and ESLint, along with project-specific architectural rules. CI accepts no warnings. Warnings that can remain indefinitely tend to become background noise, and background noise is easy for both people and agents to ignore.
When a problem repeats, we consider where the check belongs:
- Can the type system express it?
- Can a linter or structural tool detect it accurately?
- Does it require semantic judgment across the repository?
The first two give fast, deterministic feedback on every change. We handle the third group with recurring workflows described later in this article.
Narrowing error-prone patterns in AI-generated code
Some language features have legitimate uses and also appear frequently in fragile agent-generated code.
try/catch can blur an error boundary. When an agent encounters a failure, adding a catch block and fallback is an easy way to keep the current path running while losing the original error. Mixing Promise .then() and .catch() with async/await can spread control flow across several styles.
React's useEffect creates a similar problem in state management. It is often used to copy state, synchronize two sources of truth, or encode an ordering dependency that is difficult to see from the data model.
The core web platform restricts these patterns by default. A justified exception can remain, with an explicit explanation beside it.
The production code in the core web platform currently contains no useEffect. We use ccstate and other effectless patterns to model dependencies and side effects more explicitly. A small number of production useEffect calls still exist elsewhere in the monorepo, mainly in shared UI and desktop code, so the scope of the claim matters.
These rules grew out of recurring failures in this repository. When a pattern repeatedly creates the same maintenance problem, we move it from review guidance toward an executable constraint.
Testing at module boundaries: a testing trophy, not a pyramid
vm0 does not follow a conventional test pyramid. Our testing guide describes a testing trophy: static analysis at the base, integration tests as the dominant layer, and a small number of end-to-end tests for critical user journeys.
Unit tests are relatively uncommon. We use them selectively for security-sensitive logic, algorithms, and state machines. Most business behavior is tested through a module's public boundary.
API tests call the real application through its contracts. Test data is prepared through production endpoints where practical, and assertions are made through public behavior. Tests avoid reaching directly into an internal service or modifying database tables, because that couples them to the current implementation.
Internal infrastructure stays real wherever the cost is reasonable:
- PostgreSQL and pgvector
- database migrations
- the filesystem
- internal services
- mocks at external-system boundaries
These integration tests run more slowly than highly isolated unit tests and require a fuller environment. They also reduce the gap between "the test passed" and "the application can perform this operation with a real database."
Boundary tests leave room for large internal refactors. An agent can reorganize modules, split a service, or change the data-access layer while the public behavior remains protected.
Uncle Bob favors a different mix, with heavy use of unit tests, Gherkin, and mutation testing. The shared principle is independent verification. The process that generated the code should not be the only source claiming that the code works.
Giving coding agents a complete feedback loop
Our early coding-agent runs often ended with a familiar report: the code was changed and TypeScript passed; please start the application and check the page.
That leaves half of the development loop with the engineer. Someone still has to prepare a database, run the services, open a browser, create data, observe the failure, and describe it back to the agent.
Agents now have enough of the development environment to perform more of that work. They can start the application and database, run migrations, create test data, visit a pull-request preview, and carry out real browser interactions.
Browser verification catches a class of problems that type checks and API tests do not cover well: broken navigation, loading states that never settle, permission errors that appear only in a complete flow, and visual regressions.
At the end of a run, the agent reports the commands it executed, the path it tested, and what it observed. For user-interface changes, it can include screenshots. An engineer can inspect that evidence before deciding whether to open the preview personally.
A screenshot is not a test and does not prove the absence of other faults. It lowers the cost of reconstructing the agent's context. For a small interface change, reproducible steps and a final screenshot are much more useful than a message saying the change "should be fixed."
The quality of an agent's work depends heavily on the feedback it can obtain without waiting for a person.
Keeping branches short with trunk-based development
Fast code generation can produce a large inventory of branches.
Long-lived feature branches accumulate merge conflicts, duplicated work, and stale context. As pull requests grow, review becomes harder and slower. We use trunk-based development to keep branches short and integrate continuously around main.
Our main-branch rules require:
- pull requests for changes
- linear history and squash merges
- a merge queue
- Turbo, Rust, and security checks
- no routine bypass for required gates
Automation follows the same path. An agent may create a pull request, and some low-risk maintenance tasks may enable automatic merging, but the change still passes through CI and the merge queue.
The size and lifetime of pull requests help explain how 630 of them could merge in one week. A small change carries less context, is easier to verify, and is less likely to conflict with product work or another automated repair.
Knip as repository garbage collection: removing dead code
Code generation naturally adds files and abstractions. Deletion often needs a separate prompt.
After a refactor, old files can remain in the repository. Removing a feature may leave exports, dependencies, and entry points behind. These artifacts rarely break tests, yet they make the codebase harder to navigate over time.
We use Knip to find unused files, exports, dependencies, and entry points. TypeScript may confirm that the code is valid; Knip asks whether it still participates in the system.
The residue has an additional cost for coding agents. The repository is one of their most important sources of context. An obsolete helper or abandoned implementation can look like an approved pattern to the next agent that reads it. Keeping that context clean is the unglamorous half of context engineering: what an agent reads from the repository is as much an input as the prompt it was given.
Removing dead code also improves the inputs available to future runs. Knip handles the deterministic part of that work quickly enough to become a regular quality check.
Recurring AI slop cleanup workflows
Knip and ESLint have clear limits. Many forms of degradation require project context and semantic judgment.
We use "AI slop" as a practical label for this residue: unnecessary fallbacks, duplicated abstractions, tests that bypass a public boundary, or defensive branches for impossible states. Each instance may appear harmless. In aggregate, they make the repository harder to understand and give future agents poor examples to follow.
Several recurring vm0 workflows scan for these patterns on a schedule.
A daily AI-slop cleanup looks for new residue and selects a small set of high-confidence, low-risk fixes. Other workflows inspect API tests that reach into internal services, React and ccstate antipatterns, and technical debt that can be handled safely.
Each workflow keeps its changes narrow. It opens a pull request, then relies on the normal type checks, lint rules, tests, and merge queue. A pull request configured for automatic merging still has to pass the same gates.

Running agents on a schedule is not free, and we have written separately about keeping those costs down. The workflows do not attempt to clear every piece of technical debt in one pass. A small daily batch is easier to verify and less disruptive than a large cleanup every few months.
When a recurring workflow finds the same pattern often enough, we consider moving the check into ESLint, Knip, or the type system. The semantic workflow acts as a place to observe and refine the rule before turning it into a cheaper deterministic check.
Turning flaky test failures into automated repairs
Another group of workflows starts from GitHub Actions failures.
When a test fails on the main branch or in the merge queue, a workflow reads the logs, checks for evidence of flakiness, and looks at retries, timing, and environmental factors. If the evidence supports a specific repair, it updates the test or implementation, opens a pull request, and runs the full CI path again.
Passing on a retry does not make the original failure harmless. Teams that rely on the retry button gradually lose trust in red builds. Once that happens, failed checks become another form of background noise.
An automated repair workflow turns an intermittent failure into a traceable code change. The diagnosis, patch, and verification remain visible in the pull request. Engineers can inspect higher-risk changes, while narrow fixes with strong evidence can proceed through the merge queue.
Our quality system currently has three broad layers:
| Stage | Mechanisms | Typical concerns |
|---|---|---|
| While writing | TypeScript, contracts, Drizzle, ESLint | type errors, interface drift, known code patterns |
| Before merging | Knip, integration tests, real databases, previews, merge queue | dead code, module behavior, complete runtime results |
| After merging | scheduled and event-driven vm0 workflows | AI slop, semantic antipatterns, flaky tests, architectural drift |
The layers feed one another. Problems found by workflows can become static rules. Failures found in CI or production can become tests and new engineering guidance.
Where human attention goes in reviewing AI-generated code
vm0 engineers still read code, particularly for architectural changes, security-sensitive work, payments, and data migrations. We have not made "never read the code" a team rule.
What changed is the allocation of attention in code review. Code reading is one signal among contracts, test boundaries, preview behavior, screenshots, quality metrics, and workflow diagnostics.
Several decisions still require experienced judgment:
- whether the requirement is complete
- where module boundaries should sit
- which failures are recoverable
- how much business impact a fault could create
- whether a security model is appropriate
- what new failure pattern the current rules fail to cover
A similar shift happened on the design side, where design-as-code moved visual decisions into the same repository and the same review path. Engineers also maintain the environment around the agents. When a problem repeats, we decide whether to add a type constraint, lint rule, test, or recurring workflow. The development system has become an important engineering artifact in its own right.
Readable code still matters. The next reader may be an engineer or another agent. Tangled code consumes more context, expands the scope of future changes, and makes verification less reliable.
Security and the changes that do not speed up
Speed is not applied evenly. vm0 keeps a short list of changes that move at their own pace: security-sensitive work, payments, data migrations, and architectural decisions. An engineer reads those line by line, regardless of who or what wrote them.
The security risk of AI-generated code, in our experience, is less about exotic vulnerabilities than about plausible code that nobody owns. The controls that matter are ordinary ones, applied consistently:
- Unit tests, which we otherwise use sparingly, are used deliberately for security-sensitive logic, algorithms, and state machines.
- Tests mock only at external-system boundaries. Internal infrastructure stays real, so a change that breaks an internal contract fails in CI rather than in production.
- Agents work inside the dev container against a per-pull-request database branch, not a developer's machine and not a shared database.
- The merge queue and required checks have no routine bypass, including for a pull request an agent opened and marked for automatic merging.
try/catchis restricted by default, so a failure is less likely to be swallowed by a fallback an agent added to keep a path running.
Credential handling is a separate design problem with its own answer: we described the broker pattern that keeps tokens out of an agent's hands in its own article.
None of this makes generated code safe on its own. It narrows the set of changes where a human decision is the only control, and it makes that set explicit.
What these figures do not show
The weekly report demonstrates repository scale and delivery speed. It does not, by itself, demonstrate production reliability.
Evaluating the effect on runtime quality requires availability, production error rates, incident counts, change-failure rates, rollback frequency, and mean time to recovery. A green CI run describes one part of the delivery process.
We are continuing to assemble those outcome measures. Engineering practices explain how a system manages risk; production data shows how well that management works.
FAQ
What is vibe coding? Vibe coding is prompting an LLM for code, running what it returns, asking for changes, and not reading the generated code. Martin Fowler's definition is narrow on purpose: the developer is meant to "forget that the code even exists." It fits prototypes, disposable software, and tools with limited consequences.
What is agentic coding? Agentic coding is a longer-running form of AI-assisted development in which an agent reads a repository, edits files, runs tests, and iterates on its own for an extended period. People still own architecture and behavior, and they review the evidence a run produces rather than every line it wrote.
What is the difference between vibe coding and agentic coding? Attention, not authorship. In vibe coding the code is never inspected. In agentic coding the agent works independently while engineers check the evidence around it: test results, quality signals, previews, and production behavior. vm0 is usually described as vibe-coded; in Fowler's terms it is agentic coding.
What is AI slop? AI slop is the residue AI-generated code leaves behind: unnecessary fallbacks, duplicated abstractions, tests that bypass a public boundary, defensive branches for impossible states. Each instance looks harmless. In aggregate they make a repository harder to understand and give the next agent poor examples to follow.
How do you review AI-generated code at 630 pull requests a week? Not line by line. At vm0, human review goes where judgment is required: architecture, security-sensitive work, payments, data migrations. Everything else is carried by mechanisms: strict types and contracts, integration tests at module boundaries, real databases in per-pull-request previews, browser verification, Knip, and a merge queue that no change routinely bypasses.
What are the best practices for vibe coding at scale? Five hold up across eight months at vm0: converge the development environment so agents and people run the same commands; make standards executable in types, linters, and CI instead of documents; test at module boundaries against real infrastructure; give agents a complete feedback loop including a browser; and clean up continuously rather than in occasional large passes.
What are the security risks of AI-generated code?
The common risk is not an exotic vulnerability but plausible code that nobody owns. vm0 keeps security-sensitive work, payments, and data migrations on human review, uses unit tests deliberately for that logic, keeps internal infrastructure real in tests, restricts patterns like try/catch that swallow failures, and allows no routine bypass of required checks.
Does AI-generated code create technical debt? It creates a specific kind: code that still compiles and passes tests but no longer participates in the system, plus semantic residue no linter can name. Knip removes the deterministic part. Recurring workflows handle the rest in small daily batches, and a pattern that recurs often enough becomes a lint rule or a type constraint.
How much of the vm0 codebase is written by AI? Most of the implementation. Six engineers authored 556 of the 630 pull requests merged during the week of July 20–26, 2026, with release automation authoring the rest, and agents wrote the bulk of the code inside those pull requests. What the engineers own is the architecture, the constraints, and the production behavior.
What is a testing trophy, and why not a test pyramid? A testing trophy puts static analysis at the base, integration tests as the dominant layer, and a small number of end-to-end tests on top. vm0 uses it because tests written through a module's public boundary keep protecting behavior while an agent reorganizes the implementation underneath, which a large unit-test layer does not.
How do you find dead code in an AI-written repository? Code generation adds files and abstractions; deletion usually needs a separate prompt. vm0 runs Knip as a regular check to find unused files, exports, dependencies, and entry points. TypeScript confirms that code is valid; Knip asks whether it still participates in the system, which is the question that matters for dead code.
Is vibe-coded code safe to run in production? That depends on what verifies it rather than on who typed it. The signals we require have not changed: types and contracts, tests through public boundaries, real infrastructure, and a pipeline that has to be green. The figures in this article describe repository scale and delivery speed. Availability, error rates, and change-failure rates are the measures that answer the production question, and we are still assembling them.
Eight months in
Eight months is too early to declare a final method. Models, agent tools, and the repository continue to change, and our rules and workflows change with them.
One shift is already clear. As code generation became faster, the environment, constraints, tests, and feedback system took on more of the quality burden. Engineers spend less time typing implementation and more time defining behavior, designing boundaries, and improving verification.
The vm0 codebase will continue to grow. Knip removes deterministic residue. Static rules block failure patterns we already understand. Integration tests protect module behavior. Recurring workflows handle degradation that we cannot yet express mechanically.
Code still matters. The best practices in this article are not about writing less of it, but about deciding what has to be true before a change belongs on the main branch. We now use more machine-executable evidence to decide whether a change belongs on the main branch, and whether its implementation should remain in the repository.



