Journal

How I built erictorii.com with parallel AI agents

431 commits, 34 agent branches and eight waves of parallel work, and the four rules that stopped it turning into a mess. Written from the repo, with the numbers.

Draft — the facts and numbers in this post are sourced from the repository and verified. The writing has not had my pass yet.

I designed this site. I did not type most of it. It was built by AI agents working in parallel — usually two to five at a time, each on its own branch — while I directed, reviewed and made every design call.

That sentence is easy to say and easy to fake, so here is the repository instead. Everything below is a number I can point at in the git history or in a file you could read if I handed you the folder.

The shape of the thing

It is a Next.js 16 app that compiles to a folder of static HTML. No server, no database on the critical path, output: "export" in the config. The homepage is a bento grid: a 4×4 of tiles that expand into panels, with a scroll intro in front of it.

WhatWhere it stands today
Commits431
Commits co-authored by an agent290
First commit → today21 June 2026 → 1 August 2026
Agent branches merged34, across 8 numbered waves
Lines of browser test3,359 across three suites

A “wave” is one round of parallel work: I write a brief per agent, launch them in one message, they each work on their own branch, and I merge and verify the combination at the end. Wave 1 was twelve branches. Wave 7 was four. The branches are named for what they own — wave6/lag, wave7/a11y, wave8/mobileux — because the name is the ownership boundary, and the ownership boundary is the entire trick.

One law file, and every agent reads it first

The repo has an AGENTS.md at the root that is not documentation. It is a set of five numbered rules, written in my words, that every agent is bound by before it writes a line:

  1. the desktop bento must fit in one viewport, and if content does not fit, the type shrinks, never the page;
  2. every tile is a square or two squares wide, and nothing else;
  3. no logo above the grid;
  4. the accent is blue #2E5BFF, with exactly one documented exception;
  5. every load starts dark.

Rule 1 carries a sixth thing, and it is the most useful line in the file: the fold is 900 pixels. One number, for both the layout and the choreography. It is hard-coded in eleven components and eight stylesheets, and the law says they move together or not at all. Before that number existed I had two thresholds — one for when the grid reflowed, one for when the animation changed — and a tablet landed in the gap between them. The page laid itself out in one mode and animated itself in the other. That is a worse bug than anything either threshold was solving, and it is the kind of bug that only appears when several people, or several agents, each fix a viewport problem correctly and separately.

Writing the constraint down once is what makes parallel work possible. Agents do not negotiate with each other. They read the same law.

Give every agent its own copy of the repo

Two agents editing the same checkout can see each other’s half-finished files. Every parallel round I ran before I fixed this produced the same confusion — agents reporting that files in git status were not theirs, one agent’s broken file failing another agent’s build. On 25 July a favicon.ico saved in the wrong pixel format broke next build outright, and because both agents were building from the same folder, it broke both of them at once. Neither had touched it.

The fix is a git worktree: one repository, several working directories, each on its own branch, sharing one history. It costs seconds to create and it makes the collision impossible rather than unlikely. Now every agent that might touch an overlapping file, or that needs to run its own build, gets one.

Two things that bit me anyway, both worth knowing before you try it:

  • The worktrees live inside the repo, so git add -A stages them. .gitignore has to know about them, and I now stage explicit paths while agents are running.
  • They do not always clean themselves up. A worktree that did real work stays on disk as a full checkout until you remove it.

Isolation prevents collisions. It does not prove the combination works — that still takes one central verify on the merged result, every time.

A green build proves nothing about your data

This is the mistake I would most like you to skip.

On 25 July a merge silently took an empty manifest.json — literally [] — over the real one. npm run build exited 0. All 63 browser probes passed. Twenty-six art files shipped to production with nothing wired up to them, and the gallery quietly rendered its empty state. Nothing in my process complained, because nothing in my process was looking at the data; it was looking at exit codes and behaviour.

So the repo now has a second gate that reads the built artifact on disk rather than the build’s exit code: does the manifest parse, is it populated, does every file it references actually exist in out/, is the favicon encoded in a format the build can read. It is 236 lines. It has caught things the 2,523-line browser suite structurally cannot, because those failures are not behavioural — the page behaves perfectly, it just has nothing in it.

The general rule I took from it: an exit code is not evidence. After every merge, diff the data files and read the diff. Merges resolve per file, the wrong side can win silently, and nothing will tell you.

Measure the thing you are about to change

I told an agent the site felt laggy on my phone when a tile opened. The honest version of that complaint is a number, so it went and got one: Chromium at 390×844 with touch input and the CPU throttled 4× to imitate a mid-range phone, running the production build, with a DevTools timeline trace read programmatically.

The answer was that opening one tile spent 857–1053ms in a single bucket: handing a rebuilt layer tree to the compositor. The animation moves top, left, width and height — layout properties — on twelve tiles at once, so the browser re-does geometry every frame for all of them. Five separate experiments (removing the blur, removing the shadows, adding containment, killing every animation, removing a concurrent scroll) moved that number by nothing at all.

What was fixable got fixed and measured: suppressing the drop shadow only on the tiles actually in motion, only while they move, cut shadow redraw by a mean of 41%, and by 52% and 66% on the two worst cases. The report says plainly that this does not touch the big number, and that the big number has exactly one cure — animating transform and opacity instead — which is a real rewrite I have not commissioned yet.

Two traps from that investigation that I would have fallen into on my own:

  • Counting dropped frames with requestAnimationFrame in headless Chrome is useless. It is not locked to a 60Hz screen. It cheerfully reported 119fps while the page was choking.
  • A CSS transition outranks !important. Setting box-shadow: none !important on an element that has a transition on box-shadow does not remove the shadow — it starts a smooth animation of the shadow, which is worse than leaving it alone. The first version of the fix shipped this and measured worse.

The load diet

A separate wave went after bytes on a throttled 3G connection. Before it, the homepage pulled 2,107KB before the first meaningful paint. After, about 1,130KB.

The single biggest item was the art carousel: eight 1200×1200 JPEGs, 1,214KB in total, rendering into squares about 75 CSS pixels wide — roughly sixteen times more pixels than any screen could show. Re-encoded as 400px WebP they came to 199.7KB, 16% of what they were, and the two worst cases for compression artefacts (pen line art, and a large blue gradient) were opened and looked at before they shipped. The rest of the diet was theme assets preloaded for a theme the visitor was not looking at, and one asset being fetched twice.

The failures that only happen at machine speed

Three that I keep as a list, because they are all the same shape — something technically correct that no human would have written:

  • A font loader named its variable serif, and the framework mints the CSS family name from the variable name. The result was @font-face { font-family: "serif" } — a custom family colliding with the CSS generic keyword, which browsers are allowed to resolve either way.
  • Declaring an openGraph object on the homepage silently replaced the one in the layout, because that merge is shallow. Every link preview of this site was a bare grey box for several days. It was visible in the built HTML the whole time; nobody opened the built HTML.
  • The finished bento shipped with zero <h1> elements and exactly one <h2> — every tile title was a <span>, because a span was what the design needed and nothing in the design brief mentioned a document outline. To a crawler and to a screen reader the page was undifferentiated soup.

Every one of those passed a build, passed the test suite, and looked perfect.

What I would actually tell you to copy

Four things, in order of how much they bought me:

  1. Write the constraints down in one file the agents read before anything else. Not preferences — laws, numbered, with the reasoning attached so nobody unpicks them by accident.
  2. Give every parallel agent its own worktree, and give every agent a disjoint set of files it owns.
  3. Gate on the artifact, not the exit code. Open the built output. Read the data. Look at the picture.
  4. Ask for the number before the fix. Every real improvement here started with a measurement, and at least two plausible fixes died because the measurement said they did nothing.

None of this is about the model. It is about the fact that work happening in parallel needs boundaries written down, and that a machine will do exactly what you asked at a speed where “exactly what you asked” is no longer the same thing as “what you wanted”.