Spec-driven development with OpenSpec and AI agents
This whole site was built spec-first. Before any application code existed there was a proposal, a design note, behaviour specs, and a task list. The code came after, written to satisfy those specs.
What OpenSpec is
Every change is a folder that moves through a small loop. I explore to think an idea out loud, propose to generate the artifacts in one pass (a proposal with its non-goals, a design note, the spec changes, a task list), apply to implement against the specs, then archive and sync so the main specs keep describing what actually ships. Propose does most of the work, usually from one plain sentence ("all notes should have a share feature"). Apply is the stage I run over and over. First, though, every idea meets one question: should this be a spec change at all?
What a spec buys me
Each spec is a requirement with a concrete scenario. One says a note tagged with a lens that does not exist still builds, falls back to a default, and warns with the file and the bad value. That reads as documentation and as a checklist a test suite can be measured against. When work goes to an AI agent, the spec is the contract: the agent satisfies the scenarios without quietly narrowing them, and I review against the same ones. Loop engineering runs on this: most failures are a fuzzy contract, not a weak model.
The changes, and the non-changes
Some were whole features large enough to touch two parts of the spec at once. Some were tiny: a search bug where typing "ai" matched those letters inside other words became its own change instead of a silent patch. Just as many were things I chose not to build. A half-finished summary panel on the About page was pulled as unready, and the change that removed it existed only to record the retreat. A build-config tweak got cut from an unrelated proposal to keep its scope clean. A proposal reaching for a feature branch got a two-word answer: no branch. The proposal is where I argue myself out of work as often as into it.
Review before archive
Before a change becomes history I open a fresh session and have a subagent run an adversarial review and a ponytail pass over the diff, in new context so it argues with the work instead of defending it. It earns its keep: one pass caught a spec that dropped a section still referenced elsewhere; another caught an implementation that quietly scoped a removal to one page when I wanted it gone everywhere. Only once it comes back clean do I archive and sync.
By the time the A content compiler that runs at build time was written, the hard questions (draft notes, colliding URLs, missing git history) were already answered on paper, and the code was mostly transcription. That is why the loop earns the evergreen tag, and why a project like This garden, and how it’s built stays honest about scope: a proposal names its non-goals, so what I left out was left out on purpose.