The Deploy Is the Loop Now

For a couple of days I steered Claude Code on Grit Mile's onboarding flow and watched it crawl. It couldn't seem to nail basic fixes, and my first instinct was the usual wrong one — the model isn't good enough at this yet. The model was fine. The problem was the loop I'd put it in: to find out whether any change worked, the agent had to deploy to AWS and read CloudWatch. I'd wired a cloud deploy into the middle of the build-test-iterate loop, and I'd never steered it toward a way to test locally.

Grit Mile is my AI running-coach side project — serverless AWS, a Lambda behind an API Gateway WebSocket, Aurora DSQL, Bedrock underneath. Onboarding is a conversation: the coach talks to a new athlete, gathers what it needs, and hands off to plan generation. It kept breaking in small, maddening ways — a field the coach should have captured but didn't, a question it re-asked instead of moving on.

Those bugs were slow to fix for an architectural reason. The function that runs a single intake turn — the coaching brain of the conversation — only existed inside the deployed chat-stream Lambda. There was no way to run it on a laptop. So every fix went: change the code, sam deploy, wait, click through the chat in a browser, watch it misbehave, scrape CloudWatch, guess, change again. Ten to twenty minutes a lap. The code change the agent was actually iterating toward took it maybe five.

The deploy didn't get slower. The build got faster.

Faster deploys have always meant tighter feedback loops. There was a time when "the cloud" was the fast option — you shipped to EC2 in minutes instead of racking a server in your own closet over weeks. The rule never changed; only the arithmetic did.

When a human wrote the feature, that human spent maybe five days on it. A 15-to-20-minute deploy dropped into that loop is a rounding error. It doesn't register as friction — against five days of typing, twenty minutes of CloudFormation feels like a tight loop. You'd never think to optimize it.

Now an agent writes the same feature in minutes. Frontend polish on a file or two lands in three to ten; a cross-stack change with tests, fifteen to twenty-five. Those are my lived numbers from months of working this way — measured, not benchmarked. Against a five-minute build, that same unchanged 20-minute deploy is now most of the loop. It dominates by three or four times.

The same deploy earns the opposite verdict now, because the build collapsed. Coding velocity was never the metric. Iteration velocity is — the cycle time from make a change to see the result to know the next step, and I've written before that it's the ratio that compounds. Agents collapsed the numerator to almost nothing. Whatever's left sitting in the loop is now the whole cost. If that thing is a cloud deploy, the cloud deploy is your dev loop — and you've handed most of your agent's speed advantage back to CloudFormation.

This is an orchestration failure, and the default agent walks right into it

I keep coming back to one diagnostic. When an agent fails at something it genuinely needed to do, the failure is almost never that the model can't. It's a context failure or an orchestration failure — the model lacks the knowledge in scope, or the workflow won't let it use a capability it plainly has.

This one is orchestration, cleanly. The coaching brain worked. The model could run an intake turn correctly. What was broken was the workflow around it: every diagnosis had to round-trip through a deploy and a log scrape, because the capability was trapped in a Lambda that only ran in the cloud. Capability fine; workflow throttling it.

This is what Claude Code does by default. Point it at a serverless AWS app and tell it to build, test, and iterate, and it will test the only way the architecture allows — deploy, click, read logs. It won't stop to build a local loop unless you steer it to. Default Claude Code, building on AWS, will make every wrong guess cost an afternoon. This isn't a trap I stumbled into once. It's the path of least resistance for any agent on this stack. Don't let yours take it.

The fix is a seam, not a rewrite

The move is ports and adapters — plain good engineering, nothing exotic. What's new is the cost math. The instinct is that the adapter, the local shim, is overhead: extra code you write to avoid the "real" thing. That was true when a human wrote the shim by hand. With an agent, the shim is cheap and the deploy is the expensive part. Once you internalize that inversion, building the local seam stops looking like a detour and starts looking like the obvious first move.

So I had Claude pull the coaching brain out of the Lambda. runIntakeTurnCore(input, deps) now lives in core and takes its dependencies as injected ports — a transport sink standing in for API Gateway's postToConnection, injected repos and clock and LLM client, a plan-gen trigger port in place of the direct Lambda invoke. Behind those ports, local runs get file-backed adapters, a replay Bedrock shim, and an in-memory sink that just collects what the coach would have streamed. A one-command CLI drives the real function on a laptop with zero AWS in the picture.

Production didn't fork. It injects the AWS adapters behind the same ports and calls the same core function. Shared logic, swapped adapters — that's the whole trick. The coaching brain has exactly one implementation, and both prod and the local loop run it.

If the local loop doesn't run the real code, it's theater

There's a real objection: a local harness that runs a fake proves nothing. You end up debugging a simulation of your app instead of your app, and the green result is a lie.

The seam has to be built against that explicitly. You shim the environment — repos, clock, LLM client, transport. You never fork the logic. There is no local-only path that decides what the coach says or when it finalizes. A guard-rail test asserts that both the deployed worker and the local path funnel through runIntakeTurnCore; if they ever diverge, the test breaks. If a local turn and a prod turn could disagree about what the coach would say, the harness is theater and a green local run tells you nothing.

The tell that the WebSocket scaffolding was pure transport and not brain is that it simply doesn't exist locally. API Gateway's WebSocket integration has a hard 29-second timeout, and a full intake turn runs closer to 40, so production needs an ack-fast self-invoke to dodge the timeout and a client keepalive ping to defeat the idle disconnect. On a laptop, an HTTP request that streams for 40 seconds is fine — no gateway timeout, no idle socket. That scaffolding was cloud plumbing, not coaching logic, and dropping it locally changed nothing about what the coach decided. That's how you know the seam is cut in the right place.

The first local drive caught what a green deployed suite missed

The moment the local loop worked, Claude used it — on its own, without me asking. I'd steered it to build the seam and stepped away. When I came back, local intake was running and Claude had already used it to isolate a bug that a fully-green, fully-deployed test suite had been hiding.

The suite was green because it tested against a stub model that emitted perfect tool calls. Onboarding captures each answer when the model emits a record_* tool call — but the real model would rather talk. Given the choice between a conversational sentence and a typed tool call, it picks the sentence almost every time, so the capture tools never fire. Six turns of real conversation, and the coach had captured only the fields pre-seeded from the signup form — every answer given by talking was dropped. The tests had validated a model that doesn't exist. The gap was invisible right up until a live local drive put the real model in the loop.

A deploy-and-CloudWatch hunt would have burned an afternoon on that, and it's the kind of bug you misattribute — blame the prompt, the tool schema, five other things — before you see its shape. The local loop surfaced it in one conversation with the raw model output sitting right there. That's the entire argument for the seam, and it paid off the first time the agent ran it.

Local-exercisability is a service-selection criterion now

Once you've felt this, it stops being a debugging tactic and becomes an architecture criterion. Local-exercisability belongs on the same shortlist as features and price when you pick a service. A service you can drive or cheaply shim on a laptop keeps the loop fast. A service that only exists in the cloud drags a deploy into every loop that touches it — and with agents doing the building, that's often the dominant cost, not a footnote.

EventBridge is the thing to watch for: an event bus you can't really stand up locally, so any path that fires through it wants a cloud round-trip to test. Grit Mile's local loop works precisely because it fakes that bus with an in-process one behind a port. Every time you choose a service like that, you're signing up to either build a faithful local shim or eat a deploy in your loop forever. Sometimes the service is worth it — but that cost belongs in the decision, not discovered later.

A WebSocket Lambda that only runs deployed — the exact thing I built — is debt. Skipping the seam front-loaded weeks of slow loops, until the agent and I went back and paid for it.

I work at AWS, on the team that owns the documentation, so I'll say this plainly instead of dancing around it: "can an agent exercise this on a laptop" is becoming a first-class question, and the platforms that answer it well have an edge agents feel immediately. Naming the gap is how it gets closed.

Build past the harness

The rule is blunt: if you have to deploy to a cloud provider to find out whether the thing works, you're wrong. Build the local loop. This is what building past the harness looks like in the small — you notice where the tools stop, and you construct the missing piece instead of routing around it one more time.

I promoted that straight into my global setup, so every agent I run now inherits it — never put a cloud deploy inside the build-test-iterate loop. Every code path should be fully exercisable locally. When the code leans on something the cloud provides, shim it: a local Postgres, an in-process event bus, a replay-cache LLM, a direct HTTP call where API Gateway would sit. Swap the adapter, keep the use-case.

The deploy was always in the loop, for any app that needs a deploy to prove it works. It just didn't matter when a human spent days writing the thing the deploy was gating. Now the human writes intent and the agent writes the code in minutes, and the deploy is the slowest thing left. The deploy didn't change; the build around it collapsed, so the deploy went from rounding error to the main event.

That's why the deploy is the loop now. Build the one that isn't.

← Back to archive