← Devlog

m-001

The rule we wrote in the morning and broke the same night

What was true before

There was a repository and an intention, which is the least useful pair in software.

The intention was to build a persistent multiplayer text adventure in public. The repository had a Node toolchain and not much else. Between those two things sits every decision about what to build first, why, and how anyone would know whether it had been built — and none of that had been written down anywhere.

The usual answer is a tracker. Open an account, make a board, file tickets, drag them right. It is the default because it works, and because everybody already knows how to use one.

We did not do that, and this is the argument for why, followed by the part where the decision cost us something on the very first milestone.

The decision, and the option that lost

The plan lives in the repository, as files, on the same branch as the code it describes.

Requirements in docs/requirements.md. Milestones and tasks as markdown in .backlog/. Every task carries acceptance criteria and its dependencies. Every requirement names the milestone that satisfies it, and every milestone names the requirements it satisfies — both directions, so a requirement with nobody delivering it is a query rather than a thing you notice a year late.

The option that lost was a tracker, and it loses on three things.

A plan you cannot diff is a plan nobody reviews. In a tracker, a scope change is somebody editing a ticket. There is an audit log, technically, and nobody reads it. Here, changing scope is a commit on a branch with a pull request, and it shows up beside the code as + - [ ] #3 the binary prints the opening and exits 0. It gets the same review the code gets, because it is in the same diff.

The plan and the code drift the moment they live apart. Two systems describing one project disagree, and the disagreement is invisible until someone notices the ticket said one thing and the branch did another. One store cannot disagree with itself.

An agent in the repository can read a file. Most of the work here happens with an AI assistant that has a checkout and a shell. A tracker means an API call, a token, a rate limit and a schema that changes under you. A markdown file means cat. This is not a small consideration when it is how most of the work actually gets done.

Being fair to the option that lost: a tracker gives you notifications, a board your colleagues can look at, an app on a phone, and access for people who do not have a checkout. For a solo project built in the open, none of those is worth the drift. For a team of thirty, the answer probably flips.

What shipped

Four artefacts, each doing one job.

A use case says who wants something and what failure looks like — because a requirement with no failure mode attached is a wish:

docs/requirements.md

### UC-4 Ship a change to the site

**Persona:** P-1
**Trigger:** The site's copy, an install command, or a link needs to change.
**Success:** Merging is the deploy. No console clicking and no runbook, so the site can be
corrected as readily as it is written.
**Failure:** Deploying depends on someone remembering a sequence of steps, so it is done rarely
and nervously, and the site drifts out of date rather than risk it.

A requirement states one thing that must be true, cites its use case, and names at most one milestone. If it needs two milestones, it is two requirements — which sounds pedantic until you try to answer “is this done?” about a requirement half-delivered in two places.

A task is a unit of work with acceptance criteria, filed before any code is written:

.backlog/tasks/task-010 - featcli-compile-to-a-standalone-binary.md

---
id: TASK-010
title: 'feat(cli): compile to a standalone binary'
status: Done
milestone: m-002
dependencies:
  - TASK-009
---

## Acceptance Criteria
- [ ] #1 a single executable runs without Node on the machine
- [ ] #2 the binary prints the opening and exits 0
- [x] #3 macOS arm64 and x86_64 and Linux x86_64 targets all build

Those two open boxes are real, and they are still open. CI compiled the binaries and attached them to a release; nobody ran one. The format’s only virtue here is that it makes that visible instead of letting “the task is done” absorb it.

And the rule that ties the plan to the history:

AGENTS.md

- **A task is exactly one commit.** That is what makes a plan reviewable: the task list is the
  diff that does not exist yet. If a task cannot land as one coherent commit it is not one task —
  split it. If two tasks must always land together, they were one task.
- **The task title *is* the commit subject**, copied verbatim, so every task title must
  already be a valid Conventional Commit: `type(scope): imperative summary`.

That last rule is the load-bearing one, and it is not really about tidiness. If a task’s title is its commit’s subject, then the mapping from plan to history needs no database, no ticket reference, no commit trailer. You find the commits that delivered a milestone by matching task titles against commit subjects. The plan becomes machine-readable for free.

Which is how we found out we had broken it.

What it cost

The rule entered AGENTS.md at 11:38 on 21 August, in a commit called chore(backlog): install Backlog.md conventions and store.

Milestone one was delivered that night. Eight tasks — scaffold the site, add the OpenTofu stack, the front door page, the Pages project and DNS, the root scripts, the deploy workflow, the architecture docs, the Astro scripts. They landed as three commits, at 23:14, 23:23 and 23:24. Ten minutes apart, in a batch, at the end.

e728517 feat(web,infra): scaffold Astro site, OpenTofu stack, and root scripts
545a893 feat(web,infra): add front door page and declare Pages project with DNS
506805a ci(web): deploy site on merge to main, update architecture docs

None of those subjects is any task’s title. The scopes are comma-joined — web,infra — which no task uses. Three commits carry eight tasks, reworded to describe what the batch contained rather than what any one task asked for.

Eleven hours and thirty-six minutes between writing the rule and breaking it.

Nothing failed. The site deployed, the tasks were genuinely done, the acceptance criteria were genuinely met, and every check was green. The rule looked like bookkeeping, and skipping bookkeeping has no immediate consequence — that is the definition of bookkeeping.

The consequence arrived later, when a tool tried to read the history. The content pipeline resolves a milestone’s work by matching task titles to commit subjects, exactly as the rule promises. Run against milestone two, it matches four of six. Run against milestone one:

TASK-001 NO-COMMIT feat(web): scaffold the astro site
TASK-002 NO-COMMIT chore(infra): add the opentofu cloudflare stack
TASK-003 NO-COMMIT feat(web): add the front door page
TASK-004 NO-COMMIT feat(infra): declare the pages project, domain and dns
TASK-005 NO-COMMIT chore(infra): wrap the opentofu commands in package.json scripts
TASK-006 NO-COMMIT ci(web): deploy the site on merge to main
TASK-007 NO-COMMIT docs(web): record the site and its deployment in architecture
TASK-008 NO-COMMIT chore(web): wrap the astro commands in package.json scripts

Zero of eight. The drafting command refuses rather than writing a confident article about an empty diff, which is the right behaviour and no consolation at all: milestone one cannot be written up from its own history, because its history no longer says which work was which.

So this episode is not the one that was planned. The plan said milestone one’s episode would be about the website — the Astro site, the OpenTofu stack, the deploy workflow, with the code that shipped them. That episode cannot be drafted. What can be written is this one, about the method, which needs no diff to explain and has milestone one sitting in it as the worked example of what the method costs when you skip it.

It is also written by hand rather than drafted, and the episode records that: every unit carries "by": "hand" with the reason, rather than a prompt version it never had.

What it makes possible

Every milestone since has held the rule, and the mapping does what it promised: a milestone’s commits are found by asking the plan, and an episode’s snippets come from the diff that actually shipped rather than from anyone’s memory of it.

The rule was cheap the whole time. It cost the discipline of stopping at the end of a piece of work and committing it before starting the next one. Milestone one is the record of what that costs when you skip it, and it is a permanent record, because you cannot go back and re-commit a history that has already been pushed.