Alex Fedoseev

Why I Still Read Code

2026, 11 Aug

I do not read every line of code agents produce. I still read the parts that matter.

I know my codebase, so I know which areas deserve attention. Often, reading the types is enough to uncover the most serious problems because the types reveal the model the implementation is built around. I also inspect every change to shared abstractions. Agents might weaken or bypass them to satisfy the constraints of the current task.

To be frank, I encounter far fewer ordinary bugs with Sol, the model I currently use, than I did with earlier models. Those that remain tend to be less critical, yet harder to catch. But bugs are the obvious reason to review generated code; it’s not an interesting topic to ramble about. This post is about two systematic problems that can leave a codebase worse even when the feature works and every test passes: uncontrolled complexity and loss of design coherence.

My workflow

Before going into details, I want to rule out two obvious explanations: that I micromanage agents while they work, or that I do nothing to prevent this behavior. Neither is true.

My workflow starts with a plan, followed by an adversarial plan-review cycle. Once the review agents approve the plan, I review it myself to make sure the agent and I are on the same page. Only then does implementation begin. After implementation, adversarial reviews repeat until the agents consider the result ready to ship. I review the code only after that entire process.

Whenever I catch an issue, I do two things. First, I audit my own setup: I ask an agent whether anything in my configuration caused or encouraged the result. Second, I add or refine a prevention rule (or skill) to avoid the same failure in future work. In both cases discussed below, relevant rules were already active, so the audit also had to explain why they had failed. The problem was not simply that nobody had told the agents what to do.

Every agent in this workflow uses GPT-5.6-Sol exclusively, with the thinking level set to High. The examples below therefore do not come from other models or reasoning settings.

Complexity Control

Keeping complexity under control is essential to maintaining a healthy codebase. Every abstraction, branch, and piece of state adds something developers and agents must understand. Sometimes that cost is necessary, and a good abstraction can reduce complexity elsewhere. Often, however, the added complexity is not justified.

This is the first reason I still read the code agents produce: agents have a strong tendency to overengineer. They can generate a sophisticated solution in seconds, but they do not reliably judge whether the problem deserves one. Without careful review, unnecessary complexity accumulates quickly.

For example, agents do not reliably calibrate a solution to the wider product context. They may build an enterprise-grade authentication layer for a small application that has no users. The design can be reasonable in isolation while being completely unjustified for the product being built.

I often see the same problem around asynchronous code. An agent sees two async operations and immediately starts looking for races. Finding races is useful. Treating every theoretically possible ordering as something the code must handle is not.

A race does not justify a fix merely because it can exist. We also need to consider how it could happen, how likely it is in normal use, what damage it would cause, and how much complexity the fix would add. Agents are not great at this tradeoff. They will happily introduce generation counters, queues, cancellation logic, stale-result checks, and extra state to cover an execution order that is technically possible but practically irrelevant.

The added complexity, meanwhile, is not theoretical. Someone has to understand it, test it, maintain it, and account for it during every later change. It also increases the amount of code and behavior an agent must fit into its context window to understand the problem space. The more states and interactions we add, the harder it becomes for an agent to see all the relevant relationships before making a change.

Example

I ran into a good example while working on an Obsidian plugin. A user can open the plugin settings and, from there, open a dedicated modal to update a secret. Once the user confirms the update, Obsidian saves the selected secret locally.

The agent tried to prevent an older save from finishing after a newer one and overwriting its value. For that to happen, all of the following would need to occur:

  1. The user opens the plugin settings, updates a secret through the dedicated modal, and confirms the update.
  2. The local file write starts but stalls without failing or completing.
  3. While that write is still pending, the user closes the plugin settings.
  4. The user opens the settings again, opens the secret modal, and completes another update.
  5. The second write completes while the first one remains pending.
  6. The original write finally resumes and overwrites the newer value.

Strictly speaking, this ordering is possible. In normal use, its likelihood is… zero? A local file write would need to remain stuck for tens of seconds or minutes—long enough for the user to close the settings, reopen them, and complete the entire flow again—then recover at exactly the wrong time.

This is where the agent's reasoning went wrong. It found a valid execution order, but it did not evaluate the conditions required to produce it. Protecting against this case would make every normal save harder to understand in exchange for handling a scenario we are extraordinarily unlikely to encounter. That is a bad tradeoff.

Self-Audit

The response from the agent:

  • The global complexity rule existed and applied to review findings.
  • The review subprocess received global rules.
  • final-review-002 raised a speculative race without proving realistic reachability, harm, or proportional cost. The reviewer violated the rule.
  • The main agent then gave that finding too much authority. That adjudication was also wrong.
  • final-review-003 correctly applied the rule and rejected the unsupported complexity.

Recommended prevention

Keep the existing rule and priorities. Harden the gig reviewer contract:

  • Every race/edge/failure finding must include its causal proof and risk/cost assessment.
  • A finding missing those elements is invalid and cannot block signoff.
  • The main agent must reject such malformed findings rather than implement them.

So this was primarily reviewer and main-agent instruction noncompliance, with a review format that does not mechanically enforce the existing rule.

Prevention

I had already tried to prevent such a behavior. Every agent received a dedicated rule requiring implementation complexity to be justified by credible cases.

To be honest, I no longer have the exact revision that was active when this example happened. I changed it later without ever committing the earlier version. The current rule below is more detailed, but its central requirement is the same: a theoretically possible failure does not justify complexity unless the agent can establish a credible path to it.

This rule may shift errors from false positives to false negatives: a real but poorly evidenced issue can be dismissed. I haven’t tested the new revision much yet, but I accept that tradeoff for ordinary behavior because unnecessary defenses add permanent complexity. Security and data-loss risks still require investigation.

Design Coherence

The second reason I still read agent-generated code is that agents tend to patch new behavior onto a codebase instead of integrating it into the existing design.

By patching, I mean adding a special case beside an existing abstraction rather than changing the abstraction to represent the new requirement. The result can be completely correct in isolation: it compiles, the tests pass, and the feature works. At the same time, the system as a whole becomes less coherent.

This approach is locally attractive. It keeps the diff small and avoids changing code that already works. But it also preserves an abstraction that no longer describes the domain and creates another path that every future change must account for.

The harder—and often better—option is to recognize that the new requirement has exposed a limitation in the existing design. Fixing that limitation may require touching more code, but it can leave the system with fewer concepts and one shared path. Agents do not make this judgment reliably. They often fit the requirement around the current structure, even when the structure itself is what needs to change.

Example

I ran into this while adding a CLI build watcher to a test harness. The harness already knew how to start and stop several long-lived processes—an API server, web servers, and other supporting services. It stored them in one collection and handled their lifecycle through the same code.

The watcher differed from those processes in one obvious way: it was not a server. It had no TCP port and needed no readiness check.

The agent handled that difference by giving the watcher a separate type and a separate place in the test environment:

rust
struct ManagedProcess {
    server: Server,
    process: RunningProcess,
}

struct Cli {
    process: RunningProcess,
}

struct TestEnvironment {
    processes: Vec<ManagedProcess>,
    cli: Option<Cli>,
}

It then added CLI-specific branches everywhere the process lifecycle was handled: normal shutdown, cleanup after startup or readiness failures, logging, and error reporting.

The code compiled. The tests passed. The CLI watcher started and stopped correctly. The feature worked, but the design got worse.

The agent missed the more important similarity: the servers and the CLI watcher were all child processes owned by the test environment. All of them needed to be started, logged, stopped during cleanup, and reported if shutdown failed.

The new requirement had exposed that the existing abstraction combined two separate concerns. The better design was to separate the common process lifecycle from the server-specific behavior:

rust
enum Service {
    Server {
        server: Server,
        mode: SpawnMode,
    },
    Cli,
}

struct ManagedProcess {
    service: Service,
    process: RunningProcess,
}

struct TestEnvironment {
    processes: Vec<ManagedProcess>,
}

Now every owned process follows the same lifecycle. Code that needs a server-specific capability, such as a TCP readiness check, handles only the server variant. There is no fake port, optional port, or no-op readiness check.

The agent's patch was smaller, but it created a second lifecycle that every future change would need to keep in sync. The next non-server process would make another special field look reasonable, followed by another set of branches. This is how a coherent design gradually turns into a collection of exceptions—even when every individual feature works.

Self-Audit

The response from the agent:

Short answer: mostly a general agent failure mode, not caused by your configuration.

Your configuration

Your global instructions explicitly oppose the behavior:

“Implement the complete, maintainable solution; never trade correctness or coherence for a smaller diff.”

And abstraction-ownership.md describes this exact case:

“local branches, optional fields… compensate for a limitation in a shared type”

It then requires fixing the owning abstraction.

So this incident was primarily instruction noncompliance, not missing guidance.

There is mild competing pressure from:

  • “Keep diffs focused”
  • “Avoid unrelated cleanup”
  • “Ask for clarification before broad changes”
  • tooling optimized for targeted replacements

An agent can incorrectly interpret “focused” as “local.” But your wording clearly says maintainability takes precedence, and the architectural refactor was directly related—not cleanup.

Your project-local AGENTS.md did not contribute materially.

General agent tendency

This happens without custom configuration because agents naturally favor short-horizon solutions:

  1. A local patch requires less architectural understanding.
  2. It changes fewer call sites.
  3. It is easier to generate and validate.
  4. Compiler and tests confirm behavior, not architectural coherence.
  5. Review agents can become anchored to the proposed implementation and review its correctness rather than question its shape.

AI coding agents often converge on localized solutions because they are easier to generate, validate, and justify within a single task—even when integrating the concept into the owning abstraction would produce a better system.

The audit found some competing pressure toward focused changes, but that’s irrelevant. The architectural change was directly related to the task, and my instructions explicitly gave maintainability and coherence priority over diff size.

This was not a missing-rule problem. The agents failed to follow instructions, while the compiler and tests verified only that the feature worked—not that it fit the design.

Prevention

I had a rule for this as well. Unlike the rule from the previous example, I know that this exact version was active when the patch was produced:

The rule describes this case almost word for word. A new domain concept did not fit the existing model, so the agent added an optional field and repeated local branches instead of reshaping the model. The smaller diff should not have won. Still, the implementation agent produced it and the review process accepted it.

Anything else?

Complexity control and design coherence are the two code-level reasons I still review agent-generated changes. Both problems can survive planning, implementation, tests, and multiple rounds of adversarial review because the resulting feature still works.

There is one more area I review just as carefully: user experience. It is not strictly a code-quality issue, so I did not give it a separate section here. Still, agents can implement every requested interaction correctly and produce a workflow that is confusing, awkward, or inconsistent with the rest of the product. Tests can verify that a button works. They cannot tell me whether the user will understand why it is there or what to do next.


Models are improving, and I expect the amount of code I need to read to keep shrinking. But green tests do not prove that the implementation is well designed or maintainable. Until agents can reliably control complexity, preserve design coherence, and evaluate UX in the context of the actual product, I still want the final review.