There is a stage most people get stuck at. The agent works, you read what it says it did, it sounds right, you merge it. Then a week later you find the rounding is wrong in a currency you do not use, and you realise you have been reviewing prose, not code.
The fix is not a better prompt. It is refusing to be the check.
- The agent edits the file.
- It reports: “I fixed the rounding and the tests should pass.”
- You read the prose and decide whether to believe it.
- Your attention is the check.
- The agent edits the file.
- It runs the check itself.
- Exit 0, it is done. Exit 1, it reads the failure and goes again.
- The check is the check.
An agent that can run a command can close its own loop: make a change, run the check, read the failure, try again. Without a check, it produces one attempt and a summary, and the loop closes through you. That is the whole difference, and it is why “write a test for this first” is often the fastest route to a fix, even when the fix is two lines.
Give it a ladder, not one big check
Full test suites are slow, so agents avoid them, so they stop being the contract. What works is a ladder: cheap checks on every turn, expensive ones at the moment they matter.
| Cost | Check | Catches |
|---|---|---|
| free | tsc --noEmit, eslint . | type drift, dead code, unused imports |
| seconds | the build | broken imports, bad config |
| a minute | unit and integration tests | wrong behaviour |
| a few minutes | a headless browser that measures the DOM | the thing you actually shipped |
Write the ladder down where the agent will see it, because a check it does not know about does not exist. This is a four line block in a project file, and it removes the entire class of “I have made the change, you may want to run the tests”.
## Checks
Run after every change, in this order. All must be clean.
- `npx tsc --noEmit -p tsconfig.json`
- `npm run lint`
- `npm run build`
Do not report a change as done until the build is clean. The part people skip: verifying what you can see
Type checks and unit tests cover logic. They say nothing about whether the page looks right, and “looks right” is where visual work quietly rots. The usual answer is a screenshot, which is better than nothing and worse than it appears: a screenshot proves the page rendered, not that it rendered correctly.
A headless browser can be driven from the same session, and once it is, the question stops being “does this look right” and becomes “what is the computed value”. That is a question with an answer.
- 01 The question Is the card border still 1px?
- 02 The wrong way Take a screenshot and squint at it. 0.375px of a 1px border is invisible to you, and to the model.
- 03 The right way Drive the page and measure the box.
- 04 The numbers
getComputedStyle(card).borderTopWidth→"1px",card.getBoundingClientRect().height→234.375,wrapper.clientHeight→234 - 05 The answer The clip is there: a fractional box inside a whole-pixel clip.
That is not a hypothetical. On this site, the cards in a scrolling row looked like their bottom border was missing, and no screenshot ever settled it. Measuring did, in one pass: the card was 234.375px tall because a 16 by 9 image over a fractional box does not land on a whole pixel, while the wrapper clipped at 234. The bottom border was losing 0.375 of its height on every viewport width. The fix was two pixels of padding, and it was only findable because the check was a number.
You do not need a testing framework for this. A browser with remote debugging on, and a handful of evaluated expressions, is enough to turn a visual review into a set of assertions.
Ask for this shape, not "check it looks right":
Open /pricing at 1440 and at 390. For each width, report:
- document.documentElement.scrollWidth - clientWidth (must be 0)
- the computed border width and colour of .plan-card
- the bounding height of every .plan-card (must match)
Then hover the first card and report the border colour again. Make the check the definition of done
Once checks exist, the last step is to stop treating them as a suggestion. “Do X” invites a report; “Do X, and it is not finished until Y exits zero” invites a loop. Same work, entirely different ending.
- Name the command, not the intent. “The build passes” beats “make sure nothing is broken”.
- Ask for the evidence in the reply: the actual output, not a claim about it.
- When a check is slow, say when to run it, otherwise it will be run once at the start and never again.
- If you cannot name a check for a task, that is worth noticing. It usually means the task is not specified yet.
That last point is the one that keeps paying. Every workflow in the rest of this series depends on a machine checkable definition of done, because that is the only thing that lets an agent work while you are not reading every line.