Skip to content

Ongoing

How I build with AI

The model is the least interesting part. What makes AI-assisted work hold up is the written context around it and the gates underneath it.

Role
Personal practice
Timeline
2025 – present
Stack
ClaudeCursorCLAUDE.md / AGENTS.mdADRs

Context#

Most of what I build now goes through an AI-assisted loop. That sentence is worth very little on its own, because it is currently true of most people who write software, and because the interesting variance is not in which model you use.

The variance is in what the model knows when it starts, and what has to be true before its output is allowed to count.

Constraints#

Working this way fails in a small number of predictable ways:

  • Context evaporates. Every session starts from nothing. Decisions you made last week get relitigated, silently, in the direction of whatever is most conventional.
  • Plausible is not correct. Generated code reads well. That is the entire problem — it removes the surface cue you were unconsciously using to decide what deserved a second look.
  • Volume outruns review. It is trivially easy to produce more change than you can honestly check, at which point "I reviewed it" quietly becomes "I skimmed it".
  • Conventions drift. Nothing enforces the thing you decided unless something enforces it.

Each of those is a context problem or a verification problem. Neither is solved by a better model.

Decisions#

The brief lives in the repo#

Every project carries a written brief — CLAUDE.md, AGENTS.md, whatever the tool reads. Not a description of the code, which the code already provides, but the things the code cannot tell you: who this is for, what the constraints are, what is deliberately out of scope, and what the voice is supposed to sound like.

The test I use for whether something belongs in it: would a competent stranger get this wrong by default? If yes, write it down. If the answer is derivable from the repo, leave it out — a brief that repeats the codebase is a brief that goes stale.

What I turned down: keeping context in the chat. It works, right up until the session ends, and it means the context is owned by whoever happened to be typing.

Decisions get written down as decisions#

The brief holds the constraints; a decision log holds what was chosen and what was rejected. This site has a docs/decisions/ directory for exactly that — one file per decision, each naming the alternative that lost and why.

The rejected alternative is the load-bearing half. Without it, every future session — mine or a model's — re-proposes the thing you already considered, and you have to remember why you said no. With it, the argument is already made.

What I turned down: commit messages as the decision record. They record what changed, at the granularity of what changed. Nobody reads them to find out whether Vercel was considered.

Gates, not vibes#

Anything I claim should be checked by something that can fail.

This site's palette claims WCAG AA in both themes. That claim used to be a comment in a stylesheet, which is a claim nobody can check. It is now npm run check:contrast — it parses the palette out of the CSS and exits non-zero if any text-and-surface pair drops below the threshold. It caught a real failure the first time it ran, in a colour I had already convinced myself was fine.

Unconfirmed facts get the same treatment: placeholders are a component, not a convention, and npm run check:pending will list every one of them.

What I turned down: trusting review to catch it. Review catches things once. A gate catches them every time, including at 1am when nobody is reviewing.

Some things do not get handed over#

Schema design. Anything with a migration path. Any decision I would have to defend to the person paying for it.

Not because a model cannot produce them — it can, fluently. Because those are the decisions whose cost arrives months later, in a form where "it looked reasonable at the time" is not a defence. The rule I actually apply: if being wrong here is expensive and the wrongness would be invisible for a while, I do it myself.

Architecture#

A written brief and a decision log feed into a working session. The session's output passes through automated gates, then human review. Review writes back into the decision log.writes backBriefCLAUDE.mdDecision logdocs/decisionsSessionClaude / CursorGateslint · build · checksReviewhuman
The feedback edge is the part that stops this being a treadmill: what review finds becomes context, not a correction that has to be remembered.

Outcome#

The receipts are the conventions files themselves, in synkt (opens in a new tab) — and in this site, which was built under exactly the process described above and carries the brief and the decision log in the repo.

What it actually buys, in order of how much I value it:

  1. Decisions stop being relitigated. The expensive part of a session used to be re-establishing why things are the way they are.
  2. Claims are checkable. "Contrast passes AA" and "no unverified numbers ship" are commands, not intentions.
  3. Speed, which everyone leads with and which is the least of it. Faster output against a weak review process mostly gets you to the wrong place sooner.

What I'd do differently#

I would have started writing the briefs before I needed them, not after the second time I explained the same constraint.

Every one of my CLAUDE.md files exists because something went wrong twice. That is a reasonable way to discover which conventions matter, and a bad way to run the first month of a project — the cost of the first two mistakes is real and the document that would have prevented them takes twenty minutes to write.

The other one: for a long time I kept the decision log in my head and the brief in the repo, which meant the model had the constraints but not the history. It would propose something reasonable, I would reject it for a reason I never wrote down, and the next session would propose it again. Writing down the rejected alternatives fixed more than writing down the decisions did.