How a solo port turned into an autonomous pipeline, and what changed when the agents started reviewing each other.
For most of the last year I have been building the same product twice. Expause is a video social app. The mobile app is Flutter, and it came first. In October 2025 I started a React web version that has to match it feature for feature. Same behaviour, same rules, same edge cases. There is one of me.
That sounds like a job for a coding agent, and it was. Every web feature already has a working reference implementation in Dart. You point the agent at the Flutter screen, describe what "the same" means on the web, and let it type. For the first few months that is exactly what I did, and it was faster than doing it by hand. It was also the most tiring way I have ever written software.
The babysitting tax
Here is what a day looked like. Open a session. Explain the architecture, again, because the last session is gone. Paste in the conventions. Ask for the feature. Watch it write. Approve a file. Notice it put business logic in a UI hook. Say so. Approve. Notice it skipped the unit test the conventions require. Say so. Notice it invented a constant that already exists under another name. Say so. Approve. Read the diff once more, because by now I no longer trust that I caught everything, and I know that I did not.
None of those steps is hard. That is the problem. It is a stream of tiny judgements, none of them worth a coffee break, all of them required, and the stream never stops as long as the agent is typing. You cannot leave, because an unwatched agent drifts. You cannot really think either, because the interruptions come every minute. I was faster than before, I ended every day drained, and the code was not even that good. An agent that is being watched optimises for the watcher. It writes whatever makes me say "fine, next" in real time, which is not the same as what survives a proper review a week later.
Somewhere around spring I admitted that the bottleneck in my project was me, sitting next to a machine, being the review process.
Writing the rules down first
The first thing I built was not autonomy. It was boring. I wrote the rules down. One always-loaded project file with the things every agent must know, and one conventions document per layer of the app: how data access works, how the domain layer is shaped, how pages and hooks split their responsibilities. Every rule I had been repeating out loud went into a file instead.
Then I stopped using one agent for everything. The work got split into narrow roles: a planner that turns a request into small, single-layer tasks; an implementer per layer that reads only its own task and its own layer's conventions; a committer that does nothing but tick the task off and commit. Each one starts with a fresh context and a tool allowlist that matches its job. That alone removed half the re-explaining, because nobody had to remember anything across a long session. Whatever an agent needed to know was in the file it was told to read.
I was still pressing go on every task at this point. It was better. But I was still the reviewer.
The moment reviews changed the code
The change that made this a different kind of tool was adding reviewers that are not me.
A reviewer agent reads the finished diff against the plan and the conventions and writes its findings to a file. The part that mattered was a decision about tools rather than prompts: reviewers cannot edit. A reviewer that could fix what it found would quietly fix it and say nothing, and I would learn nothing. A reviewer that can only write a finding has to make its case.
The first reports came back with things I would have missed, or things I would have been too tired to look for by the tenth file. A component over the size ceiling. A hook that had quietly grown a fifth responsibility. A value copied from mobile that ignored a shared constant the web already had. So I kept adding review angles. One reviewer checks parity with the mobile app, payload for payload and threshold for threshold. One checks that every file landed in the right layer. One reviews the whole branch. The bigger reports get a meta-review before anything is fixed, because reviewers make things up too.
Then I did something I should have done from the start: I measured whether each reviewer earned its cost. The per-task reviewers, the ones that checked every unit right after it was written, turned out to be almost entirely redundant with the end-of-branch reviews, so they were dropped. What the same measurement showed was a class of defect nobody caught. The port had faithfully reproduced bugs from the mobile app, because every reviewer treated the reference implementation as the truth.
So the last reviewer added was the skeptic. Its premise is that the plan, the implementation and the reference implementation can all be wrong, and its job is to report only what the other reviewers did not. On one of its first branches it found that the mobile app never reset an "upload in progress" flag on the failure path, which the web port had copied exactly, so one failed upload would have locked the button for good. Both clients got fixed. That is when I stopped thinking of the reviews as a safety net and started thinking of them as the reason the output was good.
And this is the thing I did not expect: the code got better before the reviews ran, not only after. An implementer that will have to pass a parity reviewer, an architecture reviewer and a skeptic writes differently from one trying to satisfy a tired human in real time. The quality jump came from the agents having to convince each other.
Walking away
Once the reviews were trustworthy, letting the whole thing run without me was mostly plumbing. A small daemon watches a folder. A task prompt dropped there gets its own branch and its own git worktree, and a headless session runs the entire flow: plan, review the plan until it converges, implement task by task, run every review gate, fix every finding, test the feature in a real browser through a tester agent that can drive one and nothing else, update the docs, push the branch. I get a notification.
It did not work the first time, or the tenth. Runs got stuck, the API had bad days, a resume regenerated work it had already done. Each of those became a rule or a mechanism, and today a run that has to stop writes down where it was and continues from there later. That reliability story deserves its own article, and it will get one. For now the point is simpler: every wrong turn is a line in a file that the next run reads.
What stayed human
I still test every feature by hand. This is the part I have no interest in automating.
When a branch comes back, I use it the way I would use a colleague's branch. I open the app, click through the feature, try the edge cases, look at it on a phone. Then I write down what I found, in plain language, and drop that file where the task prompt went. A fix cycle starts: an agent verifies each observation against the code and plans the fixes, the same review gates grade that plan, the fixes are implemented, the browser tests are extended with one regression test per finding, and I get another notification.
The last step of that cycle is my favourite part of the whole system. Every finding from my hands-on review that got past all the automated gates is distilled into a one-line rule in a lessons ledger, and every planner and every reviewer reads that ledger before it starts. The rules are boring and specific. Privileged data must be gated on the server, not only in the client. Reuse the shared constant even when the mobile app hand-rolls its own. Mutate optimistically before the await, the way mobile does. The first of those got past the pipeline three times on three different branches, caught by hand each time, before it became a line in the file. It has not come back since.
So the human review is not a fallback for when the pipeline fails. It is the training signal. Every branch I review by hand makes the next one slightly harder to get wrong.
What a day looks like now
I open an ordinary session in the repository and type what I want, the same way I did a year ago. "Add the block-user action to the chat header, same conditions as mobile." The difference is what happens next. The session asks how I want it handled: run it autonomously, do it here, talk it through first, or stop asking. I pick the first, it proposes a branch name, I confirm, and that is the end of my involvement in the implementation.
Then I go do something else. Work on the mobile app. Think about the product. Have lunch. At some point a notification says the branch is ready. I review it, write what I found, drop it, and get another notification when the fixes are in. Then I merge.
It is not flashy. Nothing about it looks like the demos. It is a folder, a daemon, a set of agents with narrow jobs and narrow tools, and a stack of files that say what each of them saw. The feeling is closer to having a careful team that works while I am away than to having a fast typist that needs me in the room. The babysitting tax is gone, and the code that comes back is better than the code I used to approve file by file.
It is open source
The whole thing is published as a Claude Code plugin plus a small command-line tool that wires it into a repository: github.com/firu-daniel/autonomous-sdlc-harness, Apache-2.0. The plugin carries the agents, the commands and the review flow. The CLI generates the permission profile, the scripts and the daemon, which are the parts nobody should have to get right by trial and error twice.
The next article is the shape of it in plain words: what a run does from the moment you ask until the branch is pushed, with one diagram and no scorecards. After that, short pieces on the parts I think are interesting on their own: planning, the reviewers, the lessons flow, and how the pipeline files bugs against itself without being allowed to fix them.
If you have been sitting next to an agent all day wondering whether this is what it is supposed to feel like, it is not. It gets boring, in the good way.
Daniel Firu builds and operates an autonomous software-delivery harness for Expause, a Flutter and React product.
Top comments (2)
The agents-reviewing-each-other shift is the real unlock. Supervising every agent is a treadmill; once they check each other's output, my role drops to reviewing the review. The cost nobody mentions is the plumbing: logging what each agent actually did so you can audit who broke what, plus a way to stop a runaway run without killing the pipeline. Curious what the review setup between agents looks like here.
Thanks, and yes, "reviewing the review" is exactly where I landed too.
The setup here, in short: every reviewer is a separate agent with a fresh context that reads the finished diff against the original prompt, the plan and the project's conventions, and writes its findings to a file that gets committed on the branch. Reviewers cannot edit, by tool allowlist, so a finding has to be argued, not quietly fixed. There are a few angles: one checks parity with the reference implementation, one checks layer placement, one reviews the whole branch. The bigger reports then get a meta-review by another agent that checks the findings are real, correctly graded and complete, before any fix is applied. Last comes the skeptic, which assumes the plan, the code, the reference implementation and the earlier reviews can all be wrong, and reports only what the others missed. Then a fix loop works through the findings one commit each.
On the plumbing, agreed, it is most of the work. The audit trail is the files themselves: each agent leaves its report, and an orchestrator keeps a flow ledger of every step, so "who broke what" is a git log question. For runaway runs there is a pause marker that makes the run finish its current unit, write down where it was, and stop, and the same mechanism lets a run park itself with a clarification question and resume when answered.