Skip to content
EngineeringOctober 26, 2026

How agents ship AnyRouter: the merge and deploy loop

Coding agents write, review, merge and deploy most AnyRouter changes. In the week of 19 to 25 October 2026 that loop merged 346 pull requests. This first post in the series shows the loop: a signed local CI gate, one reviewer per commit, one merge script, and gated deploys about once an hour.

Agents do most of the shipping

AnyRouter is a small team and a lot of coding agents. Agents pick up issues, write the fix in their own git worktree, open a pull request, review each other's pull requests, merge the approved ones and deploy. The owner sets the rules and makes the calls that only a person should make.

This series is about how that works in practice: what we automated, what broke, and what we changed after it broke. Every number in it comes from our git history, the GitHub API or the scripts in the repo.

In the week of 19 to 25 October 2026, 346 pull requests merged into main. The busiest day had 71 and the quietest had 29. In the three weeks from 5 to 25 October, 1,111 merged.

That volume only works if every step from a pushed commit to a live Worker is a script with a gate, not a person clicking buttons. This post walks through that loop.

The loop at a glance

flowchart TD
I["Issue"] --> L["Agent lane in its own worktree"]
L --> PR["Pull request"]
PR --> CI["pnpm local-ci signs the commit"]
CI --> RV["Fresh reviewer agent signs the same commit"]
RV -->|"changes requested"| L
RV -->|"approved"| M["merge-pr.sh squash-merges"]
M --> Q["Merged changes wait for the next deploy"]
Q --> G{"Memory, disk and lease gates"}
G -->|"not yet"| Q
G -->|"clear"| D["Bookmark, smoke, pnpm ops deploy, smoke"]
D -->|"new smoke failure"| R["Roll back the Workers it uploaded"]
One pass of the loop. A new commit on a pull request starts over at the CI gate, because both signatures belong to one commit.

The rest of the post takes each box in turn.

GitHub Actions is off, so CI runs on our own machine

We turned GitHub Actions CI off to save money. Only the release workflows still run there. The gate moved to a local command, pnpm local-ci <pr> (scripts/local-ci.ts), which first landed on 4 October 2026.

It runs the same checks the old workflow ran, one at a time, because the machine has 15 GB of memory and cannot take parallel builds. 18 repo guards always run. They cover things like migration numbering, schema drift, app import boundaries, generated config that is out of date, and the format of the pull request body. The rest is scoped to the change: biome on the changed files, only the typecheck programs the change can reach, and vitest related on the changed code.

Two details make the result trustworthy.

  • The checks run on the pull request merged onto the current main, not on the branch alone. An old branch would otherwise skip a check that was added to main after it branched.
  • A pass is posted as a local-ci commit status by one bot account. The status names the gate count and the sha256 of the log, so a green result points at the exact output that earned it. A status from any other account does not count.

A real status from one recent pull request reads 41/41 gates passed (scoped, on main 3e175c42). The count changes with what each pull request touches.

One reviewer per commit

CI proves the code builds and the tests pass. It does not prove the change is right. So every pull request also gets a review from a fresh agent session that did not write the code. It reads the diff against our validation checklist, reruns what it doubts, and either approves or sends the change back with a precise list of fixes.

The approval is bound to a commit, not to the pull request. scripts/herdr/approve-pr.sh takes the pull request number and the exact commit the reviewer read. If someone pushed while the review ran, the script refuses, because the live head is no longer the code the reviewer saw. On success it posts a review status on that commit and adds the review:approved label.

Of the 346 pull requests merged from 19 to 25 October, 342 carried review:approved.

One way to merge

Every merge goes through scripts/herdr/merge-pr.sh. It refuses release pull requests, which the owner merges by hand. It refuses a head without a signed local-ci pass. Then it runs check:pr-merge-safety, which also wants the signed review status and the approval label on the current head, and refuses a pull request that carries review:changes.

Only then does it squash-merge, with --match-head-commit, so a push that lands during the merge fails the merge instead of shipping unreviewed code.

The script exists because of a bug in an earlier version of this step. The merge check was piped into another command, and a shell pipeline returns the exit code of its last command, so a failed check still let a red pull request merge. Now the check runs alone, and the script has a comment that says why.

Deploys, about once an hour

Nothing deploys on push. A polling loop (scripts/gh-loop/) checks GitHub every 3 minutes. When changes have merged since the last deploy and at least 30 minutes have passed, it starts a deploy lane. The lane waits until the machine has at least 5.5 GB of available memory and 20 GB of free disk, and it runs at most 3 heavy lanes at once.

The deploy itself follows a fixed order.

  1. Check out a freshly fetched main in a worktree that only deploys.
  2. Record a D1 Time Travel bookmark, so the database can be restored to that minute.
  3. Run the production smoke test to get a baseline.
  4. Run pnpm ops deploy. It takes a deploy lease, so two machines never deploy at once. It deploys only the Workers whose code changed since the commit each one runs now. It refuses to deploy a main that is older than what is live.
  5. Run the smoke test again. A failure that was not in the baseline rolls back each Worker the deploy uploaded.

Step 2 used to be a full database export. We dropped it after an export took the database offline. A D1 export blocks every query for 2 to 4 minutes, and the API failed for that whole time. The bookmark costs no downtime. The next post covers that incident and the rule it became.

On 26 October, the deploy log on one of our machines shows 15 successful deploys and 2 failed ones between midnight and 17:00 UTC. Across the 18 regular deploys it logged as successful on 25 and 26 October, the median run took 520 seconds. A full run touched 17 Workers. Other machines deploy too, so this is a sample, not a total.

What the loop will not do

The loop skips some changes completely and leaves them to the owner:

  • Pull requests that touch database migrations, billing and credits, auth, API keys and stored provider keys, or legal pages.
  • A migration that drops, renames or rewrites data. It needs the owner's approval and a backup. Additive migrations apply on their own.
  • Release pull requests, drafts and dependency bumps.
  • Anything that talks to a customer.

Each skip rule is one row in a table in scripts/gh-loop/rules.ts, and each row has a test that fails if someone removes it.

What is next in the series

Next, we cover how incidents become guards: the database export outage, a stash shared across worktrees, and merges that reverted newer code. After that come per-Worker deploys, running many agents on one 15 GB machine, and how we find free models.

Route your first request in 2 minutes

Start free with your own keys, or top up and pay per token. Get $4/mo in credits and free models on Go — $2/mo, or free when you donate a provider key.

Start free