marek@blog:~$ cat posts/stop-reviewing-code-start-defining-done.md

Stop reviewing code. Start defining "done".

In the last post I argued that the hard part of working with AI is not the tools, it is letting go of control over the code. That is the mindset. This post is about the mechanism, because “just let go” is useless advice if letting go feels reckless. It should feel safe. Here is how you make it safe.

Start with an uncomfortable admission: software was never as deterministic as we pretend.

We like to think engineering is precise. But look at how work actually flowed. Product, engineering, and business talked. We wrote things down, some of it well, most of it in our heads. We agreed on what to build, and then engineers mapped that agreement into code. The code was the artifact, but the real work was translating a fuzzy intent into something exact. And it drifted constantly. We planned for X and shipped X-1, or shipped Y because we discovered something halfway through, or ran out of time. Every engineer knows this. Determinism was always partial, held together by good people remembering the agreement and expressing it in code.

So when I say “determinism moves to the guardrails,” I am not adding a new burden. I am asking us to make explicit what we always did informally.

We have always made unreliable things reliable

Here is the reframe that makes agentic AI feel like continuity instead of a leap of faith.

Engineers have never trusted their components to be deterministic. Networks drop packets. Hardware fails. Third-party APIs return nonsense on a bad day. We never solved that by controlling the internals of the network or the vendor. We wrapped them: contracts, timeouts, retries, validation, tests. We constrained the inputs and we verified the outputs, and we stopped worrying about what happened in the middle.

An AI agent is another unreliable component. Capable, fast, and non-deterministic - ask it the same thing twice and you can get two different implementations. The move is the same one we have always made. Stop trying to control how it works. Constrain what goes in, verify what comes out, and let the implementation vary. Determinism moves out of the author’s hands and into the guardrails around the work.

If this still feels like a stretch, notice that you already trust thousands of lines of code you never read - every framework and library you depend on. That trust was not free. The industry built it over decades: standards, norms, ways of verifying things work. The job now is building the same kind of trust, much faster, for a builder that does not behave the same way twice.

That splits the job into two shifts.

Shift one: own the scope, not just the code

The first guardrail is on the input. The intent going into the work has to become well-specified scope, captured in a form precise enough that success is not a matter of opinion.

This is where a lot of engineers have to leave a comfortable position. For years the safe stance was “I am an engineer, I own the code.” Scoping, defining outcomes, pinning down what “done” means - that was someone else’s job, or a thing you did loosely on the way to the real work of typing. But that framing was always a bit of a bunker. The engineers I have always rated most highly were the ones who owned the outcome, who pushed back on vague requirements and turned them into something concrete before writing a line.

That skill used to separate great engineers from good ones. With today’s tools it is not a differentiator anymore, it is the price of staying relevant. If the implementation is increasingly generated, the value you add moves upstream, to defining the problem precisely and downstream, to verifying the result. The middle is exactly the part the machine is good at.

Shift two: make the guardrails explicit and automated

The second guardrail is on the output. This is where “we tested it” has to grow up.

In a lot of organizations, verification is still mostly human. An engineer tests a little. A reviewer reads the diff. QA manually clicks through and confirms it still works. Then UAT follows. That worked when a human was writing every line at human pace. It does not survive a component that produces large changes quickly and slightly differently each time. You cannot manually re-verify everything, every time, forever.

So the shift is from “someone checked it” to replicable, easily runnable, automated steps that enforce the outcome. Not because automation is fashionable, but because it is the only thing that makes “done” mean the same thing on run one and run one hundred.

And the place where both shifts meet is a simple idea: make “done” executable. When your acceptance criteria and your tests are the definition of success, precise scope and automated verification become the same artifact. The implementation can be anything. The bar it has to clear does not move.

What this looks like in practice

Let me make it concrete with a deliberately boring, strictly technical task, because the principle is clearer when there is no product complexity to hide behind. It scales to features, improvements, and bugs, but let us keep it simple.

You have a service. It works. Its API has been tested by humans hundreds of times, changes get reviewed, there are a few unit tests scattered around. The database behind it, an old ElasticSearch version, is being deprecated. You have a month to migrate to a current OpenSearch instance. Critically, the API contract does not change. Same inputs, same outputs. Only the guts move.

The old way: swap the library, then manually walk every endpoint, controller, and service function until it compiles, deploy it, test it by hand, find what broke, repeat. The engineer checks a bit, but the real sign-off lands on QA to manually verify the whole application still behaves, even though nothing about the contract was supposed to change.

The guardrails way flips the order. Before touching the migration, you turn to an AI agent and say: build me blackbox tests that cover every endpoint of this API. Here are fixtures - a subset of a lower-environment database, or synthetic data, whatever fits. Wire them into CI and a pre-commit hook, and give me a way to run the whole thing locally with docker compose.

Now something important has happened. The current, working behavior of the service is captured as an executable specification. Whatever anyone does to this service from here, it must pass those tests, because passing them is the definition of “still working.”

Then you make that knowledge reusable. You write into the repo’s AGENTS.md what this service is, how to run it, and how to test it, so any agent - or any human - knows how to operate it. I like to call this the application harness.

Only then do you hand over the actual task: agent, replace the ElasticSearch library and database with the latest OpenSearch. Your goal is to make all the blackbox tests pass. Then… you go get a coffee.

That is the TDD pattern most of us already know, on steroids, with AI doing the heavy lifting on both the tests and the migration. And the same discipline applies to unit tests and linters at the repository level, and extends to end-to-end tests. More complex, same principle. Nothing new under the sun, just applied with better tools.

I have watched this play out for real. A few months ago we migrated from an older ElasticSearch to OpenSearch. The team estimated two sprint (3-4 weeks), reasonably, because the system had dependencies across many applications. The migration itself took a couple of hours. The whole project took two to three days, and most of that was standing up the new instance and copying data, not rewriting the API. We already had blackbox testing in place. The only thing that had made it look like a two-sprint job was the team still working in “I write the code” mode, not fully using the tools already sitting in front of them. The machine was ready before we were.

What to watch for

This is powerful, and power used carelessly is just faster damage. Three traps.

False velocity. AI lets you produce an enormous volume of change quickly. Without guardrails, all you have done is ship unverified change faster. The industry data already shows more AI usage correlating with more instability wherever the surrounding discipline did not keep up. Speed without verification is not progress, it is risk with a shorter fuse.

Weak guardrails give false confidence. Green CI proves nothing if the tests do not actually cover the contract. A suite you do not trust is worse than no suite, because it waves bad change through with a badge of approval. Building coverage you can actually rely on is now the real engineering work, not a chore you do afterward.

Unprepared repositories turn AI into a liability. Drop an agent into a repo with no local run, no tests, and no context, and it will produce plausible, confident, wrong code - and hand you more to review, not less. Application readiness is the precondition, not a nice-to-have.

One pragmatic caveat that matters. There is no magic number of tests, and no required level of detail. Do not turn test count into a KPI. The moment coverage becomes a target, it stops being a good measure - people weaken tests to hit the number, or drown the repo in brittle ones. Be pragmatic. Aim at the outcome you need to protect, and add only the harness that serves it. The same restraint applies to documentation and context. More is not better. Enough is better.

Which brings me back to the thing under all three traps: the tools are usually ready before the team is. You can build every guardrail perfectly and still get two-sprint estimates for two-hour jobs, because the constraint was never the technology. It is how people work, and how they are led. That is what I’ll be exploring in the next post.


Still sharing as I go, from things I have actually run into rather than a tidy playbook. If this is useful, the final post is about scaling it - taking this from one engineer’s discipline to how a team and an organization actually work. I would like to hear where your guardrails are strong, and where they quietly are not.