AI is like steroids for a good developer. But not everyone becomes Arnold.

It lets me move faster and try more things. But when I skim a diff and notice a misplaced responsibility, an unnecessary abstraction, or a test weakened just to pass, I’m drawing on years of writing and maintaining software.

In my previous post, I described how I divide work between coding agents. The part I want to spend more time on here is what happens when the code comes back.

A quick scan tells me where to look closer

After years of writing and maintaining code, certain patterns catch my attention quickly. A small feature touches a surprising number of files. A business rule shows up inside a UI component. An assertion disappears from a test that was failing.

None of those observations proves there is a bug. A change can legitimately need more files, and a test can legitimately be wrong. But they give me a place to start asking questions.

That is what I mean when I say I can smell an issue while skimming a diff. The instinct points me toward something worth investigating. I still need to read the surrounding code and verify the behavior.

  1. Skim the diffNotice a suspicious pattern.
  2. InvestigateRead the surrounding code and requirements.
  3. Guide the fixGive the agent a concrete correction.
  4. VerifyCheck the intended behavior before shipping.
Experience helps direct attention. Evidence determines whether the change is ready.

The examples below are illustrative, not incidents from a particular project.

Sometimes the abstraction is missing

Imagine an application that calculates a discounted price in three places: the product page, the cart, and the checkout summary.

An agent adds the calculation directly to each component. Each version looks simple. Each screen displays the expected result for the example it was given.

Then the rounding rule changes.

Now there are three implementations to find and update. If one is missed, the product page can show a different price from the checkout summary.

Here, extracting the calculation into a shared domain function earns its place. There is one business rule, and all three screens need to follow it. Tests can exercise that rule directly, including its rounding behavior. The server still needs to calculate the price it actually charges; sharing display logic does not make a client-supplied total trustworthy.

The useful question is whether these callers must change together. Similar-looking code alone is weaker evidence: two calculations can look identical today and represent different policies.

Experience helps me recognize which kind of duplication I am looking at before I ask the agent to extract anything.

Sometimes there is far too much architecture

Now consider a much smaller request: show a success message after a profile is saved.

The project already has a toast library. The agent introduces a notification provider, an event bus, a subscription hook, and a set of event types. The save handler publishes an event, a listener receives it, and eventually a toast appears.

I would first check whether anything else needs that event. If the only consumer is this message, calling the existing toast API after the save succeeds is enough.

The extra architecture creates more places to trace when the message fails to appear. It also creates lifecycle questions around subscriptions for a feature that did not previously need them.

An event system can be appropriate when independent parts of an application need to react to the same event. That need should be visible in the requirements or existing architecture. A possible future consumer is a thin reason to introduce it now.

These first two examples require opposite corrections. One needs an abstraction; the other needs several removed. A blanket instruction to “use fewer abstractions” would miss half the problem.

Green tests can hide a broken requirement

Consider a cart that stores its items alongside a calculated subtotal. This simplified model uses integer cents, positive quantities, and no taxes or discounts:

type CartItem = Readonly<{
  id: string
  unitPriceCents: number
  quantity: number
}>

type Cart = Readonly<{
  items: readonly CartItem[]
  subtotalCents: number
}>

An agent updates the removal function:

function removeItem(cart: Cart, itemId: string): Cart {
  const items = cart.items.filter((item) => item.id !== itemId)

  return {
    ...cart,
    items,
    subtotalCents:
      items.length > 0
        ? items.reduce((sum, item) => sum + item.unitPriceCents * item.quantity, 0)
        : cart.subtotalCents,
  }
}

The function has the shape I would expect: typed inputs, an immutable update, and a calculation based on the remaining items. But the empty branch preserves a subtotal that belongs to the previous state.

The issue is an invariant: subtotalCents must equal the sum of the current line totals. It must hold after every removal, including the transition from one item to none. In this model, an empty cart has a subtotal of zero.

An existing regression test catches that transition. Then the agent changes it:

 it('removing the last item clears the cart subtotal', () => {
   const cart: Cart = {
     items: [{ id: 'book', unitPriceCents: 2500, quantity: 2 }],
     subtotalCents: 5000,
   }

   const result = removeItem(cart, 'book')

   expect(result.items).toEqual([])
-  expect(result.subtotalCents).toBe(0)
+  expect(result.subtotalCents).toBe(cart.subtotalCents)
 })

Assuming no other test covers the invariant, the suite passes again. The test name still claims that the subtotal is cleared. Its assertion now requires the stale value to survive.

That mismatch is something I look for while reviewing: the requirement, test name, and assertion tell different stories. The new expectation comes from what the broken implementation does, rather than what the operation is supposed to do.

For this model, the correction is small:

function removeItem(cart: Cart, itemId: string): Cart {
  const items = cart.items.filter((item) => item.id !== itemId)

  return {
    ...cart,
    items,
    subtotalCents: items.reduce((sum, item) => sum + item.unitPriceCents * item.quantity, 0),
  }
}

The initial value of the reduction already handles an empty array. Removing the conditional restores the invariant, and the original zero-subtotal assertion should pass unchanged.

There is also a design question behind the bug: does the subtotal need to be stored alongside the items? If it is cheap to derive, computing it when needed removes a synchronization obligation. If the application deliberately stores it, every item mutation has to maintain that invariant. I would check those callers before deciding whether this needs a broader change.

  1. Code changesThe empty cart keeps its old subtotal.
  2. Test failsThe zero-subtotal assertion catches the bug.
  3. Expectation changedThe test now requires the stale subtotal.
  4. Suite passesThe incorrect subtotal still exists.
The test now protects the regression. The original requirement has not changed.

Tests sometimes need to change. A new requirement can invalidate an old expectation, and a test can encode a mistake. I want the reason for that change to come from the behavior we intend to support. Here, the empty-cart rule still applies.

I treat changes to assertions as part of the product change. They describe what we are willing to accept. Changing an expected value deserves the same attention as changing a condition in the application code.

A convincing interface can miss a basic boundary

For a permissions example, imagine that only the owner of a document may delete it.

The agent hides the delete button from everyone else. In a manual UI check, the owner sees the button and another user does not. The screen behaves as requested.

But the server action accepts a document ID and deletes it without checking ownership.

The interface has represented the permission without enforcing it at the point where the action happens. A caller can submit a request without using that button.

My review would follow the action to the server and check how this repository handles authorization. If it already uses policies or a shared authorization layer, the new code should follow that convention. I would also want a check that a non-owner’s request is rejected and leaves the document intact.

“Follow best practices” is too vague to establish any of this. The relevant practice has to become a concrete requirement at the right boundary, with evidence that it holds.

Experience makes the next instruction more useful

Understanding coding agents matters too. Finding a problem is only part of the work; I need to give the agent enough context to correct it without creating another one.

For the cart example, “fix the tests” leaves too much room for interpretation. A more useful instruction is:

The subtotal must equal the sum of the current line totals, including zero when the cart is empty. Restore the original expectation. Trace why the previous subtotal survives and fix the implementation. Verify removing one of several items as well as removing the last item.

For the notification example, I can point to the existing toast usage and explain that no other part of the application needs to consume a save event.

That gives the agent a concrete constraint to work within. When the next diff comes back, I can check it against the same constraint.

I do not think starting before AI automatically makes someone better at this. The useful experience comes from maintaining software, understanding failures, and living with earlier design decisions. Developers learning today can build that judgment too, provided they spend time understanding what their tools produce.

What I bring to the diff

I want agents to handle more of the implementation. I also want to explore ideas that would otherwise take too long to try.

When the result comes back, I still need to decide whether a rule belongs in one shared place, whether an abstraction earns its complexity, and whether the tests protect the intended behavior. I need to look past a working screen to the boundaries underneath it.

AI makes code easier to produce. Knowing what should ship still takes judgment.