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.
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.jscalls 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.jsruns the project'svitestsuite vianpm testinside the container, and a subtask that authors a router can't promote without a passing test that covers it.
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:
- Planning — analyzing 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.
- Finishing — reviewing 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.