How this site is built
Agents wrote nearly all of this site’s code, and I reviewed it. They built it from issues I’d triaged with them and decisions I’d written down first, and I ruled on whatever they couldn’t decide. The repository is private, so I’ve copied the pieces of that practice here.
Static Astro on Vercel
The site is static. Astro prerenders every page to HTML when the site is built, and Vercel serves those files as they are, so no server has to put a page together when you ask for it and it arrives fast. The one page that isn’t prerendered is a Preview Link, drawn on request so I can read a draft in the site before I publish it. The only JavaScript the site builds is its motion, which has a performance budget: 30 KB on Home once it’s compressed, and it sits just under that.
Headless WordPress for the words
I write in WordPress, at my desk or on my phone, and WordPress is never in a visitor’s path. Pressing Update asks Vercel for a build a minute later, and each save after that pushes it back, so a burst of edits is one build. The build reads every word over WordPress’s REST API and checks it before it draws a page.
Every page’s editable parts are listed once, in the code, with what each is called and whether it’s required. That list is the single source of truth: WordPress’s editing screens are made from it, the build checks each page against it, and a test fails if WordPress ever falls out of step with it. So what I can edit and what the site expects always match.

If I publish a page with something required left empty, the build stops rather than ship it. The site stays as it was, and my phone gets a message saying what’s missing and where to fix it. Emptying the bullets Home shows for this page would send this:
How this site is built: the page cannot be built from its slots. homeSummary: a required slot arrived empty. Fill it in on the How this site is built page in WordPress and publish again. The build stops here rather than ship a gutted page.The Motion library for the moves
Everything that moves is Motion, the animation library, run from plain scripts with no framework around it. Home performs once as it loads: the full stop in phil.dev drops, the headline lands word by word and a panel of facts slides in, all in about 1.3 seconds. Every other page is calmer, and if your device asks for reduced motion, things fade in and nothing moves. No feature writes a curve of its own: each takes one from a file of named springs, and these are the six the whole site shares:
export const springs = {
/** hover lift, arrow nudge, the return from a press */
lift: { type: "spring", visualDuration: 0.3, bounce: 0.25 },
/** the icon on hover, the release of a press */
pop: { type: "spring", visualDuration: 0.35, bounce: 0.5 },
/** the headline's words on arrival */
land: { type: "spring", visualDuration: 0.55, bounce: 0.15 },
/** section reveals on scroll */
rise: { type: "spring", visualDuration: 0.6, bounce: 0.1 },
/** blocks on a Note or a door page, the Testimonial into its card; the meter uses it stretched to 0.9 s */
settle: { type: "spring", visualDuration: 0.5, bounce: 0 },
/** the Wordmark's stop */
drop: { type: "spring", visualDuration: 0.5, bounce: 0.5 },
} as const;
Agents on the issues
I planned the site as issues before any of it was built: a map of where it was going, and a ticket for each decision on the way. The map opened on its destination:
A build-ready brief for phil.dev: the Identity chosen and recorded (an ADR, a tokens file and the tone term in `CONTEXT.md`), the hosting topology and stack fixed, the content model and the day-one page inventory settled, and the showreel and case-study decisions made. The map is done when build work can be cut into ready-for-agent and ready-for-human issues. Not the build itself, and not the common platform.Most changes since have started as an issue too, often one I’ve raised from my phone, and every issue is specced through a workflow: one of the agent skills this repository uses, which sets out step by step how an agent takes the work on. With /triage, an agent reads the issue, the code and what’s written down, asks me what only I can answer, each question with the answer it would pick, and writes the brief another agent builds from. With /grilling, it works through a bigger decision with me the same way, a round of questions at a time. Then the issue gets one of five labels, as docs/agents/triage-labels.md has them, which say what happens to it next and who does it:
| Label | Meaning |
|---|---|
needs-triage | Maintainer needs to evaluate this issue |
needs-info | Waiting on reporter for more information |
ready-for-agent | Fully specified, ready for an AFK agent |
ready-for-human | Requires human implementation |
wontfix | Will not be actioned |
Several agents build at once, each in its own copy of the repository, on a Mac mini that stays on. Each ends with a pull request setting out the decisions it made and why, for me to keep or overrule, and nothing merges until its tests pass. On 25 September, a week after I started building, the site had 3,046 tests and had been through 103 issues and 109 pull requests.
Two more labels hand an issue to the Mac mini while I’m away. It looks every five minutes, and when it finds one, it starts a job in a copy of the repository of its own, running the same workflow I’d run myself. claude-triage gets the issue triaged: the job applies one of the five labels and leaves a comment saying why, or asking what it needs to know, and writes nothing to the repository. claude gets it built: the job works test first, runs the whole suite, reviews its own change and opens a pull request, a draft if the tests are red, or declines in a comment saying why. It runs one job at a time, triage first, since a triaged issue is what makes a build worth running, and a label says how each one went:
| Label | Meaning |
|---|---|
claude | Implement this issue: opts it in for the issue runner |
claude-triage | Triage this issue: opts it in for the issue runner |
claude:working | A runner job is in flight on this issue |
claude:triaged | The runner triaged this issue; the role it applied and its comment say what it concluded |
claude:pr-opened | The runner opened a pull request for this issue |
claude:declined | The runner judged this issue not implementable as code; see its comment |
claude:failed | The runner’s machinery broke on this issue; it needs a human |
Each outcome reaches my phone as well, so an issue I label at night has its answer by morning.
A glossary and decision records as the context layer
An agent’s work is only as consistent as what it reads first, so every session here starts at CLAUDE.md, a short file that tells it to read the glossary and the decision records before it explores the code. The glossary is CONTEXT.md, where every word the site and its agents share is defined once, with the words to avoid. This is the entry for this page:
**How this site is built**:
The page where the site accounts for its own making as evidence for the working-with-agents door: how it is built and edited, and the practice behind it, shown by quoting the real artefacts rather than describing them.
_Avoid_: colophon, about this site, tech stack, uses pageA decision that would be hard to reverse gets a record of its own, named for what it decided, with its reasons and the options it turned down. There are three so far:
ls docs/adr
0001-one-thesis-three-doors.md
0002-the-identity-is-the-workbench.md
0003-the-platform-is-a-boundary-in-this-repo.mdThe first opens like this:
The site presents Phil as one consultant with a single thesis, "teams ship better when their judgement is written down and made reusable", and hangs three offers off it: working with agents, design systems, and product engineering, in that order. We chose this over presenting three equal, independent services because three unrelated pillars read as three different people, or a generalist with no edge, and because the thesis is what makes the AI offer credible. A design-systems track record is direct evidence of the governance and adoption work that most AI rollouts lack.CLAUDE.md holds the working rules as well. The words on this page were written to this one:
Words a visitor or an editor reads, whether in WordPress or built into the code, are written to `docs/copy/voice.md`: read it before writing or rewording any.A Markdown copy of every page, for agents
Every page has a plain version in Markdown, made from the page as the build finishes, so it says only what the page says. It’s what I’d hand an agent in place of the designed page. Press Markdown at the top of this one to see it, or point an agent at llms.txt, which lists them all.
I’d build the same setup with your team. A glossary your people and their agents share, a label on every piece of work that says who picks it up next, and a check that catches a mistake before a visitor sees it.
Say hello
Email hello@phil.dev. I read everything that isn’t a sales pitch.