To the front page

Field notes

Two Models and an Engineer

Adversarial spec review on ordinary work

Written with the method it describes.

Paul Panotzki works two toy robots like marionettes. The grey one draws a bracket with a pencil, the yellow one inspects it through a magnifying glass, and an approval stamp waits beside the drawing.

NeoLogic has been my company since 2002. Most of what I know about software I learned delivering it for other people, under their constraints, on their deadlines.

What follows comes from the product side of my work: a venture I run with a partner and a handful of contractors across several disciplines. What we build has to survive app review, paying customers, and its own second year. I mention that because most writing about coding agents comes out of weekend demos, and very little of it survives contact with a product that has users.

Two things changed for us in the last six months. The first is the one everybody has written about: coding agents got good enough that implementation stopped being the expensive part. The second happened gradually. I cannot name the day it started. It has done more for our quality than the first change did for our speed.

None of the parts are new. Independent review, named ownership, written decisions, approval gates. All of it is older than my career. I invented nothing here. I applied it in a place where it had never been affordable.

A new kind of wrong

When code gets cheap the constraint moves upstream. Everybody says this. What nobody warned me about is how the failure mode changes.

A vague ticket used to cost a developer half a day of confusion, and the confusion was visible. They would come back and ask. Now a vague ticket costs an afternoon of confident, well structured, internally consistent, thoroughly wrong implementation. It arrives faster than it used to and it looks better on arrival. It has tests. They pass. They test the wrong thing.

So the specification becomes the expensive artifact, at the level of detail that survives contact with a real codebase. Not "add attribution to media", but which table, which write path, what happens on delete, whether an existing contract still holds, and what breaks for data already shipped to customers.

That level of detail is exactly what nobody has ever had time to write. Attacking a spec properly before anybody builds is the highest value activity I know of in software. It is also the most consistently skipped, for the boring reason that it costs senior attention. Getting two senior people in a room to tear a spec apart is expensive, so we do it for the big frightening features and skip it for the other ninety percent.

Why a ticket

The origin is unglamorous. I had known for a long time that a second model makes a useful critic. The reason I did not do it routinely was pure friction: it meant copying text between two chat windows by hand, over and over, and re-explaining the context every time to a model that had not seen the previous round. The idea was never the obstacle. The copying was.

What I needed was one place both models could read from and write to, where the current version of the truth was unambiguous and the argument behind it was preserved. A ticket already is that. The description holds the current state, the comment thread keeps the argument, and the status field says which gate the work is sitting at. I did not have to invent a format. I had to stop copying and pasting.

The human prioritises, decides, approves The ticket description · current truth comments · the argument status · the gate Responsible model owns the brief writes and revises Critic model different vendor comments only
One ticket both models read and write, with the human above it. Chat windows stay disposable.

The second reason is that an agent's own planning mode was not enough. It plans one thing, in one session, while I wait. Work does not arrive one thing at a time. I have a backlog with several features and a handful of bugs in it, and while one agent spends forty minutes building something, I want to be roughing out the next three briefs.

That is a concurrency problem, and I am the part of it that does not scale. What makes it survivable is that the state lives in the tickets and not in my head or in a chat scrollback. I can leave a brief at version two waiting for a critique and go do something else for a day. When I come back, the thread still says exactly where it stopped and what is still open. Rough input from me, refinement from the models, a backlog that moves in parallel instead of in sequence.

The conventions came out of that. Who owns the current text, how revisions are numbered, what a critique looks like, where approval is recorded. I wrote them so two tools could share one document without confusion. What they turned into was an adversarial specification review cheap enough to run on ordinary work rather than only on the frightening features.

The loop

  1. I describe the problem. Rough is fine, sometimes a sentence.
  2. I tell one agent it is responsible for the brief. It reads the codebase and writes brief v1 into the ticket, including a mandatory grounded facts section: verified claims about how the system works today, each citing a file and a line, pinned to a repository revision so the citation still means something next month.
  3. I read it, then tell the other agent to critique it. Comments only. It may not edit the brief, the status, or anything else.
  4. The responsible agent answers every point in writing. Accepted, declined, or deferred, with a reason. Anything that is a product decision rather than a technical one comes to me instead of being decided.
  5. I decide those, the brief bumps a version, and it goes round again until a critique returns nothing structural.
  6. I approve, with a date. Only then does anyone plan or build.
Rough problem from me, a sentence Brief vN grounded facts, cited Critique numbered, comments only Dispositions accepted, declined, deferred product questions to me goes round again, version bumped nothing structural left Approved a name and a date Plan and build only now
One pass round the loop. Nothing is built until a critique comes back empty of structure.

Two details are worth spelling out. The tracker is the hub rather than the chat, which is what lets me run several parallel workstreams without holding any of it in my head. And prioritisation is mine. The models never decide what gets built, or when, or whether it is worth building. That is the part people assume automates first. It has not moved at all.

There is no pipeline

I want to be explicit about how manual this is, because the word "agents" makes people picture an orchestration layer. There is not one.

I have two coding agents open in two terminals. I tell each of them when to write and when to review. Nothing fires on its own, nothing is chained, no framework is coordinating anything. The gaps between the steps are the point. That is where I read the thing and argue with it. Sometimes I add a constraint I know from a previous product. Sometimes I throw out a direction entirely because I have watched it fail before. A critique is one input into a decision that stays mine.

I am also not loyal to a vendor. Those two happen to be the ones I keep open, and I will take a question to a third or a fourth when it suits. Different models miss different things, which is what I am actually paying for, and nothing here depends on which two you pick.

The whole toolchain costs less per month than one wrong week of building the wrong thing, and the comparison is not even close.

What it catches

A real example with the domain filed off. The feature adds a new global entity, links it to existing content through an ordered relation, and ships part of it inside the versioned bundles we publish to the app. Medium sized. The kind of thing I would previously have specified in two paragraphs and discovered the hard way.

Brief v2 came back with eight numbered findings. Two of them:

Deleting one of the new join rows would produce no history event and would not mark dependent bundles as stale, because the stream processor that handles auditing exits early on delete events by design. Cited, with the handler and the line number. The brief had assumed the generated delete was safe. The symptom would have been silent: a customer facing bundle serving stale content with nothing in the audit log.

Two files stated the same versioning contract in subtly different words. One says bump the version whenever the contract changes, the other says bump it when the resulting bytes change. The plan in the brief was valid under the second reading only. Under the first it would have invalidated every content approval in the system on deploy, and nobody would have known which reading was authoritative until production said so.

Seven of the eight were accepted, one partly. The next round found five more, this time implementation gaps rather than design flaws.

None of this is exotic. A good senior engineer finds most of it. The point is where a good senior engineer finds it: in a review meeting that never gets scheduled, because the feature looked small and nobody was going to read the spec twice. Here it took about an hour of elapsed time and twenty minutes of my attention, before a line of code existed.

Take the second finding. At spec time it is one paragraph in a document. After deploy it is every approval in the system silently invalidated, an incident, a rollback, and a week spent rebuilding trust in a system whose entire job is to be trustworthy.

Why two models

I have tried the cheaper variant of asking the same agent to review its own brief. It defends the design. It finds typos and edge cases, not premises. A model with no authorship of the text goes hunting instead. Using a different vendor widens the gap, because they tend to notice different things, but I would not claim that vendor diversity guarantees independent judgment. The separation of roles matters more than which two tools you pick.

Two mechanical rules matter more in practice than the choice of models. Every claim and every finding must cite code that exists, which kills a large category of plausible imaginary problems and lets me spot check in seconds. And every point gets a written disposition, so nothing is quietly dropped. In every human review process I have worked in, quietly dropped is the default fate of comments three through eight.

Where it breaks

These are the limits that have actually cost me time, rather than the ones that sound good in an article.

The tools are not the point

I use Linear because its agent integration is the best I have used, but that is a preference more than a requirement. Atlassian's own MCP server for Jira and Confluence is generally available. It does the same reading and writing from the same clients, and admins keep control over which tools may connect. What the method needs is only that agents can read and write tickets and comments, and that the history stays legible afterwards. If your company is on Jira, none of this is closed to you.

The same goes for the roles. The responsible party does not have to be a model. On a team it is usually the engineer who will build the thing, with the model as critic. What you gain is that the review happens at all, on every ticket, instead of only when somebody schedules a meeting for it. The senior engineer who would have chaired that meeting spends twenty minutes adjudicating findings instead of two hours discovering them.

And for anyone working with contractors, which we do across several disciplines, the output matters more than the ritual. A finished brief carries the verified current state, the decisions and who made them, an explicit out of scope list, and acceptance criteria. You can hand it to someone who has never seen your system. That is a procurement improvement as much as an engineering one.

Try it on one ticket

It costs an afternoon, plus twenty minutes of setup. The setup: put the durable rules about your system into one instruction file in the repository that every tool reads, and write down the review format itself somewhere both models can reach it.

  1. Take a medium feature you were about to specify in two paragraphs.
  2. Have an agent write the brief against the actual repository, with every claim about current behaviour citing a file and a line. Spot check three. If they do not hold, you have learned something valuable before spending anything.
  3. Have a different vendor's model critique it. Comments only, every finding citing real code.
  4. Answer every finding in writing, accepted, declined, or deferred, with a reason. Send the product questions to whoever owns the product.
  5. Approve explicitly, with a date, before a line of code is written.

By the end you will be looking at a list of things you would otherwise have found in production, and you can judge for yourself whether the list was worth it.

I did not set out to build a process. I set out to stop copying text between two chat windows. But the rules that fixed it gave me ownership, independence, a written history, and a human approval gate. Those are the same four properties that make any serious review process work. The only new thing is the price. Adversarial specification review is the thing our industry has always known it should do and has never been able to afford on ordinary work.

The conventions are below. There is nothing proprietary in them and nothing to buy. If you are working out how your own team should use these tools in production rather than in demos, that is the kind of problem I spend my days on at NeoLogic, and I am happy to talk about it.

Appendix: the conventions

Adapt the vocabulary to your tracker. The value is in the constraints.

Sources of truth. Durable technical truth in the repository. Current work in the tracker. Tool conversations are disposable and must be promoted or lost. No secrets in the tracker, ever.

Shared instructions. Both models have to work from the same instructions, or you are not running one process, you are running two. Each repository carries a single instruction file holding the durable rules: what the system is, how it is deployed, what the conventions are, and what requires human authorization. Different tools look for different filenames, so keep one canonical file and have the others import it. In our repositories AGENTS.md is canonical and CLAUDE.md is a one line import of it, which means a rule is written once and every tool obeys it. Anything you would tell a new contractor in their first week belongs there. Nothing machine specific ever does.

The protocol itself is written down, where both models can read it. Ours is a document in the tracker, referenced from the repository instructions. A critic that has not read the critique format writes an essay instead of numbered findings. A responsible party that has not read the response format silently drops half of them. Rules only work when the participants can look them up, and that includes the rule about how to disagree.

Roles. Exactly one responsible party per brief, named in the brief. Only they edit the description, status, labels, or ownership. Critics comment and nothing else. A named human approves.

Brief header. A fixed block at the top of every managed ticket:

Brief v3
Responsible: <name>
Repository revision: <commit>
Last revised: <date>
Approval: Pending

Pin repository references to a revision whenever their future meaning matters.

On approval that last line becomes a name, a date, and a link to the approving comment. Once a plan exists, add a plan line and a separate plan approval line in the same two forms.

Versions. Briefs are numbered and bumped on every content revision, never on metadata edits such as recording an approval. Plans committed to the repository are versioned by the repository and cited as path plus commit.

Critique comment. Fixed opening lines naming the brief version and the reviewer, then numbered points, each citing real code.

Response comment. Written by the responsible party, naming the critique it answers and its outcome, then one line per point: accepted, declined, or deferred, with a reason. Never keep the critique list inside the description. The tracker holds the history.

Statuses and gates. Map your existing workflow onto the gates rather than inventing states. You need waiting for review, actively being revised, approved and ready, in progress, and done meaning complete end to end including anything deployed. The responsible party owns the transitions. Critics never touch them.

Operating vocabulary. Define what each of your own instructions means, so that "critique the brief" cannot be read as permission to rewrite it. It is a short section and it prevents more damage than the rest combined.