DEV Community

Cover image for Inside an unattended coding run: from a dropped file to a reviewed branch
Daniel Firu
Daniel Firu

Posted on

Inside an unattended coding run: from a dropped file to a reviewed branch

What happens between the moment I ask for a feature and the moment a reviewed, browser-tested branch shows up.

Last time I wrote about why I stopped sitting next to coding agents all day, and what changed when the agents started reviewing each other instead of reporting to me. I closed that piece by promising the follow-up: the shape of the thing in plain words, one diagram, no scorecards.

This is that piece. One feature, one run, from the moment I ask to the moment a branch shows up. No success rates and no cost tables, because those are a separate article and they mean nothing until you can picture the machine they came out of. Just the shape.

The thing being described is open source: autonomous-sdlc-harness, a Claude Code plugin plus a small Node CLI. I built it for my own product, a Flutter mobile app with a React web version that has to match it feature for feature, and I use it every working day.

The thirty-second version

A daemon watches a directory. I drop a file in it that says what I want. The daemon gives that file its own git worktree and its own branch, and launches a single headless session inside it. That session plans the work, implements it one piece at a time, puts the result through several independent review gates, drives the running app in a real browser, and stops at a pushed branch. Then it notifies me when it's done.

It never merges. It never opens a pull request. It never touches main.

Here is the whole thing on one page:

  I type a request in an ordinary session
        │
        ▼
  a file lands in the inbox:
  <project root>/autonomous_inbox/feat_block_user_task_prompt.md
        │
        ▼
  THE DAEMON  (files, worktrees, processes, a registry)
    · routes by filename
    · creates the worktree and branch, commits the prompt
    · launches ONE headless session, tees its event stream
    · classifies how it ended, notifies me
        │
        ▼
  THE RUN  (phases, agents, artifacts)

    PLAN     plan writer  ⇄  parity reviewer
                          ⇄  architecture reviewer
                          ⇄  plan reviewer
             loops until every gate passes, then commits the plan

    BUILD    for each task on the index:
               implement  →  commit

    REVIEW   parity reviewer
             architecture reviewer
             branch reviewer  →  meta-review  →  fix each finding
             the skeptic      →  meta-review  →  fix each finding

    TEST     drive the real app in a real browser,
             one fresh agent per test case

    CLOSE    docs · statistics · self-reported harness defects
        │
        ▼
  branch pushed, ready for a human review
Enter fullscreen mode Exit fullscreen mode

Everything below is that diagram, slowly.

The drop

I don't usually write the file by hand. I open an ordinary session in the repo and type what I want, the way I always did: "add the block-user action to the chat header, same conditions as the mobile app." The session notices that this is a change request rather than a question, offers to run it unattended, proposes a branch name, and on my yes writes my sentence verbatim into the inbox directory. That's the last thing I do.

What the daemon does next matters more than it sounds. It reads the filename to decide which entry point to run and which working-copy strategy to use, because a task prompt, a review of mine and a docs pass are different files taking different routes. It creates a fresh git worktree on a new branch, commits my prompt into it, and launches one headless session in that worktree.

Two small decisions pay off all the way down. The prompt is committed before anything runs, so the run starts with a genuinely clean tree and a first commit that says what was asked. And the session is handed the path to the prompt, never its contents pasted into its instructions, so the planner reads it as untrusted data. A prompt that tries to be an instruction ("ignore the conventions and…") is being read as text by a component that treats it as text, and everything underneath is locked down anyway: a deny floor that wins over every allow, an allowlist of exactly the wrapper scripts a run legitimately needs, no deploy command reachable, no push to a protected branch.

Planning, and why it loops

The run doesn't write code first. It writes a plan, and then it argues with itself about the plan.

plan writer → parity reviewer → architecture reviewer → plan reviewer
     ↑______________ FAIL: revise, re-run every gate _________|
Enter fullscreen mode Exit fullscreen mode

Three different readers, in a deliberate order: cheapest-to-fix-if-wrong first. The parity reviewer asks whether this plan targets the right behaviour. For a port, that means checking it against the mobile implementation, payload for payload and threshold for threshold. The architecture reviewer asks whether each piece lands in the right place and whether the dependencies point the right way. Only then does the plan reviewer grade the plan as a plan: is each task small enough, self-contained enough, actually implementable. A plan aimed at the wrong behaviour is worth catching before anyone spends effort on where its code should live.

The loop runs until all three pass, with a hard cap of five rounds, after which it stops and shows me the disagreement instead of grinding. When it converges, the plan is committed to the branch, which is what makes it resumable and, more importantly, reviewable afterwards. I can read the plan the code was written from.

The plan's shape is the part I'd steal if I were building this myself. It isn't one document. It's a thin index, an ordered checklist of tasks, plus one self-contained detail file per task. Nothing needs the whole plan in context. An implementer gets handed one file.

Building, one piece at a time

Then the run walks that checklist top to bottom. For each unfinished line it dispatches an implementer and hands it exactly two things: its own task file, and the conventions document for the part of the codebase that task touches. Not the plan. Not the other tasks. Not the codebase-wide rulebook.

When the implementer is done, a separate agent commits. That agent does nothing else: it flips the checkbox on the index and stages that change together with the code, so progress and code land in the same commit. You can't have one without the other, which is exactly what you want when a run dies halfway.

That's the whole build phase. It's unglamorous on purpose.

Review, four ways

Now the interesting part. The finished branch goes through several gates with genuinely different failure models, in a fixed order.

Parity re-checks the implementation against the reference implementation. Same question as at plan time, now against real code.

Architecture checks where every new file landed, which part of the codebase owns which responsibility, and what had to accompany the change.

The branch review reads the entire diff against the plan and the conventions, and writes findings graded Must Fix, Should Fix, Nice to Have. Its output is split the same way the plan was: a thin findings index plus one self-contained file per finding. Then a different agent meta-reviews that review before a single fix is applied, because reviewers make things up, and a fabricated finding turns straight into bad code if nobody checks it. Then the fix loop walks the findings index the same way the build loop walked the task index.

The skeptic goes last, and it's my favourite thing in the system. Its premise is that the plan can be wrong, the implementation can be wrong, and the reference implementation can be wrong. It re-runs the earlier gates' checks from the opposite posture (is this new code even reachable, is that cited justification real, was that "intentional divergence" call actually justified) and it files only what the earlier gates missed. It exists because measurement found a class of defect nobody else could catch: a port that faithfully reproduces a bug in the thing it's porting from. Every other reviewer treats the reference as the truth. This one doesn't.

Here's the structural point worth noticing: every one of those review phases has the same shape. Generate findings, commit the index, walk the checkboxes, fix each, commit each. Same loop, same committer, same readiness list. Adding a review gate means adding an index, not adding a mechanism. That's why there are four of them and not one.

Reviewers, by the way, cannot edit files. Their tool allowlist doesn't include it. A reviewer that could quietly fix what it found would quietly fix it, and I'd learn nothing. A reviewer that can only write a finding has to make its case.

Testing the thing that's actually running

Then the run starts the app and uses it. A tester agent drives a real browser through the test cases, which were themselves planned and reviewed earlier in the run by the same writer-and-reviewer loop as everything else. It's the only agent in the whole fleet whose allowlist includes browser tools; every other agent is closed out of that namespace by construction.

The part that needed real effort here is embarrassingly mundane: everything that isn't inside git. Two runs never share a working tree, an index or a branch, because each one gets its own worktree. A dev server, a browser and a set of test accounts are all outside that boundary, and each of them needed its own mechanism.

Ports came first. Each run probes for a free port before it starts anything, counting up from a seed that deliberately sits one above the port a dev server usually takes, so an automated run never fights a human developer for the port they're already using. The run then starts its server on the port it was given and tears down by naming that same port, so a finishing run can't kill a sibling's server. And because a server answering on a port tells you nothing about whose server answered, the run also checks that its own server process is still alive before it trusts the response at all.

Test accounts were the one that bit. Two test sessions running at once happily drove the same test account, signing each other out mid-test and writing conflicting data as the same user. That produces failures that look exactly like product defects, which is the worst kind of false signal: it sends you hunting through application code for a bug that lives in your harness. Accounts are now reserved through a lock set that is machine-global rather than per working copy, which is the only scope that works, because two working copies of the same repository are precisely the pair that would otherwise grab the same account.

The general lesson I took from both: a green test result is only as trustworthy as your certainty about what it was green against.

Closing out

Last phase, in order: update the docs the change affected, write the branch's own statistics report, and then the one I'd call distinctive, write down every problem the run hit with the harness itself. Agents log their own friction into a per-branch intake file that no agent ever reads back. A human folds those into a single list, because for process defects frequency is the diagnosis and one run can't see frequency. There's a whole article in that; it's coming.

Then it pushes and notifies me. That's it. The last mile, opening the PR and requesting review and merging, is mine, deliberately.

The two decisions the rest of it rests on

If you take two things from this, take these.

State lives in git, not in the model's memory. Long agentic runs get their context compacted, crash, and hit account rate limits. So nothing that matters is held in a transcript. Progress is checkboxes in committed markdown at two levels: a phase-level ledger that says which phase, and the detail indices that say which item. A phase flips to done only after its artifact is committed. "Where do I resume?" is therefore computed, by jumping to the first unchecked box, rather than remembered.

That single decision is why pausing, parking to ask me a question, auto-pausing when the account hits a rate limit, and a watchdog restarting a hung run are all the same mechanism seen from four sides. It's also why a run that needs to ask me something ends its session instead of blocking: it writes a question file and exits. Waiting costs nothing. The daemon notices my answer, relaunches the same entry point in the same worktree, and the run picks up at the first unchecked box.

Nobody reads more than they need to. The orchestrating session is forbidden from reading anything substantive: not the plan, not the diffs, not the review reports. Reviewers return a verdict line, not a report. The orchestrator passes a path to the next agent and never opens it. Even routing a task to the right implementer uses a tag on the index line, so the detail file stays unopened. This isn't tidiness. It's the difference between a run that completes hundreds of dispatches and one that suffocates on its own context a third of the way through.

What it deliberately doesn't do

  • It doesn't merge, doesn't open pull requests, doesn't talk to GitHub at all. It ends at a pushed branch.
  • It runs on one machine, one repo per daemon. Nothing spans hosts.
  • It's git only, and Claude-bound today: one engine, no abstraction in front of it yet.
  • Nothing in it turns a design file into code.
  • And the protected-branch guarantee is a backstop, not a floor. There's a git hook that fires however git was invoked, plus a faster string-matching guard above it that any allowlisted interpreter could defeat. A forge-side branch rule would be the real floor. The repo says so in those words, because a limit you've written down is an engineering decision and a limit you haven't is a surprise.

See it before you adopt it

It's Apache-2.0: github.com/firu-daniel/autonomous-sdlc-harness. The plugin carries the agents, commands and flow; the CLI wires a repository, generates the permission profile, and installs the daemon, which are the parts nobody should have to get right by trial and error twice. There's a small example project in the repo carrying the committed artifacts of one real end-to-end run, including the three failed meta-review rounds it took to converge. Reading someone else's run artifacts is the fastest way to tell whether any of this is real.

Next up, one file at a time: how a defect that got past every automated gate becomes a one-line rule that every future planner and reviewer reads before it starts, and why that single file did more for output quality than any prompt I ever tuned.

Daniel Firu builds and operates an autonomous software-delivery harness for Expause, a Flutter and React product.

Top comments (0)