Back to blog
Series · Day 26
Software Engineering in the AI Era
View all lessons →
Engineering taste

Day 26: Taste Comes From the Options You Rejected

You can ship good code through an agent for two years and still not be able to make the call yourself. That's because judgment is the one part of engineering an agent can't hand you. You build it by watching options lose and understanding why they lost. By default, your agent hides every option that lost.

The blank look in review

A junior on my team shipped a caching layer for a slow product-catalog endpoint. The work was clean: a Redis read-through cache in the service layer, sensible TTLs, passing tests. In review I asked, "Why here and not at the gateway?" and got a blank look. They weren't being defensive. They honestly didn't know there had been a choice.

They hadn't been careless either. They had read the diff, run it, and checked the numbers. The decision had been made inside the agent's reasoning, and only the result came out. You can't question a choice you never saw anyone make.

What taste actually is

Taste is what's left over from the options you've seen rejected, plus the reasons they were rejected. Senior engineers didn't get it from writing good code. They got it from watching bad options lose: a first draft that fell over under load, a reviewer's question that sank a design, a 2 a.m. incident caused by a cache they were sure was safe. Every loss leaves a small rule behind. One example: "invalidation is the hard part, so put the cache where the writes are visible." Once you've collected enough of those rules, you start recognizing a situation before you've reasoned it through. People call that recognition taste.

What agents quietly removed

Before agents, you could see the rejected options. They were the false starts in your editor, the branches you deleted, the whiteboard arguments, the review threads with "have you considered...". Even beginners saw the losing options, usually because they had written them.

Now the back-and-forth happens inside the agent, and you get one tidy diff. The output is better. The part of the work that used to teach you is gone. Day 19 covered reviewing agent diffs and Day 22 covered writing specs before prompting, and both assumed you already know which decisions are in play. This lesson is about getting those decisions back in front of you.

Why the usual fixes don't work

  • ▹"Just read the code more carefully." Careful reading checks the option you were given. It can't show you the ones you weren't given.
  • ▹"Turn the agent off sometimes." It's slow, it feels like a punishment, and most people give up on it within a sprint. It also only shows you your own false starts, not the range of options an agent can lay out in seconds.

Step 1: The losing-options prompt

Make the agent show you what it threw away. Put this in CLAUDE.md (or AGENTS.md, or your task template) so it runs on every non-trivial task:

markdown
## Losing options (required for any non-trivial change)

Before implementing, list the top 3 approaches you considered and rejected.
For each one:
1. What it is (one or two sentences, concrete enough to implement).
2. Why it lost *here* (tie it to this codebase, this data, these constraints).
3. What would have to be true for it to win instead.

Rules:
- Only list options a competent engineer would seriously consider. No straw men.
- If a rejection depends on something you assumed rather than checked
  (load, data size, query plan, team ownership), mark it as ASSUMED.
- Then state the chosen approach and its biggest known weakness.

Clause three is the one that matters most. "Gateway caching lost" is a verdict, and it only applies to this one PR. "Gateway caching wins when responses don't vary per user and invalidation can be driven by time alone" is a principle, and you can bring it to every caching decision you make from now on. The verdict tells you what happened here. The condition is what builds judgment.

The ASSUMED marker has a practical job. Agents often reject an option because of a fact they never checked, and the marker shows you exactly where to dig.

The caching task again, with the losing options visible

When we re-ran the catalog task with the prompt in place, the agent returned roughly this. My notes follow.

markdown
### Rejected 1: Cache at the API gateway (HTTP cache on GET /catalog)
Why it lost: responses include per-tenant pricing, so the cache key needs the
tenant ID from the auth token. Our gateway config can't key on decoded JWT claims
without a custom plugin. Invalidation on price change would be TTL-only.
Would win if: responses were identical across tenants, or stale prices for
up to the TTL were acceptable to the business.

### Rejected 2: Write-through cache (update Redis on every catalog write)
Why it lost: catalog writes come from three places (admin UI, nightly import
job, pricing service). Write-through means touching all three, and missing one
means silently stale data with no TTL safety net.
Would win if: all writes went through a single service we own.

### Rejected 3: No cache - fix the query instead
Why it lost: the query already uses indexes on tenant_id and category_id;
remaining latency is inherent to the join size. (ASSUMED)
Would win if: the slow path were a missing index or N+1 pattern.

### Chosen: Read-through cache in CatalogService, keyed by tenant+category,
5 min TTL, explicit eviction from the pricing service's update event.
Biggest weakness: import job writes don't emit events, so imports rely on TTL.

What was good:

  • ▹Rejection 1 rests on a real constraint: per-tenant pricing. Its condition carries over to other work, because "does the response vary per user?" is the first question to ask about any edge cache.
  • ▹Rejection 2 names the actual risk of write-through, which is having several write paths. It doesn't just say "it's complex." You'll see its condition, a single owner for writes, again in event-driven systems.
  • ▹The chosen approach admits its own weak spot: imports skip eviction. That's exactly where a reviewer should push.

Where the reasoning was weak:

  • ▹Rejection 3 was marked ASSUMED, and the assumption was wrong. When we ran EXPLAIN ANALYZE, the planner wasn't using the composite index because of a type mismatch on category_id. The agent had read the schema, seen the index, and stopped there. It never checked whether the planner actually used it. Once we fixed the query, the endpoint got much faster without any cache, and the cache became an optimization instead of a necessity.
  • ▹This pattern comes up a lot. Agents dismiss the boring option (fix what's already there) after a quick read, then build the interesting option thoroughly. If a rejection depends on a performance claim nobody measured, treat it as unproven.

Step 2: Argue before you accept

The agent's list is raw material. Reading it won't teach you much. Arguing with it will. Before you accept the change, pick one rejected option and write two sentences on whether the agent was right to reject it. Those sentences go in the PR description.

markdown
## Argued rejection
Option: No cache, fix the query.
The agent rejected this assuming indexes were used, but EXPLAIN showed a seq scan
caused by a varchar/int mismatch on category_id. Fixing that first is cheaper and
shrinks what the cache has to protect, so I'm doing both, query fix first.
  • ▹Pick the rejection you're least sure about, not the one that's easiest to agree with.
  • ▹"The agent was right" is a perfectly good conclusion, as long as you add one fact you checked yourself.
  • ▹If you can't write the two sentences, you don't understand the change well enough to merge it. Better to find that out now than after the merge.

Two sentences is small on purpose. It costs a few minutes, and that's when the principle moves out of the agent's output and into your head.

Step 3 (for leads): Ask "which option did you reject?"

Stop asking "why did you do X?" in review. That question invites people to justify the code after the fact, and with agent-written code the honest answer is often "the agent did it." Ask "which option did you reject, and why?" instead. That's a question about the decision, and with the prompt in place there's always an answer.

What changed on my team: within a few weeks, juniors were showing up to review having already read the rejected list, because they knew the question was coming. Review threads moved away from naming and style and toward boundaries, ownership, and invalidation, which is where judgment actually gets passed on. A couple of people started adding a rejected option of their own when the agent missed one they'd thought of. That was the clearest sign it was working.

What didn't work: sometimes agents invent straw-man alternatives just to fill the three slots. Signs to watch for:

  • ▹No reasonable engineer would propose the option ("rewrite the service in Rust").
  • ▹The rejection just restates the option, as in "rejected because it adds complexity," without saying what complexity or where.
  • ▹The "would win if" clause is empty or impossible ("if requirements were completely different").
  • ▹All three options lose for the same reason. That usually means the agent thought of one real alternative and padded out the rest.
  • ▹The boring option is missing entirely: do nothing, fix the existing code, or delete something.

When you spot these, reply with "give me the strongest alternative a senior on this team would argue for." That usually gets you one real option, and one real option is worth more than three fillers. We also learned not to require the full list on small changes. Run it on a typo fix and you get padding, and people learn to skim it.

A note from multi-agent research

This problem isn't limited to humans learning. In my PhD work on multi-agent systems, the same thing happens between agents. When a planner agent passes along only the plan it chose, the downstream agents and the humans debugging them can't tell whether a bad outcome came from a bad choice or from a good choice built on wrong assumptions. Systems that record the rejected candidate plans and the reasons for rejecting them are much easier to debug and to trust, because you can see where the reasoning went wrong. The explainable-planning community has made this argument for years under the name contrastive explanation: answering "why this plan and not that one?" Your juniors need that same contrast in order to learn.

Do this week

  • ▹Add the losing-options block to the CLAUDE.md (or AGENTS.md) of one repo you work in.
  • ▹Read the rejected lists on the next five PRs, yours or a teammate's. Before reading each one, write down what you think the agent rejected.
  • ▹If you couldn't predict any of them, you've found where your own taste is thinnest. Spend your reading time there.
  • ▹Leads: ask "which option did you reject?" in every review for two weeks, and watch how the answers change.

Tomorrow, on Day 27, we stick with judgment but move from design to debugging: how to keep your own hypotheses in play when an agent goes straight for the fix.

Flashcards
Check yourself

Extend your knowledge

  • ▹Read Michael Nygard's original post on Architecture Decision Records (ADRs), then look at the MADR template and its "Considered Options" section. It's the same idea applied to long-lived decisions. Try turning one good rejected-options list into an ADR.
  • ▹Look up contrastive explanation in explainable AI planning (XAIP), the research on answering "why plan A and not plan B?" for automated planners.
  • ▹Go back to Gary Klein's work on recognition-primed decision making (for example, "Sources of Power"). It explains why experienced people seem to just know, and why that knowing comes from cases they've piled up over time.
  • ▹Take one of your merged PRs from last month and write its losing-options list from memory. Then ask an agent to do the same and compare the two lists.
Test yourself on this lesson →

Discussion

Chat with Chi Cong (AI) about this article. Your conversation is private to you — you can publish a summary for others when you're done.

Ask me anything about “Engineering taste” — trade-offs, decisions, or the story behind it.