Tech Debt Disguised as Best Practices

The most expensive code your agents write is clean, tested, documented and unnecessary. A working 2026 protocol for finding what to remove, before it hardens into architecture.

Tech Debt Disguised as Best Practices — AI

Agent code review is very good at inspecting what exists.

It catches null dereferences, races, incomplete error handling, suspicious control flow, missing tests, style violations, and patterns applied incorrectly. Give it a factory and it will inspect the factory thoroughly. What it rarely asks is whether the factory should exist at all.

That is not a small gap. It splits review into two different activities, and the tooling is overwhelmingly pointed at the first one.

Agent review catches thisIt usually cannot establish thisWhy
A branch can dereference nullThat branch represents a case the product should supportThe bug is in the diff. The product decision is not
A retry loop can amplify loadThe operation should be retried at allCorrectness is local; retry policy is a system semantic
The factory violates its own patternThe factory is unnecessaryConformance is easy. Contesting the pattern is not
A test case is missingThe thing being tested is not worth maintainingCoverage is measurable. Product value is external
Two threads can raceThe system could avoid shared mutable state entirelyThe race is in the diff; the alternative architecture is absent
An extension point is inconsistentNo second implementation will ever existSpeculative extensibility looks prudent without roadmap context
Documentation is staleThe documented subsystem should be deletedComparing text to code needs no judgment about value

Diff review privileges local evidence. Strategy needs counterfactuals: what if we did not build this? What is the smallest behaviour that satisfies the actual requirement? Which future are we paying for, and how likely is it? A reviewer that sees only the patch cannot answer those. A reviewer that sees the whole repository but no product context usually cannot either.

The dangerous output is not messy code

An ambiguous task goes in. What comes out is an interface, two adapters, a configurable policy, a plugin point, a retry harness, structured errors, telemetry hooks, and tests for every permutation. Everything is named well. Everything is documented. The suite is green.

The product needed one function.

🧱
This is tech debt disguised as best practices.

It is harder to fight than ordinary debt, because every individual decision has a respectable justification. The interface improves testability. The config preserves flexibility. The retries improve resilience. The abstraction follows dependency inversion.

Review each choice on its own and the system wins every argument. Review the total and the design is absurd.

Which makes "best practices" the actual attack surface. A general coding model has seen thousands of examples where extensibility, configurability, abstraction and defensive handling were praised. It has not attended your planning meetings. It does not know the feature is an experiment, that only one backend will ever exist, or that this team values easy deletion over generality.

It is a context failure, not an intelligence failure

This is the part most people get backwards. More capable models do not automatically fix it — and under the wrong objective they make it worse. Ask for "production-ready code" and give a strong model more thinking time, and you get a more elaborate structure with a better defence prepared for it. The reasoning is not wrong. The objective is. Greater capability does not supply missing product context.

So the scarce engineering skill in 2026 is not generation. It is subtraction. And almost every tool we have points the other way — reviewers suggest additions, linters demand handling, coverage tools demand tests. As Saint-Exupéry put it:

Perfection is achieved, not when there is nothing more to add, but when there is nothing left to take away.

What follows is the working 2026 toolkit for the taking-away half. Some of it is tooling, most of it is process, and none of it removes the human from the decision. I have written before about keeping the workflow itself simple and about review being automated while the decision is not — this is the sharper version of both.


1. Separate the reasoning from the editing

Aider's architect/editor mode splits one request across two models. The architect reasons about the problem and emits natural-language instructions. A separate editor model takes those instructions and produces the actual diff.

In its September 2024 architect-mode post, Aider reported that o1-preview as architect, paired with either DeepSeek or o1-mini as editor, reached 85% on its code-editing benchmark — a high on that benchmark at that time, with models that are now several generations old. Do not read it as an enduring leaderboard result. Read it as evidence that the split itself helps: each model got the job it was better at, open-ended problem solving on one side and reliable repository editing on the other.

The same shape now shows up everywhere as plan mode, ask-then-code, and hand-rolled two-model pipelines. Ask for a plan with edits disabled, review the strategy, then hand the accepted plan to an editing model and review the diff against it.

The value for our problem is the seam. It creates a moment where the strategy can be challenged before any code exists to make it feel inevitable, and it leaves behind an artifact that says what you meant to build.

Its limit matters just as much. Architect mode improves the translation from reasoning to edits. It does not give the architect taste. An architect model will happily propose four layers where one would do, and a good editor will implement that mistake beautifully. Treat the boundary as a review gate, not as evidence that architectural judgment has been automated. The related pattern of running two models over one branch is the review-side version of the same idea.

2. Switch phases on purpose, and change who is asking

Early implementation should be loose. Build enough to find the shape of the problem. Call the real API. Let awkward facts kill the clean design you imagined before contact with the code.

Then switch. The exploratory question is "can we make this work?" The subtractive question is "now that it works, what did we learn that lets us remove things?"

Do not ask the same agent to make that switch in the same session. Models are conditioned by the conversation they have already produced — earlier choices become assumed premises, and a review request produces local repairs that preserve them. It looks like the model is defending its work. Really, contextual consistency rewards justification over repudiation.

So: fresh context, and where practical an independent reviewer — a different model family diversifies the critique, though it does not make it wiser by default. Give it the requirement plus the code plus repository context, but not the authoring conversation. Then frame it adversarially rather than neutrally.

🔪
A useful subtraction prompt

Assume this implementation is overbuilt. Identify behaviour, abstractions, configuration and failure handling that can be removed while still satisfying the stated acceptance criteria. For each removal, state what behaviour is retained, what interfaces disappear, and what risk it creates.

Demand concrete deletion proposals, not an essay about simplicity. A different model is not automatically wiser — its value is independence from the path that produced the code.

3. Invert the interview

Matt Pocock's /grill-me skill is the cleanest implementation I have found of a less common technique: stop prompting the agent for answers and make the agent interrogate you.

It interviews you in rounds, and the definition of a round is the good part — not an arbitrary batch, but the complete frontier of questions whose prerequisites you have already settled. Your answers unlock the next frontier. A published example ran roughly four rounds and 46 questions. It carries its state in the conversation and writes no files; what you keep is a clarified idea rather than an artifact.

It also does something I have not seen packaged elsewhere: it identifies ungrillable questions — decisions that discussion cannot settle because they need a prototype, a measurement, or contact with reality — and stops asking about them. Architecture arguments routinely disguise empirical uncertainty as opinion, and naming that category kills a lot of circular debate.

Why inverting the direction surfaces redundancy: forward prompting smuggles the solution into the request. "Build a configurable ingestion framework" already contains the framework. An interview instead asks how many sources exist today, which second one is actually committed, what differs between them, what has to be configurable without a deploy, and what it would cost to add the abstraction later.

Answers of "one", "none" and "about an hour" remove the justification for most of the design — and they are answers only you can give.

4. Use multiple lenses, and add the one nobody ships

A single reviewer asked to check correctness, security, performance, maintainability, testing, API design and simplicity will not distribute attention evenly. High-signal local defects crowd out strategic objections every time.

Qodo's PR review, per its February 2026 release, is described as running four specialised agents in parallel and then synthesising their findings. Whatever the product details, the shape is the point: specialisation gives each reviewer a narrow objective, so no concern disappears inside a generic prompt.

The lens missing from every product I know of is subtraction. I have not found a review product that ships subtraction as a first-class lens with its own mandate to propose deletions.

LensRequired outputIts characteristic failure
CorrectnessCases where behaviour violates the contractHappily accepts an unnecessary contract
SecurityExploitable boundaries and unsafe flowsRecommends more machinery regardless of product value
PerformanceMeasured or plausible cost centresOptimises work that should not exist
MaintainabilityCoupling, duplication, unclear ownershipRewards abstractions that look maintainable locally
SubtractionFeatures, layers, states, dependencies or tests removable without losing required behaviourMay remove real optionality if product context is missing

Constrain the subtraction reviewer: it may propose consolidation, inlining, deletion or postponement, and every proposal must tie to acceptance criteria and name the evidence that would make the complexity necessary later.

And when you synthesise, preserve the disagreement. If the correctness lens wants another recovery path and the subtraction lens wants the whole retry subsystem deleted, collapsing both into "improve error handling" throws away the only genuinely useful output of the review. That conflict is the decision. Give it to a human. My third-pass review structure exists for exactly this reason.

5. Retrieval is the actual review engine

Architectural review quality is won or lost before inference starts. A reviewer needs more than the changed lines:

RetrieveWhat it lets the reviewer say
Definitions of everything the diff calls"The function you wrapped already retries"
Callers of every modified public interface"No current caller uses this hook, and the plan commits no second one"
Related tests, including integration tests"This case is already covered upstream"
Nearby implementations of similar problems"We solved this three modules over"
Recent commits touching the same paths"We deleted an abstraction just like this recently"
The linked spec, PRD or ADR"The design said single-provider"
Ownership and dependency boundaries"This makes a leaf package an integration hub"

Note this is not "put the whole repo in the context window" — more context often obscures the decision. The goal is the smallest evidence package capable of challenging the design. The history row is the one people skip and the one that pays: a new abstraction looks reasonable right up until the reviewer can see that its twin was deleted recently. I wrote about hitting the limits of this in the harness, loop and graph vocabulary.

Without called definitions, tests, history and the design artifact, a request for "deep architectural review" is badly under-informed. The model has nothing to be deep about.

6. Render the architecture so a human can object to it

The agent does not have to make the architectural decision to improve it. It can make the structure visible.

Generate dependency graphs, call graphs, module maps and diagrams from the code the PR touches. Show before and after. Highlight new nodes, new edges, cycles, new public interfaces, new background processes, new configuration sources, new external dependencies.

Because a human can object to a picture in ten seconds — why does this feature cross five modules, why did one dependency become six, why is a leaf package now a hub, why are there three configuration paths, why does deleting this feature require touching an unrelated service. That reaction is valuable precisely because it arrives before the detailed defence of every class.

Keep the diagrams disposable and reproducible. A hand-maintained architecture diagram is just another stale artifact. The model can render the structure and challenge it. The accountable reviewer decides whether its cost is justified.

7. Make the design reviewable before the diff exists

A diff is a terrible representation of intent. By the time anyone sees it the design has already accumulated implementation weight. Write a small artifact first — a spec, a PRD, or an architecture decision record — stating the required outcome, the explicit non-goals, the constraints, the expected scale and lifetime, acceptable failure behaviour, alternatives considered, why the chosen option is the smallest adequate one, and what would have to become true to justify more complexity later.

There is a specific reason to keep writing them now: an agent will treat an undocumented constraint as an accident. A weird-looking guard with no stated reason is exactly what a cleanup pass removes. Writing the why down somewhere durable has become a defensive measure rather than documentation hygiene — with the obvious caveat that it only helps if the record actually makes it into the agent's review context.

Then review in two passes: the design against product reality, and the implementation against the accepted design. When the code introduces a plugin interface the design never mentioned, that is visible scope expansion rather than a judgment call. When the spec says "single provider for this experiment", a multi-provider abstraction now carries the burden of proof. This is the mechanic behind spec-driven agentic development and the architect's protocol.

Specs can also rationalise overbuilding. The cure is not a longer spec — it is strong non-goals and explicit deletion criteria.

8. Put complexity on a budget

Numbers cannot identify bad architecture. They can force the conversation before complexity becomes ambient.

BudgetA starting policyWhat it revealsWhat it cannot prove
Cyclomatic complexityWarn above 10 per function, rationale above 15Branch-heavy local designWhether the function should exist
Diff sizeExtra design review above ~400 non-generated changed linesScope growth and reviewabilityWhether a 100-line abstraction is justified
Runtime dependenciesEvery new direct dependency needs an owner and a removal planSupply-chain and maintenance expansionWhether it creates product value
Bundle growthFail above an agreed compressed-byte increaseUser-visible delivery costWhether the feature deserves its bytes
Module edgesReject new forbidden dependencies or cyclesBoundary erosionWhether the existing boundaries are correct
Dead codeNo new unused files, exports, flags or dependenciesAbandoned scaffoldingWhether the used code is useful

The tooling is unglamorous and real: ESLint's complexity rule, Radon or Xenon for Python, Knip for unused JavaScript/TypeScript files, exports and dependencies, dependency-cruiser for JS/TS module rules or ArchUnit for Java architecture tests, Size Limit for frontend bytes. The dead-code lane is worth taking seriously — a JSNation 2026 talk described a production cleanup that removed 28,000 lines, with Knip doing the identifying and people doing the verifying. Start in report-only mode; blocking CI on day one just alienates a team with a legacy repo.

Treat thresholds as alarms, not scores. Engineers game metrics that become goals — one complex function becomes five pointless ones, a large PR gets split without reducing scope, a dependency hides behind an internal wrapper. Budgets catch symptoms. The debt this article is about is clean by construction, so it passes every one of them.

9. Make deletion first-class work

A review process whose only verbs are add, handle, document and test can only ever grow a system. That is worth sitting with, because it describes almost every review process in existence.

So create deletion PRs with explicit targets: remove an unused extension point, collapse an interface that has one implementation, delete speculative configuration, eliminate a duplicate retry layer, drop a dependency. Include the evidence that the retained behaviour still satisfies the contract.

And do not measure deletion in lines. Removing one line that deletes a public interface or a dependency is worth more than deleting a thousand generated ones. Count removed concepts, states, edges, operational responsibilities and supported permutations. Deletion is product work — it reduces the number of futures the team has to stay compatible with.


The protocol, at PR level

Nothing here needs an autonomous architecture oracle. It uses current agents for planning, questioning, retrieval, rendering, specialised criticism and editing, and keeps the strategic decision with humans.

#Step
1Attach a design card. Outcome, non-goals, constraints, expected lifetime, acceptable failures, simplest proposed shape. Link an ADR for anything non-trivial
2Interrogate the assumptions. Agent-led interview in rounds. Mark ungrillable questions for prototypes instead of resolving them rhetorically
3Prototype only the uncertain parts. And do not let the prototype's incidental structure become the production architecture by default
4Separate planning from editing. Review the natural-language plan before any repository edits are permitted
5Stop at the first complete path. Once one end-to-end path meets the acceptance criteria, freeze feature expansion. Every further abstraction is a separate proposed decision
6Assemble a review context pack. Called definitions, callers, related tests, similar implementations, recent commits, the governing design artifact
7Render the structural delta. Before-and-after module map or dependency graph, with new interfaces, dependencies, processes and config sources highlighted
8Run independent lenses in fresh contexts. Correctness, security, performance, maintainability, subtraction. Different model from the author for the subtractive pass
9Demand deletion proposals. What can go now, what waits for a second real use case, what evidence justifies each surviving abstraction. "Looks reasonable" is not evidence
10Apply the numeric gates. Route violations to human review rather than auto-adding scaffolding to satisfy them
11Resolve strategic disagreement explicitly. Do not let synthesis average the conflict away
12Merge the smaller design. Strip rejected optionality in the same PR where safe, otherwise open a bounded deletion PR with an owner and a date. Not an open-ended cleanup ticket

What does not work yet

Asking an agent "is this too complex?" gets you very little. It is a leading question with no decision procedure behind it, and the model has no stake in the answer. Ask "is this design appropriate?" and you will often get an equally fluent defence. Underspecified review questions produce framing-sensitive answers.

It does not carry the pager. It does not explain the roadmap slip. It will not migrate the database in two years or delete the abstraction when the imagined second implementation never arrives. And product strategy mostly lives in conversations, customer commitments, failed experiments and staffing constraints — a large context window is not organisational memory.

There is also a taste problem, and taste is not knowledge of design patterns. It is calibrated discomfort with concepts that cost more than they return, and it comes from watching systems age. That is the thing on my list of things AI is still bad at that has moved least in a year.

For a model to do subtraction on its own, several things would have to change: durable memory of product strategy, non-goals and prior decisions; feedback tied to total system cost over months rather than patch acceptance; evaluation that rewards removed concepts and avoided maintenance; access to real usage data; explicit authority to reject scope; and reliable uncertainty handling that says "measure this" instead of inventing a defensive framework.

Until then the strongest final check is still low-tech: an independent reviewer with enough product context to ask why each concept exists. Agents can surface deletion candidates and render the structure. Somebody accountable still has to ask the question the diff cannot answer — why does this need to exist?

📖
Related Reading

KISS Your AI Workflow — the same principle applied to the workflow instead of the codebase.

The Review Is Automated — the Decision Isn't — where the line between agent and human actually falls.

The Third Pass — the review structure that keeps independent lenses from collapsing into one.

Things AI Is Surprisingly Bad At — the taste problem.

Claude Now Watermarks Your Text — what the invisible mark actually proves, and why code is the hardest case for it.
💬
Working with a team that wants to adopt AI-native workflows at scale? I help engineering teams build this capability — workflow design, knowledge architecture, team training, and embedded engineering. → AI-Native Engineering Consulting