Inspiration

The messy, multi-step chore we picked is the least interesting hour in software delivery, and every team repeats it once per ticket, forever: someone reads a card, works out which files it touches, writes the change, opens a pull request, and drags the card to the next column.

We already run an autonomous board-to-PR runner in production at OpsAgents, so we knew the hard part before we started — and it is not writing code. Any capable model can write code. The hard part is restraint: knowing which cards to refuse, stopping before a change grows past what a human will actually review, never committing a credential, and never being able to authorise its own writes. An agent that is 95% right and unbounded is worse than no agent at all, because the 5% lands in your main branch at 3am.

So we built the intelligence as an agent crew, and the authority as ordinary code the crew cannot reach.

What it does

AutoPilot Agentic drains one board card per run:

  1. triage_agent decides whether the card is deliverable as a single self-contained change, and extracts its acceptance criteria. Saying no is a correct outcome — a refused card is cheaper than a bad pull request.
  2. planner_agent produces the smallest change that satisfies every criterion, file by file, with the real risks named.
  3. implementer_agent writes the complete new content of every planned file.
  4. reviewer_agent checks the produced change set against each acceptance criterion in turn, and approves only what is demonstrably met by the code.

Then the deterministic half takes over: a shape-matched secret scan, blast-radius caps with a never-write list, and an approval gate. Only if all three pass does a branch get pushed, a pull request opened, and the card moved to Code Review with the evidence written back onto it.

The public console runs the whole thing live and stops at the gate — you see the diff it wanted to push, and the refusal that stopped it.

How we built it

  • Google ADK — a SequentialAgent over four LlmAgents, each with a typed Pydantic output_schema, so every stage hands the next one structure rather than prose and the console can render a stage the moment it lands.
  • Gemini 3.5 Flash via Vertex AI, with a model ladder (3.5 → 3.6 → 3.7) probed at boot rather than a hardcoded constant.
  • Cloud Run for the service and the console, Firestore for the run ledger, Cloud Build + Artifact Registry for the image, and Workload Identity Federation for a keyless deploy from GitHub Actions.
  • FastAPI + Server-Sent Events for the live run stream.
  • The whole tool layer — board client, repo client, guard, ledger, model ladder — is standard-library-only and covered by 38 tests that need no network, no credentials and no SDK.

Architectural decisions worth defending

No agent holds a write tool. Pushing a branch, moving a card and clearing a guard live in the orchestrator, executed by plain Python after the crew has finished talking. An approval gate a model can argue with is not a gate, and the cheapest way to guarantee it cannot argue is never to hand it the verb. In the demo video you can watch the reviewer approve a change that the guard then refuses.

A finding never carries the value. The secret scanner reports kind and locationgithub-token at app/x.py:12 — and never the matched string. We learned this the expensive way: an agent doing a routine read of an OAuth console printed a client secret into its own transcript, and rotation was then declined as too costly. An agent that prints a secret in order to report it has still leaked it, and a transcript is forever.

empty is not success. A run that produced no change set is recorded as empty in the ledger. Inferring success from "nothing raised" is how an audit trail quietly becomes a decoration.

CI configuration is on the never-write list. A runner that can edit its own workflow can grant itself anything.

The console is served by Cloud Run directly, not from a CDN in front of it. A run streams for well over a minute, and a hosting rewrite in front of a long SSE response returns a frontend 502 at about the one-minute mark while the same request succeeds against the service URL.

Challenges we ran into

A 404 that reads like a permission wall. gemini-3.5-flash returns a flat 404 from a regional Vertex endpoint — "not found or your project does not have access to it" — which is indistinguishable from an entitlement problem and sends you off to audit IAM. The Gemini 3.x family answers on the global endpoint. That is why model.py probes a ladder of candidates and pins the first that answers, and why /api/health reports the model the instance actually resolved rather than the one it was configured with.

A health check that reports a dead service. Our health endpoint was /healthz, and every probe of it returned Google's own 404 page. It was not our app: on a *.run.app hostname the request never reaches the container and appears in no Cloud Run request log at all — while /zzz on the same service does log a 404 from the app. Meanwhile /, /api/runs and /openapi.json were all serving perfectly. The endpoint is now /api/health, and the deploy workflow polls it until it names a resolved model before the deploy is allowed to go green, because a deploy command that exits 0 is not the same as a service that works.

Accomplishments we're proud of

The refusal in the second half of the demo is a real one. The card — "add a CI workflow that runs the tests on every push" — is an ordinary request, the crew implemented it correctly, the reviewer approved it, and the guard still stopped it at .github/workflows/ci.yml. Nothing about that path is staged.

What we learned

Autonomy is a budgeting problem, not a capability problem. Every hour spent on guard rails bought more usable autonomy than an hour spent on prompts would have, because the binding constraint on letting an agent near a real repository is how confidently you can describe what it will refuse to do.

What's next

Cloud Scheduler ticks for continuous multi-board draining, per-tenant credential scoping so it can run a customer's board rather than only our own, and feeding reviewer findings back into the planner so a bounced card comes back smaller.

Disclosure of pre-existing work

Everything in the repository was written during the submission period. Two things it draws on that were not: OpsAgents' private board-autopilot runner supplied the product concept — an autonomous board-to-PR loop with per-board cadence and a cost cap — and the operating rules behind the guard module were learned in earlier OpsAgents work. No code from those projects is reused here; the lessons are, and they are cited in the source where they apply. Third-party dependencies are the Google ADK, the Google Gen AI SDK, the Firestore client, FastAPI and Uvicorn, all installed from PyPI and none vendored.

Built With

Share this project:

Updates