TL;DR — For almost two years the thing that kept me copy-pasting between the browser and my editor instead of going fully onto coding agents wasn’t the models’ intelligence — it was the quality of the code they generated by default. Models are trained on the world’s code, and the world’s average code is low quality: ask for a unit test and you get ten-plus lines of preparation and mocks pasted inline into every test, where a three-statement test belongs. The piece that closed the gap was a unit-test skill: a reusable instruction set that makes the agent write tests — and therefore code — to my standard, not its training data’s average. With the tests as the guardrail, review became skimming to confirm. If the quality of the output is your last holdout too, this is the piece.

The quality gap
The models were capable early; their defaults were not. What they produced by default read like the median of public code, and the median of public code is low quality. You could see it most clearly in the tests: ask for a unit test and get every test opening with ten-plus lines of preparation and mock wiring, copy-pasted and slightly drifted from test to test, asserting whatever the code happens to do. The way I write that test is three statements: arrange through a factory, one action, one assertion. The same gap ran through the production code it wrote. The price was constant guidance: every session, on every task, I steered it by hand to what the skill now does on its own — write the unit test the way I write unit tests, and keep the production code clean and readable. So the agent stayed in the browser, and I stayed the typist between it and my editor.
Both sides moved in those two years: the models got better at code, and the tooling category is younger than it feels — agents that run in your terminal, with full access to the repository, are a recent arrival. That second one is the precondition for everything below. A chat window in a browser has nowhere to keep your conventions, so you retype them, every session, forever. Once the agent runs where the code lives, the conventions can live there too — as files in the repository that it loads every time. That is what an AGENTS.md, and then a skill, actually is.
So the local agent made the fix possible. It wasn’t the fix. The one thing missing was trusting the code quality, and the piece that finally closed that gap was getting the tests right.
Encode the discipline once
A skill is a reusable instruction set the agent loads when a matching task comes up, so you teach the conventions once instead of re-explaining them every session. Mine encodes a discipline I had been working to for years, long before agents — drawn from the classics: Clean Code, Kent Beck’s Test-Driven Development, Osherove’s The Art of Unit Testing, Freeman and Pryce’s Growing Object-Oriented Software, Guided by Tests. It settled into London School TDD with strict Arrange-Act-Assert. Each rule below carries its reason:
makeFactory()owns the entire Arrange phase. Every test calls it, destructures only what it needs, acts, and asserts. The factory sits at the bottom of the file, after all the tests — you read tests to understand behaviour, and only scroll down when you need to change the wiring. Scenarios are factory overrides —makeFactory({ zeroChargeAmount: true })— not copy-pasted setup drifting from test to test.- One
expect()per test — two assertions is two tests. When a test fails, one assertion means you know exactly which behaviour regressed; multiple assertions hide which one broke and make the failure ambiguous. - Mock every collaborator at the class boundary. Tests that hit real services are slow, flaky, and environment-dependent — they fail for reasons unrelated to the code under test.
- Never let a test depend on the real clock, network, filesystem, or random values. A test that passes today and fails tomorrow without any code change is not a test — it is noise that erodes trust in the entire suite. Time is frozen in the factory; throwaway data comes from a faker.
- Test behaviour, not implementation. Behaviour is the observable output of the unit’s public interface — return values, thrown errors, messages sent to collaborators. Tests never assert on internals — private methods, internal state, the order of operations.
- No code comments — a comment is a missing test title. Anything you would write as a comment about what the code does or why must instead be a test title asserting that behaviour. Tempted to write
// falls back to eu-west-1 when no region is set? Delete it and writeit("falls back to eu-west-1 when no region is set"). A comment is static prose that silently drifts out of sync — nothing fails when it becomes wrong, so it rots. A test title is living documentation: bound to an assertion, so if the behaviour changes, the test goes red. The docs cannot go stale, because the suite enforces them. The only permitted comment is an irreducible external constraint no test can express — a link to an upstream bug, a regulatory note — and it is rare. - A bug fix starts with a failing test — red before green. A test written after the fix can go green even if the fix is wrong; the red step is what proves the test actually catches the bug. That test is the bug’s permanent tombstone, and it never gets deleted.
- Specification-style names.
it("does not create a payment when charge is zero")— a plain-English sentence describing the behaviour; the runner already shows the file and class, so repeating them in the name is noise.
The skill applies automatically on every code change — creating, fixing, refactoring, or changing any function, class, component, or module — not when someone remembers to ask for tests.
None of this is settled doctrine. Mocking every injected collaborator is the London School position, and the classicist school leans on real objects where it reasonably can; the fair objection to my side is that doubling every seam couples tests to structure, so refactoring internals can break tests that shouldn’t care. I take that trade because the alternative fails for reasons that have nothing to do with the unit under test. If you land somewhere else, the skill still works — the point is that your discipline gets encoded, not that it has to be mine.
The skill is public, with a runnable example: github.com/rdok/unit-test-skill — including the deliberate-break exercise, which matters more than the green run.
What it looks like in practice
This blog’s own migration, this week, ran on the skill. One example: a component that embeds commit-pinned code samples from GitHub, whose line-range logic had to exactly match the semantics of the Jekyll tags it replaced. The agent wrote the tests first, against a module that did not exist yet — factory-arranged, one assertion each:
it("selects a zero-based inclusive range, matching the old Liquid tag", () => {
const { content } = makeFactory();
expect(selectSampleLines(content, 1, 2)).toBe("second\nthird");
});
Fifteen tests, green. Then the standing verification rule from my wider ruleset kicked in — a green suite proves nothing until you have seen it go red. I mutated the code, changed the inclusive slice to exclusive, watched exactly the three range tests fail and nothing else, and reverted. Green now means something specific: when a later change breaks those semantics, the suite says so.
What actually changed
The shift wasn’t that I stopped checking the code — I still review it. It’s that I went from constantly teaching the agent how to write proper tests and clean code to just skimming to confirm, with the tests as the guardrail, which freed my attention for architecture and product.
“Skimming” undersells one part, though. The tests carry correctness; what they don’t carry is whether the shape of the code is right — whether this belongs here, whether that seam is the one I want. That reading stays mine, and it is the part that got more of my attention rather than less. In practice it rarely bites: the agent has the surrounding code as its example, so what it writes usually fits the architecture already there. But it is the thing I am actually looking at now, and no green suite will tell me about it.
The other effect is that the skill is a version-controlled file rather than something held in one person’s head or one assistant’s private memory: I fold lessons back in as I go, so they compound instead of starting cold.
Trade notes
If you’re holding back from going fully onto coding agents, it’s worth naming what your actual last piece is. If it’s what mine was — the default quality of what they write — then the answer isn’t waiting for a model trained on better code. It’s encoding the standard you already work to, once, so the agent reasons from the patterns you already follow instead of guessing. The comments are open — I’d like to hear where others are with this.