The Agent Loop That Ships Its Own Code
How Starship's coding agents go from a planned task to a merged pull request without a human driving each step, and the gates, retries and postmortems that make that safe to leave running.
6 min read
Getting a model to write code is the easy part. The hard part is a loop that can take a task, change a real repository, prove the change is good, and land it, all without someone watching. When it can't do that, it has to stop cleanly and say why.
That loop is the heart of Starship. Here's how it's shaped, and what broke on the way to trusting it.
Two planes, one rule each#
The system is split in two, and I keep the line between them sharp.
The control plane is a web app. It plans work, stores tasks and their state, and enqueues runs. It does not run the coding agent.
The execution plane lives on a coordinator Mac in my studio. It leases a queued job, runs the agent loop against the model gateway, applies the gates, and reports back through a webhook.
There's a third plane for context, which I'll cover in another post. The split matters because each side can fail without corrupting the other. A planning bug can't push code, and an agent bug can't rewrite the plan.
Inside the execution plane, one rule matters more than any other: the worker owns git, not the model. The agent gets a small, explicit toolset: list a directory, read a file, search, edit, write, run a command, and see the diff. Branching, committing, pushing and opening the pull request belong to the worker. Git publishing commands are blocked inside the agent's shell. The model proposes a change, and the system decides whether that change becomes a PR.
A loop with a plan and a contract#
The coder doesn't freestyle. It moves through a small state machine: plan, read, edit, verify, self-evaluate, submit, escalate.
It has to submit a structured plan before it edits anything. The plan is validated against a schema and names:
- one to twelve target files
- a change summary
- the kind of diff it expects
- the commands that will verify it
Each task also carries a work mode, such as code, document, config, analysis, mixed or ops, and every mode has an artifact contract. A code task that produces only prose fails its contract.
The guard I lean on most is the simplest one: no diff means no PR. An empty or no-op patch is a failure with a name, not a quiet success.
Deterministic gates, bounded retries#
Everything after the model is deterministic where it can be. Lint, typecheck, tests and browser journeys run before any model reviews anything. The retry policy is explicit, and it lives in code:
maxAttempts: 3,
retryIf: ["lint_failure", "typecheck_failure", "test_failure", "browser_journey_failure"],
escalateIf: ["touches_auth", "touches_billing", "touches_migrations",
"changes_more_than_max_changed_files", "third_attempt_failed"],
maxChangedFilesWithoutApproval: 10,
Mechanical failures get another try. Anything that touches authentication, billing or migrations, or that spreads across too many files, goes to a person. The change's blast radius is calculated from a code graph, not from the files the model happened to touch.
A separate standards agent then reviews the change against the target repo's coding standards, or a sensible default when the repo doesn't ship its own. If its output is malformed, the verdict is reject, never approve.
Across every lane, the bigger principle is a bounded ladder: generate, re-score, retry once, escalate one tier, then stop and park the item for a human. Nothing loops forever. I'd rather have a parked task than a doom loop.
What the postmortems taught me#
I write postmortems for this system the way I would for a production outage, because that's what they are. A few lessons have stuck.
Pick a canary that can't hide. My first end-to-end target was a README edit. If a simple docs change can't reliably produce a patch, larger coding tasks will fail. That canary exposed a lot:
- When the agent read files and then produced nothing, the fix was a correction pass with a tighter prompt and file-only tools. If that also fails, the task blocks with evidence.
- The correction pass was hitting rate limits because the gateway charged both passes to one generic budget. Each pass now has its own.
- Pull requests were being opened when work was created, before any patch existed. PR creation now belongs only to the worker's publish step.
- For live board updates, I rejected a three-second auto-refresh as "too half-done" and built a proper event stream instead.
Configuration you can see is not configuration that's used. A timeout was set correctly in the service environment, but the code ignored it and used a hard-coded 60 seconds. Restarting a service didn't reload newly added environment keys. A stale heartbeat made a perfectly healthy model look missing. Now I verify the loaded state, not the file.
Mixed batches aren't evidence. Several test runs were contaminated by half-applied configuration. I drain the queue, prove the runtime is in the state I think it is, and launch exactly one clean canary.
When the agent loops, look at the harness first. A hard security fix burned 24 tool calls and made zero edits, re-reading the same large file. Three changes got it through:
read_filelearned to page by offset and limit.- A loop-breaker steps in after three identical calls with a direct instruction to stop re-reading and edit.
- The scanner's own remediation guidance now travels with the task.
The same task then ran in 6 calls with 1 edit and merged on its own. Each named failure code now maps to a recovery playbook that's injected into the correction prompt, so the model gets specific guidance instead of a generic "try again."
Letting it merge, carefully#
Approved changes auto-merge, but only into a staging integration branch. Main is never touched, and promoting staging to main is a human step. Each run's base branch fast-forwards to staging right before it's used, so work always builds on the latest integrated code.
That design was tested quickly. A pull request auto-merged into staging with four failing CI checks. The merge gate had trusted the agent's own QA and the sandbox's local checks rather than the repository's real check suite, and staging had no branch protection. The fixes came in layers:
- Branch protection on staging.
- A gate in the execution plane that holds an approved change until CI is green.
- A delta gate that only blocks failures the PR itself introduced.
The delta gate taught me something too. When the meaningful checks run only on pull requests, the base branch has almost nothing to compare against, so every failure looks new. A reconciler now re-checks held work every 90 seconds and merges it once CI goes green. Genuinely red changes stay held.
The process lesson mattered most. The repository's baseline CI was broken for every pull request. I started a hand-fix branch, then discarded it. Baseline CI rot is work for the system, so it should be enqueued as a task, not patched quietly by me in a terminal. If I fix it by hand, the system never learns that it's its job.
Where it stands#
The loop runs end to end: plan, edit, verify, review, merge to staging, then report and learn. Every failure has a code, and every tool call is recorded. Some subtle tasks still hit the limits of the local model, and I'd rather say that plainly than tune the numbers.
The takeaway#
The agent isn't what makes this trustworthy. What makes it trustworthy is everything around the agent: who owns git, which gates are deterministic, how many retries are allowed, and where the system stops and asks. A model will always surprise you. The loop is there so the surprise is small, named and recoverable.
Related
- Engineering
The Art of the Rock Tumbler: Polishing Loops for Code and Agents
A rock tumbler doesn't make a stone smooth in one pass. It uses coarse grit, then finer grit, then polish, and checks between stages. That's how I build agent loops in Starship: bounded retries, deterministic gates, and lessons that are only allowed to hurt once.
Agents · AI · Architecture · Delivery7 min read
- Engineering
Nothing Goes Live Because It Exists
In my agent rig, prompts, routes, skills and even fine-tuned models all start in shadow. Promotion is a separate, evidence-backed step that a person takes. Here's the lifecycle, and why it matters more as the number of moving parts grows.
Agents · AI · Local LLMs · Delivery6 min read
- Engineering
Why I Run My Own Model Fleet
Starship runs its coding and planning agents on a small fleet of Apple Silicon Macs I own. Here is when local models beat the cloud, what it cost me in operational lessons, and where I still reach for a hosted model.
AI · Agents · Architecture · Local LLMs6 min read