Givi Pataridze blog

My personal blog descr

July 16, 2026 · 12 min read

Autopilot is an event-driven feedback loop built on GitHub webhooks and headless Claude Code sessions. A label on an issue triggers an implementation session in an isolated git worktree; a failed CI run triggers a fix session; a review comment triggers a revision session; and a second, independent AI reviewer gates every merge with a hard verdict. This article walks through how it works, the fail-closed guardrails that keep it from destroying my repos, the embarrassing bugs (including a garbage collector that never collected anything for weeks), and the uncomfortable questions it raises about what "being a developer" now means. Written, fittingly, with the help of the system it describes.

Last Tuesday, at 2:47 AM, a pull request was opened in one of my repositories. The code was written, tested against the full gate suite, and pushed. A reviewer read the diff, requested changes on a genuinely questionable error-handling path, and the author revised the code and replied with a justification. By the time I woke up, the PR was green, approved, and auto-merged.

Nobody involved in any of that was a human being.

I know how that sentence reads. Half of you just rolled your eyes at another "AI replaced me" clickbait piece, and the other half are already drafting a comment about how this is exactly what's wrong with our industry. Good. Stay with me, because the reality is more interesting than either reaction — and I have the failure logs to prove it.

The confession that started it

Here's the thing nobody says out loud: for the past year, a growing part of my job was being a clipboard.

An issue comes in. I read it, I open my AI assistant, I paste the issue, I get code, I test it, I push it, CI fails, I paste the CI log back into the assistant, I get a fix, I push again, a reviewer comments, I paste the comment into the assistant... You see where this is going. I had become a very expensive, very tired message bus between GitHub and a language model. The model was doing an increasing share of the actual reasoning. I was doing the ceremony.

The realization that triggered this whole project was brutally simple: every one of those copy-paste moments was already an event GitHub knows about. Issue opened. Workflow run failed. Review submitted. Comment created. GitHub literally offers to POST them to any URL you give it.

So I gave it a URL.

What Autopilot actually is

Autopilot is not a product. It's a couple thousand lines of Python — a FastAPI webhook receiver, a job dispatcher, and a pile of hard-won guardrails — running on a machine in my office, connected to GitHub through a tunnel.

The core loop looks like this:

  1. An issue gets a priority label (P0P3). A P0 triggers an immediate implementation session; everything else waits its turn. The session gets its own isolated git worktree, reads the issue, writes a design doc, implements with TDD, runs every gate the repo defines — lint, typecheck, tests, build — and opens exactly one pull request with Closes #N, properly labeled.

  2. CI fails on an autopilot PR? The workflow_run webhook fires, and a fix session spins up in that branch's worktree with one instruction that matters: reproduce locally, find the root cause — no guessing. It reads the failing check logs itself with gh, fixes, and pushes.

  3. A reviewer leaves feedback? The receiver routes it into a revision session. The agent reads the full PR context, makes the change (or — and this is the part that surprises people — pushes back with an argument if the feedback is wrong), and replies on the thread.

  4. A PR gets opened? A different AI reviews it. More on this in a second, because it's the load-bearing wall of the entire system.

Every session is a headless Claude Code run: a fresh process, a scoped token for exactly one GitHub org, a hard turn limit, and a one-hour timeout. No memory between runs. No shared state except what's in git and on the issue threads — which, it turns out, is exactly the right amount of state. The GitHub thread is the database.

The rule that makes it work: the author never approves

Early on I made the one decision I now consider non-negotiable for any autonomous coding system: the AI that writes the code must never be the AI that approves it — and they can't even share credentials.

Autopilot runs two separate GitHub Apps: a coder identity and a reviewer identity. The coder writes code and can't approve anything. The reviewer reviews and can't push code. The reviewer's contract is deliberately rigid: its first output line must be exactly VERDICT: APPROVE or VERDICT: REQUEST_CHANGES. Not "looks good with some nits." A verdict.

And here's the paranoid part I'm most proud of: ambiguity fails closed. If the reviewer returns anything without a verdict line — empty output, a friendly "LGTM!", a crash — the system treats it as REQUEST_CHANGES. If it can't read the PR's labels, no verdict. If it can't enumerate the changed files, no verdict. If the branch head moved while the review was running, the approval is thrown away and a fresh review is queued, because the approval was for a diff that no longer exists.

On top of that sits a deterministic policy layer that no LLM output can override: certain paths — CI workflow files, database migrations, anything smelling of secrets — always force REQUEST_CHANGES plus a needs-human label, regardless of how confident the reviewer is. An LLM can be sweet-talked by a prompt injection buried in a diff. A fnmatch pattern cannot.

Why so paranoid? Because the failure mode of an autonomous coder isn't writing bad code. It's writing bad code and then approving it. Break that loop with cryptographic identity separation and dumb deterministic rules, and everything else becomes recoverable.

Escalation, or: teaching a machine to give up

The second non-negotiable: bounded retries with a human escape hatch.

Every job type has an attempt cap. Three CI-fix attempts without green? The PR gets a needs-human label and a comment saying, in effect, "I've hit my limit — a person needs to look at this." Same for review-response rounds, same for issue triage threads. The caps count consecutive unproductive rounds, not lifetime attempts — a subtle distinction I got wrong at first, which meant a PR that succeeded every round could still exhaust its cap and page a human for no reason at all.

needs-human turned out to be the most important feature in the system, and it's barely code. It's an admission, encoded in a label: this system knows it has edges. The issues that get escalated are genuinely the interesting ones — ambiguous requirements, architectural forks, tests that are wrong rather than failing. The machine does the grind; the weird stuff still finds me. That's the deal.

The bugs that humbled me

Let me tell you about the garbage collector that never collected anything, because nothing captures autonomous-systems development better.

Sessions run in git worktrees — one per feature branch, so parallel jobs can't trample each other. Worktrees pile up, so I wrote a reaper: when an issue closes, reconstruct its branch name as feat/issue-<N>, find the worktree, check the PR is merged, delete it. Clean. Logical. It ran for weeks without deleting a single worktree — silently, successfully, doing nothing — because nothing in the system actually names branches that way. The sessions generate names like feat/42-add-rate-limiting. The path I was reconstructing never existed. The fix was to stop reconstructing reality and ask git for the actual worktree list. The lesson was bigger: in an autonomous system, "no errors" and "working" are entirely different claims. Silence is not success. Silence is just silence.

Or the pm2 incident: under the process manager, every headless session inherited an open-but-empty stdin pipe. Claude would stall three seconds, print a benign warning about receiving no stdin — and that warning would land at the tail of stderr, exactly where my truncated error logging looked first. For days I "debugged" a stdin problem that was actually an API timeout, because my own logging kept showing me the wrong suspect. Now every failed run dumps its complete output to disk for postmortems, and stdin is explicitly closed. The system's observability had to grow up before the system could.

Or my favorite category: time-of-check-to-time-of-use races against humans. The reviewer checks a PR's hold labels, spends eight minutes reviewing, and approves — but a human added a hold label during minute six. Autopilot now re-checks labels immediately before posting any verdict. When your collaborators include people who don't know they're racing you, you re-check everything.

Watching it think

For months, my window into the system was a log file and a six-column HTML table. Job running. Job succeeded. That's it. It felt like managing an employee through a keyhole.

So recently I rebuilt the operator panel to stream everything: each session's model, elapsed time ticking live, token consumption (input, output, cache), running cost in dollars, turn count, and a live activity feed of what the agent is actually doing right now$ run the full test suite, Edit src/api/routes/users.py, followed by the agent's own narration. The headless runner now parses the event stream in real time instead of waiting for a final summary.

Two things about that rebuild deserve a paragraph, because they capture where this all gets philosophically weird.

First: Autopilot's dashboard was built by the thing the dashboard watches. I described what I wanted; a Claude session read the codebase, made the changes, updated nineteen tests, and verified the streaming pipeline against a real run. The system upgraded its own instrumentation.

Second — and this is the part I keep telling people — I then ran a multi-agent code review over that change: seven parallel reviewer agents attacking it from different angles, findings adversarially verified. They found real bugs. The best one: the new streaming loop only exits on pipe EOF, so a background process left running by an agent — say, a dev server a session forgot to kill — could hold the pipe open past the kill signal and wedge a worker thread forever. A hang the old code was immune to, introduced by the improvement, caught by AI reviewing AI-written code before it ever ran in anger. If you want a picture of 2026-era software engineering, it's that sentence.

The economics, without romance

Let's talk numbers, because the discourse desperately needs fewer vibes.

A typical CI-fix session costs me cents to low single-digit dollars. A full implementation session on a real issue — design doc, TDD, gates, PR — costs a few dollars and burns anywhere from twenty minutes to an hour of wall-clock time I am not spending. The panel now shows me cost per session, and the honest summary is: an accepted PR costs less than the coffee I'd drink while writing it myself, and the failed attempts are the tax. The tax is real. Sessions time out. Sessions produce mediocre first drafts that the reviewer bounces twice. The attempt caps exist because without them, the tax compounds.

What it's genuinely good at: well-specified features in codebases with strong test suites, CI fixes with reproducible failures, mechanical refactors, review-feedback loops. What it's genuinely bad at: ambiguity, taste, anything where the issue itself is wrong. It will faithfully implement a bad idea with excellent test coverage.

The part that's supposed to trigger you

So here's where I say the thing.

I didn't build Autopilot because AI is going to replace developers. I built it because a huge fraction of what we've been calling "development" was never the valuable part. Shepherding a green checkmark. Re-running flaky CI. Translating "the reviewer said X" into a three-line diff. We dressed that work in the identity of engineering, and we defended it because it was billable and familiar. It was always ceremony, and ceremony is exactly what event-driven systems eat.

What's left after the ceremony is gone is uncomfortable and much smaller and much harder: deciding what to build, recognizing when a spec is wrong, designing guardrails, defining "done," and taking responsibility for what ships. My commits went down. My accountability went up — every merged line is something I chose to allow through a system I designed. There is no "the AI did it." I built the AI's cage; whatever escapes it is mine.

If your reaction is "I would never trust an AI to touch my codebase" — I'd gently point out that you already trust Dependabot, autocomplete trained on strangers' code, and a CI pipeline you didn't write, and none of those are forced to survive an adversarial reviewer with a separate cryptographic identity and a deterministic policy layer before merging. Your untrusted AI has fewer checks than my distrusted one.

And if your reaction is "this is the end of junior developers" — maybe. Or maybe the junior role was never "write the easy code"; that was just the training exercise we had lying around. The new training exercise might be operating systems like this one: reading escalations, tuning guardrails, learning to smell a wrong needs-human from a right one. I learned more about software engineering from Autopilot's failure modes than from the last several years of writing code by hand.

What I'd tell you to steal

If you build your own — and you should, it's a webhook receiver and a CLI, not a moonshot — steal these:

The 2:47 AM pull request wasn't the machine replacing me. It was the ceremony finally running without me — while the decisions, the guardrails, and the blame stayed exactly where they've always belonged.

I didn't lose my job. I lost my excuses.

Subscribe

Get the next issue in your inbox. Free — unsubscribe anytime.

← All issues