Wilden

Plans as Execution Engines: Driving Hundreds of PRs From a Single Document

By Robin Dowling · 3 months ago

The hardest part of migrating Wilden's backend from Node to Rust wasn't the Rust code. It was continuity. The migration ran across four weeks and hundreds of work sessions, most of them driven by an AI agent that starts every session with no memory of the last one, many of them my own fifteen-minute check-ins from my phone. There was no internal context between sessions to rely on. Chat logs don't persist, memory doesn't transfer, and "I'll remember why I did that" is a lie you tell yourself once per week.

The thing that made it work was treating the plan document as a program: not a to-do list, but an execution engine and a state store that any session, human or AI, could pick up cold and run one safe step of.

The mental model: stateless worker, durable shared state

Think of it as a CPU and memory.

  • The session is the CPU. Ephemeral, no persistent registers. It executes one instruction, one unit of migration, and halts.
  • The plan doc is the program and the memory. It holds the instruction stream (what to do), the program counter (a "find the next task" router), and all persistent state: the status of every module, every decision made, every deviation recorded.
  • The repo is the heap, the durable artifacts the instructions mutate.

A session becomes one tick of a loop: read state → select next task → execute → verify → present → (human gate) → write state back → halt. Nothing of consequence lives in a session's head. If it isn't written in the plan or the repo, it didn't happen.

That single property, durable state plus a stateless worker, is what makes the whole process resumable (any session starts cold), parallelisable (independent lanes run at once), auditable (the plan's own git history is the migration's logbook), and deterministic (the same starting state routes every session to the same next task).

What goes in the plan

A plan that can run itself needs a few specific ingredients:

  • A cold-start preamble, the bootloader. Assumes zero prior context: where work happens, what infra already exists, how to confirm the environment is green, where to find the next task.
  • A tiny status vocabulary: [ ] not started, [~] in progress, [x] done. The set of all symbols across all modules is the migration's progress, at a glance.
  • The unit inventory, a dependency-ordered checklist of every module, each carrying its status symbol and a short landing note. This is the state store proper; the row is where a unit's state and history live.
  • A "find the next task" router, a deterministic protocol: scan for the first [ ], skip anything with an unmet dependency, and you land on exactly one next task with no judgement call.
  • An authoritative per-unit procedure, a fixed, numbered recipe every module follows, ending in a verification gate (type-check, both test suites, lint, build) that must pass before "done".
  • A decision log: cross-cutting choices recorded as short, numbered, immutable ADRs, so independent sessions stay consistent instead of contradicting each other.

The session lifecycle

One session, start to finish, is a loop with an inner review-until-clean loop and two terminal gates:

  1. Base on a fresh copy of the target branch and verify it before anything else.
  2. Find the next task via the router; confirm its predecessors are done.
  3. Implement and review, repeatedly. Run the per-unit procedure, then the full gate and a fresh critical review of the whole diff. Fix the real findings, refute the rest in one line, then re-run the gate and a fresh review. Repeat until a review pass finds nothing. "Tests pass once" is not the bar; "a fresh review finds nothing" is.
  4. Rebase on the latest target and re-run the whole gate.
  5. Gate: the automated checks must be green as a unit.
  6. Present: open the PR with what changed, why, and exactly how to test it by hand. Crucially, update the plan's own state in the same PR: flip the symbol, add the landing note, record any decision.
  7. Pause, then merge: a human manually tests against those instructions and merges. One PR per session. Then halt.

The next session repeats from step 1 against the new state. No handoff conversation is needed. The handoff is the committed plan plus the merged PR.

Fold the prompt into the plan

Here's the operational punchline. Because all standing context lives in the plan, the per-session kickoff prompt collapses to a single, identical, copy-pasteable line that only ever changes two pointers:

Work the plan in docs/plans/<plan>.md. Base everything on a fresh origin/<base> (verify HEAD == origin/<base> before doing anything) and follow the plan's session protocol. Then stop.

Anything a prompt would otherwise repeat every time (the setup, the find-next-task router, the gate, the review-until-clean loop, the rebase step, the write-back of state) lives in the plan, not the prompt. If you find yourself writing a long prompt, that's the signal: the missing instruction belongs in the plan, so the next session inherits it for free. A long prompt is per-session memory by another name, and per-session memory is exactly what this method abolishes.

This is what let me run the migration the way I actually did: offline sessions from my phone, kicking off that one line whenever I had a spare moment, then reviewing and manually testing the results at night. The plan remembered everything so I didn't have to.

Scaling out: many sessions at once

Because all durable state lives in the doc and the repo, never in a session's head, many sessions can run concurrently. The whole scheme rests on one safety rule:

A set of work is safe to run in parallel iff every member touches a disjoint set of units, stays in the same risk class, shares no unmet dependency, and makes at most one edit to any shared seam.

To make that hold, classify modules by blast radius: safe lanes (peripheral, low fan-in, parallelise freely), caution lanes (touch a sensitive seam, at most one per batch), shared seams (files many units co-edit, only one editor at a time), and hot units (the behaviour-changing endgame: a single serial track, never parallelised, but allowed to run alongside the safe lanes because the two never collide in code).

The plan doc is the one file every session co-edits, since each flips its own status row. Those collisions are expected and benign: you resolve them mechanically on rebase by re-applying your own row. You never design batches around avoiding plan-doc conflicts; you design them around the one rule.

Truth-maintenance: a stale plan is a bug

There's one discipline that holds the whole thing together: the plan must always read true. Because sessions act on what the doc says, inaccurate prose isn't untidiness, it's a defect that routes the next session into wrong work. So every session corrects stale claims on sight, updates the docs that the change touched, and drains any deferred work into a single durable backlog with a falsifiable "done" check. The same rule applies to deliberate non-changes: "we looked at X and chose not to touch it, because…" gets recorded too, so a future session doesn't re-litigate a settled decision.

What it prevents

Every failure mode of a long, multi-session migration maps to a missing ingredient:

  • Lost context between sessions: the state store plus the cold-start preamble.
  • Sessions contradicting each other: the decision log.
  • Premature, unsafe cutover: phases with hard gates and the bridge/dual-coverage pattern.
  • "Green once" shipping subtle bugs: the review-until-clean inner loop.
  • Acting on stale info: truth-maintenance.
  • Parallel sessions colliding: the one rule for safe concurrency.

The migration shipped across hundreds of pull requests, and I never once had to reconstruct "where was I?" The plan always knew. That's the real product here, not the Rust, but a way of working where the document is the source of truth and the sessions are interchangeable. The full playbook lives in the repo as a template you can lift for any large, incremental migration.

Same seed, same world. Same plan, same next step.