MF
Writing
ai-agentsclaude-codeloop-engineeringdeveloper-workflowautomation

Engineering loop: an AI pipeline that ships PRs on its own

Sunday, August 30, 2026 · 11 min read

A few months ago I wrote about running multiple agents in parallel with git worktree and I closed that piece with an honest admission: parallelism works, I don't. The agents were producing more code than I could review. This is what I built to fix that.

The bottleneck. It was me.

Let me describe what a ticket actually looked like once the worktree setup was in place.

Read and analyse the ticket. Create a branch, a worktree, an environment, spin up docker. Implement, write tests. Run lint, run types, check translations. Commit, push, open the PR, assign it. Announce it on Slack. Answer review comments. Watch CI, fix what's red, re-push. Clean up after the merge.

Out of that list, exactly one step required me to think: reading and analysing the ticket. Everything else was mechanical work I happened to be doing by hand because nobody had told a machine to do it. And with several agents running in parallel I was doing that mechanical work several times over, in parallel with myself, which is a very expensive way to be a human being.

So the question stopped being "how do I review faster" and became "what am I actually for".

The shift. From code detail to solution design.

The answer we landed on at Lago is that our focus moves upstream. Architectural analysis, design patterns, best practices, product decisions, how the code should be structured. We design and analyse, an agent executes. Borrowing the loop engineering idea: spec, build, review, with no human sitting in the middle of the cycle.

That sounds like a slogan until you make it concrete, so here is the concrete version. Every skill in the pipeline carries what we call an autonomy contract: between the input and the final Slack post the pipeline runs alone, and it never pauses for approval mid-run. The only things that bring a human back in are an exhausted retry budget, a decision the agent is not allowed to take, or an unrecoverable external failure.

Which leaves the human with three moments:

  • Designing the solution. Not writing a ticket, designing the thing: which pattern this belongs to, where the boundaries sit, how data flows through it and who owns each piece of state, which architectural decisions are expensive to reverse and therefore have to be made now. That thinking then gets written down in two places, the functional analysis in Notion (product behaviour, UX flows, edge cases, constraints) and the technical breakdown in Linear (scope, acceptance criteria, files, structure decisions). The ticket is the output of the design work, not a substitute for it.
  • Unblocking a run the agent can't decide. Three failed reviews, or a CI failure that isn't the ticket's fault.
  • Final review and shipping. Reading the diff as the person accountable for it, then merging. The pipeline never merges, never approves, never force-pushes.

The uncomfortable consequence is worth naming early: a well-analysed ticket IS the program. Quality of analysis in, quality of PR out. A vague ticket still produces vague code, only now it produces it faster and with a green CI badge on top.

The foundations. This is the part people skip.

Here's the thing that took me a while to accept: the loop is not new magic. It's the assembly of pieces we had already built over months, and none of it would have worked if those pieces weren't there first.

Isolation, first. Every run needs a place to execute where it can't hurt anything. That's the git worktree setup and the orchestration script from that same article. The pipeline never reinvented the infra, it just calls the script we already use:

bash
lago-worktree create ING-517-swap-customer-overview-connection \
  --from-front=main --from-api=main

One command, and the ticket gets a branch named <ISSUE-ID>-<topic-slug>, a worktree in front-worktrees/, a dedicated frontend container on its own port and an isolated API worktree on its own. The main checkout is never touched. And because the app runs live on a real port, at the end of a run I can open it and manually test the output end to end.

Conventions, second. A front/CLAUDE.md with our actual best practices. Not a generic "write clean code" document, but the specific opinions this codebase holds: the canonical drawer pattern and how to test it, the canonical dialog pattern and how to test it, how pagination works for lists and tables, the organization slug architecture and which API to use from which caller. Around five hundred lines of "this is how we do it here". Without it the agent writes code that passes every check and still looks like it came from a different company.

Skills, third. A library in front/.agents/skills, versioned in the repo, shared by the whole team. Each one encodes a task we used to explain over and over:

  • make-tests: writes tests the way we write tests, and the build phase is forbidden from hand-writing them.
  • make-e2e-tests, react-testing-library: the same idea for the other layers of the pyramid.
  • migrate-formik-to-tanstack, extract-section-to-drawer: repetitive migrations that are mechanical but easy to get subtly wrong.
  • graphql and apollo-client: how we query, how we type, how codegen fits in.
  • tailwindcss, git-commit: styling conventions and commit conventions.

Those three things are why the loop produces code that looks like ours. Take them away and you get a very fast machine for generating pull requests nobody wants to merge.

And it's open-ended. Any skill we add to the repo tomorrow becomes part of the loop the same day.

The loop. Five skills, two commands.

The whole pipeline is two commands. The first one takes a ticket and gives back a pull request.

bash
/loop-run ING-538

That's it. From here on nobody types anything until the PR is open and CI is green.

Under the hood /loop-run is an orchestrator that chains skills: sweep, spec, build ⇄ review, ship.

  • loop-clean runs first as a sweep. It finds worktrees whose PR is already merged and proposes destroying them, so a run never starts on top of yesterday's leftovers.
  • loop-spec reads the Linear ticket (description, acceptance criteria, relations and the full comment thread, because decisions live in comments) plus every linked Notion page, explores the codebase to locate the files actually touched, and writes spec.md.
  • loop-build creates the worktree, claims the ticket on Linear as "Dev in Progress", implements against the spec, invokes make-tests, and gets the gates green: pnpm lint, pnpm types, pnpm translations:inspect, pnpm translations:ensure-consistency, and a scoped jest run on the touched paths.
  • loop-review is dispatched to a fresh subagent and writes a PASS or a numbered FAIL list. Maximum 3 cycles.
  • Ship is not a skill, it's the last stretch of the orchestrator: commit with our conventional message template, push, open a ready PR self-assigned to whoever ran the loop, move the Linear issue to "In Review", watch CI (maximum 3 fix cycles), and post to the right Slack channel only once CI is green.

All the run's state lives in a per-developer state dir, ~/.claude/loop-state/<ISSUE-ID>/, outside the repo and never committed: spec.md, state.md, review.md, ci-failure.md, and the history files that make retries smarter.

Review runs in a fresh subagent

This is the part I'd defend hardest if someone told me to simplify the pipeline.

The review is dispatched to a brand new agent with clean context. It doesn't know how the code was written, what the builder struggled with, or which shortcut felt justified at 4pm. It knows three things: the ticket, the spec, and the diff. Running it inline in the same session is explicitly forbidden.

The first question it has to answer is not a checklist item. It's this: does this diff, as a whole, make sense for the objective of the ticket? A diff can pass every mechanical check and still miss the point, and that is a FAIL.

Only then does it go through the rest: acceptance criteria actually mapped to code, no scope creep, nothing reinvented that the design system already gives us, translations and tests in order. And it re-runs the gates itself rather than believing the build phase's claim that they were green, because a reviewer that trusts the author isn't a reviewer.

After the PR. /loop-revise.

The PR is open, a colleague leaves a comment, Copilot leaves three. This used to be the other half of my day. Now it's the second command:

bash
/loop-revise ING-538

In plain terms: it goes and reads every comment left on that PR, decides which ones it agrees with, applies the ones it does, pushes the fix, and answers all of them on GitHub. Give it a sentence of your own instead ("the empty state should say something friendlier") and it treats that as the feedback. It reuses the same worktree the build ran in, so nothing is set up twice.

The important word is evaluates. It weighs each comment before touching a single line. The skill says it out loud: you are a senior peer, not an executor. Three possible outcomes per comment:

  • Sound. Checked against the acceptance criteria, the design system and the styleguide, then applied, tested, pushed. Reply on GitHub with the sha.
  • Sound, but better done differently. The suggestion works, and the agent sees a cheaper option ("renaming works, but that hook is imported in 7 files"). It proposes the alternative and lets the author pick instead of applying blindly.
  • Breaks something. It contradicts an acceptance criterion, duplicates the design system or degrades the code, so it's not applied and the reply explains concretely what it would break.

Two rules on top. If the feedback asserts something about the code ("this rerenders twice"), the agent has to verify it before agreeing, because implementing performatively to please is the failure mode of every review bot I've seen. And the same scrutiny applies to everyone: a Copilot suggestion gets no free pass, and no external comment is ever left unanswered.

When it gets stuck. It comes and finds me.

A loop that fails silently is worse than no loop. The retry caps are not a matter of the model counting in its head, they're enforced mechanically by front/scripts/iter-budget.sh. Three failed reviews or three red CI cycles and the run stops.

Then it pings me for real. The Slack side has two identities, deliberately. The team announcement goes to the team channel from my own account, because a PR announcement should come from a person. The alert comes from a bot app through front/scripts/loop-notify.sh, because Slack does not notify you about your own messages and a notification you don't receive is not a notification.

text
🚨 loop-run stopped - ING-538

Reason: CI red on Run Codegen. The API added
AuthenticationMethodsEnum.EntraId, the mapping is no longer exhaustive
under codegen + tsc. Doc-only PR, fix out of scope.

Stage: CI cycle 1/3
PR: github.com/getlago/lago-front/pull/4069
Next: reply here. (a) fix the mapping in a dedicated PR, (b) merge anyway,
(c) wait for the companion API PR and rebase. No team post while red.

Before the DM goes out it writes an impediment.md: stage, cause, what it tried, what a human has to decide. Enough to reconstruct the failure without reading a chat transcript.

Then it waits, polling that DM for up to an hour. I reply in plain language ("flaky test, raise the timeout"), the reply gets the same critical evaluation as a review comment, and the fix cycle re-enters. No reply and it closes cleanly, resumable later with /loop-revise.

Not every stop is a failure, and the pipeline says so: a run that's green everywhere except a security alert I'd already dismissed once exits as needs-operator-adjudication, with a message written not to read like a crash. The code is done, one human decision is outstanding.

Guardrails. What the loop is never allowed to do.

Every skill carries the same hard rules:

  • Humans merge. No self-approval, no merge, no auto-merge flag.
  • Three attempts, then stop. Both the review cycle and the CI cycle have a hard cap, enforced by a script rather than by the model counting in its head. An agent left free to retry will happily burn an afternoon of tokens converging on nothing.
  • Stay inside your own sandbox. The pipeline touches its own branch and its own worktree, and nothing else. It never destroys work to start over clean, it stops and asks.
  • Nothing goes out while things are red. No announcement to the team until CI is actually green.

Rules like these are dull to write and they're most of what separates an agent you can walk away from and one you have to supervise.

It learns from its own runs.

Every run writes down how it went. Two things come out of that.

Inside a run, it never tries the same fix twice. Each failed attempt is saved, and the next one has to read it and say what it's doing differently. Fail on the same thing twice and it has to change approach entirely, not polish the same idea a third time.

Across runs, it proposes improvements to its own skills. When something goes wrong that a better instruction could have prevented, it writes that instruction down in a file. Failed reviews and red CI runs feed this, but the best material comes from the review comments my colleagues leave and the loop agrees with: those are our standards, and the fact that a human had to point one out means the skills were missing it.

Where this leaves me.

Three manual steps per ticket after the analysis: launch the loop, test the output, merge. No PR babysitting in between. N loops in flight, so while one executes in its isolated worktree I'm analysing the next ticket. Zero merges or approvals by AI, ever.

The last article ended with me as the bottleneck and no idea how to fix it. This is the fix, and it's less about the agents than I expected.

What's left for me is the part that was always the actual job: deciding what to build and why, structuring it well enough that it can be executed, and taking full ownership for what ships.

Share