Our engineering velocity was slowing because bugs and escalations were consuming roughly 30% of our time: reproducing ops tickets, tracing payments, and writing fixes. Releases slipped because the engineers meant to ship them were stuck in the queue.

So I built a cloud coding agent to absorb that work. Nineteen weeks later, about 49% of the pull requests opened across our GitHub org come from it, roughly half of its work starts from a ticket filed outside engineering, and bug and escalation work is down to 5% of engineering time.

Off-the-shelf agents are already proficient at writing code. Whether anyone adopted our tool hinged on a handful of infrastructure and interface decisions, several of which I got wrong first.

Wingspan is a payroll platform for companies that rely on contractors: onboarding, identity verification, payments, tax compliance, 1099 filing. We move money on behalf of businesses and the people they pay, so an agent with access to any of it needs a real trust model rather than a good intention.

I’m Wingspan’s Director of Product and I lead our product team, so building this was not my day job.

01

Guiding principles

I went into this build with a few key principles:

  1. Start where the work lives. If using the agent requires a new tool, people won’t use it.
  2. Return something a reviewer can judge in two minutes. Every run ships with a screen recording, screenshots, and CI results attached, so an engineer can decide whether the fix is real without checking out the branch and reproducing the bug themselves.
  3. Let the work survive interruption. Someone should be able to read a pull request on Thursday, leave a comment about work the agent did on Tuesday, and get an updated pull request back. Sessions resume with their original state instead of starting over.
  4. Grant the agent no native credentials. Everything it reaches should go through one place I can revoke, rate-limit, and audit.
  5. Make changing models a config change. I assumed the frontier would move faster than I could rebuild, so a bad model choice should cost one line of JSON to reverse.
  6. Run it on infrastructure the platform team already operates. Then approving it is a review of something familiar rather than an argument about a new category of thing.

02

Why build rather than buy

By the time I started, three good write-ups existed and I read all of them before committing: Ramp on why they built their background agent, Stripe on Minions, and Harvey on Spectre. Ramp’s argument is the one I’d borrow: your own tooling only has to work on your code.

For us that meant three things no vendor product does. Wingspan engineers test against a complex multi-service integration environment using an internal tool that swaps individual services in and out of a running stack.

Investigating a production payment issue means reading logs, BigQuery, and Spanner through a read-only impersonated identity across many services. Our ops and support teams live in a Linear ticket queue, not in a terminal.

03

The five key decisions

The harness matters more than the model

I started on Claude Code’s SDK to prototype because I was most familiar. I replaced it with headless OpenCode driven through its typed SDK.

I switched for the architecture. OpenCode runs as a server with a typed client, so I could own the session lifecycle, resume a session after a crash, stream events into my own control plane, and read the harness source when behavior surprised me.

Over four months the platform ran production sessions on eight models: GPT-5.2, 5.4, 5.5, 5.5 Pro, and 5.6 Sol, plus Gemini 3.5 Flash, GLM-5.2, and Claude Opus 4.8. Every one of those swaps was configuration. None was a rewrite.

It’s critical not to tie yourself to a single lab’s models. Costs can quickly get out of control. We experimented with open-weight models like GLM-5.2, which had similar performance to GPT-5.5 across 99 sessions for us at a fraction of the frontier price. Welding a model into the harness costs you that option when new open-source models become valuable.

Isolation: pick the primitive your platform team already runs

The first version ran on Google Cloud Workstations, because that’s what already existed in the repo when I inherited it. It worked, though it presented three specific challenges. Each session got a persistent disk I had to provision, chown, and clean up. Start time was tuned for a human opening an IDE, not for a process that needs to exist for four minutes. And the isolation was a VM per developer, which is the wrong shape when the thing inside the box is executing model output.

Fortunately, Google shipped GKE Agent Sandbox as a managed GKE add-on in April, and I moved Wingman onto it in about ten days for four reasons.

  • Per-session gVisor isolation shrinks the kernel surface available to whatever the model decides to run, which is what makes it reasonable for a support person to hand a payments bug to an agent without an engineer standing behind them.
  • The SandboxTemplate object puts the image, network policy, service account, and resource limits in one declarative file, so the security posture of every session is reviewable in one place instead of inferred from provisioning scripts, and our platform team can approve a change to it in minutes.
  • Pod Snapshots to GCS handle session resume, which deleted the persistent disk from the design and delivered goal three: a session comes back with the same branch checked out, the same dependencies installed, and the same context, so a comment in Linear on Thursday updates Tuesday’s pull request instead of opening a new ticket.
  • It runs on GKE, which our platform team already operates.

The security model: auditable, no PII, and still able to reproduce a real bug

Every action needs to be traced back to a person, because “the agent did it” is not an acceptable answer during an incident review. No customer PII can end up somewhere it doesn’t belong, which rules out the easy version where the agent pokes at production until the bug reproduces. And the agent still has to reproduce a real bug, which rules out the safer approach of working against fixtures and mocks that don’t fully simulate our stack.

Here’s how those resolve.

Auditability
Pull requests are opened by a dedicated GitHub App with narrow repository scope, and commits are attributed to the engineer who asked for the work, so review and blame land on a person rather than on a bot. Every tool call the agent makes is logged to an audit trail alongside the session record. Sessions link back to the ticket that started them and forward to the PR they produced, so a reviewer can see the whole chain, and a reviewer who left comments can check whether they were actioned.
No PII in the wrong places
Production access is read-only, through an impersonated identity issuing short-lived tokens, and exists for investigation only: logs, BigQuery, Spanner reads. The agent never writes to production and never runs tests against it. Repository contents and generated code stay in the sandbox and its snapshot, and are never copied into the control plane’s datastore, which holds session metadata and the audit trail. The sandbox network is default-deny with an explicit egress allowlist through a proxy, so data can’t leave to an unexpected destination even if something in a session decides it should.
Realistic testing anyway
Tests and reproductions run against our integration environment, using the same service-swapping tool our engineers use, so the agent exercises real service topology and real gRPC paths rather than mocks. Integration holds shared real data, so agent runs use distinctive prefixes and clean up after themselves. This is the compromise that makes the other two requirements affordable: the agent gets an environment where bugs actually reproduce, and that environment is not where anyone’s tax record lives.

Underneath all of it, the sandbox holds no long-lived credentials at all. That was not the original design. The first working version handed the sandbox a GitHub token, a model provider key, and npm credentials, which is the obvious approach and the wrong one. A sandbox running model output, installing packages, and cloning repositories is the worst place in your infrastructure to keep a secret.

Git access now goes through a smart-HTTP broker: the agent runs native git clone, fetch, and push, and the control plane does the authenticating and logs what moved. Model and MCP traffic routes the same way.

Prompt injection stops being a credential-theft problem and becomes an allowlist and rate-limit problem.

The property I wanted was a single revocation point and a single audit log. When a session goes wrong, the blast radius is the broker’s permission set, which I can inspect and change in one place, rather than a token I have to hunt for across pods and snapshots.

The UX surface

The alpha shipped in mid-April as a CLI: wingman spawn --task "...", wingman status, and wingman watch, plus a web dashboard. I announced it to the engineering team and asked them to break it.

In the whole period since, the CLI accounts for 254 sessions and the web dashboard 76, and that usage tracked the handful of engineers I had asked directly.

Five weeks later I wired Wingman into Linear as a native agent: it picks up assigned issues, reads attachments, holds a conversation in the issue thread, and links its pull requests back to the ticket.

Wingman session showing an investigation, completed tool steps, linked issue details, cost, evidence controls, and a final verdict with next steps.
A completed Wingman investigation. The session keeps the issue, model, cost, evidence, findings, and follow-up work in one reviewable place.
68%

of 1,035 sessions since late April were started from Linear. Of the sessions tied to a specific issue, 64% came from operations and about 70% from queues outside engineering.

Product, support, and payment operations staff get the most out of this, and I built it for engineers. They were already writing detailed bug reports. Now they get an investigation, and often a pull request, back on the same ticket, and can push it further by replying in the thread. One of our payment ops leads pastes Wingman’s findings straight into PagerDuty incident threads during live incidents.

Adoption follows the surface where the work already exists, and the interface has to be easy enough to disappear. Installing a CLI, authenticating a cloud SDK, and learning five subcommands is a tax that filters out exactly the non-engineers who benefit most. Assigning a ticket in our existing project management system is ideal because nobody needs training for it.

04

What it cost and what it returned

Since late April: 1,035 sessions.

It cost under $2,200 to run the platform for three months, against 412 unique pull requests, so about $5 per pull request produced. Cost never became the binding constraint, so optimizing it would have been wasted effort.

Three months of operation<$2,200412 unique pull requests · ≈ $5 each

05

What I’d tell someone starting this now

Own the harness. Pick a runtime you can drive as a server with a typed client and read the source of, then treat models as swappable, because the open-weight ones are improving fast enough that provider lock-in carries a real cost.

Put it where the work already lives and make the interface easy enough to disappear. A CLI selects for the people who need this least.

Use the infrastructure your platform team already runs. Same cloud, same identity model, same network policy, same audit trail. It’s less fun, and it’s the difference between one security conversation and six.

Design the security model as a set of competing requirements, not a checklist. Auditability, keeping PII where it belongs, and reproducing real bugs all constrain each other. Writing down how they resolve is what let the platform team say yes.

Build the evidence trail before tuning code generation. Recordings, screenshots, CI results, and a link back to the session are what make a reviewer willing to open the pull request at all.

Budget for teardown as a deliverable. Compute you migrate away from keeps billing.

Plan review capacity alongside the platform. Review throughput was our constraint the whole time, and no model release moves it.

The version I’d build next starts at the review bottleneck, because that’s where the constraint sits. The rest of it, at this point, is a solved problem that takes about four months and one person.