The build loopPart 1 of 1

Building in stages: scope to a usable core, verify each step, show the work

An AI agent builds an app in a single pass — plan, write, wire, check at the end. For a small app that's the right shape; for an ambitious one it's opaque, all-or-nothing, and prone to drift over a long horizon. Three design choices reshape it: the planner scopes the request to a usable v1 core and banks the rest as a tracked backlog; every step earns "done" against objective checks — a contract probe on the live app, the project's own tests in-pod, a regression gate before prod; and each phase streams onto an activity feed so a minutes-long run stays legible. Here's the pipeline and the mechanics.

Building in stages: scope to a usable core, verify each step, show the work

The way an AI agent builds an app is a single pass. It gets a plan, a large context window, and a set of tools, and it runs until it decides it's done — writing files, running SQL, wiring routers, verifying at the very end.

For a small app, one pass is exactly the right shape. For an ambitious one — an LMS, a marketplace, a project-management tool — one long undifferentiated pass has three properties that don't scale:

  • It's opaque. A spinner can't distinguish a healthy fifteen-minute build from a stuck one. There are no milestones to report because the run has none.
  • It's all-or-nothing. A failure three-quarters of the way through leaves nothing you can open — not a smaller app, an empty one.
  • It drifts over a long horizon. A bigger context window mostly improves retrieval — the model can see more of the codebase — not execution horizon, the number of correct steps it can chain before it loses the thread. With verification deferred to the end, nothing catches drift while it's cheap to fix.

The robust response isn't a larger context window; that bets everything on execution horizon never degrading. It's to change the shape of the work: scope the build to a usable core, verify each step as it lands, and narrate the run so it's legible. Three ideas, one section each.


Scope: a usable core, then a tracked backlog

When a request is large, the planner doesn't try to fit the whole thing into one build. It designs a v1 core — the smallest version that's genuinely usable — and records everything else as a structured deferredStages backlog attached to the plan, rather than as prose it will forget.

That has an immediate effect and a structural one.

The immediate effect is the one you see: a preview you can open, click through, and use in minutes, instead of a single enormous build that's worth nothing until it's complete. The backlog is what lets the app, once your v1 is running, offer to build the next stage — surfaced on the plan page as Upcoming Stages and in Telegram, per version. Accept, and it builds that stage forward from what you already have.

bank the rest

accept → build next stage

fork forward

ambitious request

plan: scope v1 core
+ deferredStages backlog

build v1 core

running preview
committed · on a CoW branch

deferred stages

build stage 2

The structural effect is subtler, and it's aimed straight at the long-horizon problem. A v1 core is a shorter plan, and a shorter plan is a shorter execution horizon — less distance for the agent to drift before there's a working, checkable result. Scoping isn't only a UX nicety; it's the cheapest way to keep the run inside the range where the model stays coherent. Tighter scope is a more durable bet than a bigger context.

Expanding in stages is safe because each version is a real, restorable point. Every generation is committed to the project's git history by commitGeneration, and every project's database is a copy-on-write timeline, so "your app after v1" is a durable snapshot of code and data. Building the next stage isn't an in-place mutation you can't undo — it's a step forward from a version you can always return to.


Verify: every step earns "done"

Scoping tighter only helps if each step is actually checked; otherwise it just makes the drift arrive sooner. So verification doesn't wait for the end of the build — it runs on every step, against the running system.

Every project is born testable. The scaffold templates ship a test runner and a browser-like environment already wired up — vitest, jsdom, @testing-library — baked into the base image. Tests can be written and run from the first step, inside the real app pod, never a simulation.

As the build proceeds, each subtask is judged by verify-pipeline.js before it can be marked done. Two of its layers do the load-bearing work, and both exercise the deployed app rather than a mock:

  • A contract probe hits the live app. When a subtask adds an API route, contract-tests.js calls the running pod through its own dev server (the tRPC procedures are reached via the Vite proxy on port 3000): do the declared procedures respond without 500s or Zod errors, and do the tables they depend on exist? If the plan called for a procedure and it isn't really there, that fails — objectively, not by an LLM's opinion.
  • The app's own tests run in the pod. project-tests.js runs the project's vitest suite via npm test inside the container, and a subtask that authors a router can't promote without a passing test that covers it.
app pod (live)verify-pipelineproducer (agent)app pod (live)verify-pipelineproducer (agent)"done" is earned by a check that ran — not a file on disksubtask complete? (todo_write)contract probe · :3000 (procedures respond · tables exist)npm test (the app's own unit tests, in-pod)resultspassing · or reopened (fix, then re-check)

The producer also runs these checks inline, via run_acceptance_check, before it claims a subtask is finished — so the gate isn't a surprise at the boundary; it's the same objective bar the agent is working against the whole time. "Passing" is earned by a check that actually ran, never by a file existing on disk. That distinction is the whole game: the gap between "the code is there" and "the code works" is the gap between a demo and a product.

Because the app's tests accumulate as it grows, there's a final backstop before production. deploy-fullstack.js re-runs the whole suite as a release gate; a red suite blocks the deploy. Adding a stage can't quietly break what an earlier stage already got right — and none of these verdicts come from a model grading its own work.


Narrate: the build reports as it runs

A build with real, checkable steps has something truthful to say about itself, so the run is streamed rather than hidden. Each phase emits typed events onto an activity stream (activity.js) — Server-Sent Events to the web app, pushed through to Telegram — grouped into the phases you see while it works:

  • Planninganalyzing your idea, mapping features, writing the build plan. These are the planner's real steps, streamed as the plan is composed.
  • Building — work reports as it happens; file writes and SQL runs are grouped under the active step with a running count.
  • Finishingreviewing the build, deploying, starting everything up. The finalize phase is a sequence of real operations — the release check and bringing the app up — so it narrates each rather than going dark.

One detail matters more than its size. Telegram's "typing…" indicator expires after about five seconds, but composing a plan for a real app takes one to three minutes. So during a long turn the bot re-issues the signal on a short interval and, once, sends a brief "still working" note (telegram/bot.js) — a deliberate keepalive that keeps a slow turn from looking like a dead one. The point of the whole layer isn't a progress bar; it's that every line corresponds to something that actually happened.


What this gives you

  • A usable app, fast. An ambitious request comes back as a working core plus a tracked backlog, not one deferred gamble — there's always something to open.
  • Restorable versions. Each generation is a git commit on a copy-on-write database branch, so expanding your app moves forward from a snapshot you can roll back to — code and data together.
  • Checked, not claimed. Objective checks run per step and again as a release gate; "done" means a contract probe and the app's own tests passed, not that a file was written.
  • A legible run. The build streams its real phases, so a minutes-long agent run is something you can watch rather than wait on.
  • Model-agnostic by construction. None of this depends on execution horizon improving. A better model simply does more inside the same disciplined shape — scoped work, checked steps — so the platform is never one regression away from silent, unverified drift.

The shape of it

Stop thinking of a build as a single act of faith that either works or doesn't, and start thinking of it as a sequence of scoped, checked, narrated steps. Scope turns an ambitious request into a usable core with a shorter horizon; objective verification turns "the agent said it's done" into "the running app passed the checks"; narration turns an opaque wait into a legible run. The build was always doing this work — the change is that now it's bounded, verified, and visible while it happens.

Build on the platform these posts describe.

Describe your app in plain English — Adorable writes the code, sets up the database, and ships it live.

Start building free