I Said 'Report in the Morning' and Went to Sleep. It Shipped a Release.
I handed an agent a goal and one sentence, then slept. 23.7 hours of unattended runtime later, Anvil v0.3.0 was on PyPI. Here is what it woke me for, and what it shouldn't have.
Sekou M. Doumbouya
Listen to this article
The views expressed here are my own and do not represent those of any current or former employer.
The last thing I typed before bed was not code. It was a sentence: here is the goal, here is what “done” looks like, keep going until it is, report in the morning. Then I left the fans audible from the next room and went to sleep.
When I woke up, Anvil had shipped. Not a branch. A tagged v0.3.0 release on GitHub, published to PyPI, 32 tasks done across 21 merged pull requests with a hotfix among them, behind a build session of 23.7 hours of wall-clock runtime. I had been present for about two dozen messages of that. The machine had been present for all of it: 1,537 assistant turns, 28.4 million generated tokens, 496 subagents fanned out across 26 background workflows. I drove 26 human messages into it. It did the rest while I was unconscious.
I want to be honest about what that one sentence is actually doing, because “report in the morning” sounds like a flex and it is not. It is a boundary. The whole question of whether you can let a coding agent run unattended for a full day comes down to where you draw that line: what the machine is allowed to decide alone, and what it has to wake you for. I got the line roughly right this time. I also got lucky in places I will not pretend I planned. Both halves are the post.
How I Got to a Sentence I Could Mean
I did not start the day intending to run an autonomous build. I was using Anvil to build Anvil.
That sounds cute, but it is the most useful way I know to work on a tool: drive your own project through it, and the moment it gets something wrong, fix it in place and keep going. A good number of those 21 PRs exist because I hit a rough edge while using the thing and patched it mid-stream. At one point I was fixing an unrelated bug and realized I needed two PRDs open at once, two separate plans live in the same project, each worked independently. Anvil could not do that. It was a real gap, and a structurally annoying one, because giving PRDs their own identity meant touching paths and risking the plan I already had loaded. So I gave myself an escape hatch first (point the database somewhere else, leave the existing one untouched), and the gap I had stumbled into became the headline feature of the release the build was about to produce. The tool surfaced its own next requirement by being used.
Then I did the part I actually love. I wrote the new PRD, and Anvil interviewed me for it, one question at a time, until the thing was actually specified instead of vaguely intended. It ran analysis over what we had written, surfaced the gaps, made me answer the open questions, scored each task for difficulty, and right-sized the work so each piece had a real chance of succeeding on the first try. What came out was a clean backlog: not a wish list, a queue of tasks that were each small enough to finish and clear enough to verify.
I ran the first few by hand, between other things. They went well. And then it got late, and I had the thought that this whole post turns on: this is Git. At the end of the day, every one of these steps is something I can revert. So instead of feeding the agent one task at a time and watching each one, I told it to repeat the exact process I had just run by hand, the one that was working, over every remaining task, and to write me a report when it was done. Claim the next task, take a lease on it, execute it, review it adversarially, fix what the review found, merge, repeat. Then sleep.
The Loop Has to Verify Itself or You Cannot Leave
You cannot walk away from work you have to trust on faith. The only reason “report in the morning” is not reckless is that Anvil does not record work as done because a model said so. This is the rule the whole system is built on, the same one I keep coming back to from the other end of a runtime port: a claim is “it works,” evidence is an exit code. An agent claiming a task is a database row with a lease and a heartbeat. Completion is gated on attached proof. If the proof is not there, the task is not done, no matter how confident the prose around it sounds.
The reusable unit overnight was a workflow I think of as the task-cycle: implement one task, verify it, review it adversarially, fix it. I am a genuine believer in the adversarial part. Spinning up a cluster of independent reviewers whose job is to attack the work and each other, rather than one reviewer nodding along, surfaces things a single pass never will. That cycle ran 18 times across 361 of those subagents, about 19.2 million tokens. A separate review family ran 8 times, 135 agents, including a 53-agent pass whose only job was recall: ten different finder angles hunting for what the per-task reviews had missed.
The orchestrator on top of all that stayed small on purpose. About 90 percent of the generated tokens ran inside the background workflows; the main loop spent 2.77 million, roughly a tenth, almost all of it on git, CI, and re-running the full test suite. Look at the shape of what it kept for itself: of 535 main-loop tool calls, 314 were Bash, 105 edits, 54 reads, 28 workflow launches, 5 questions back to me. That is not the profile of a thing doing the work. It is the profile of a thing dispatching the work and checking the result. A 23-hour run stays coherent precisely because the thing holding the thread is not also doing the heavy lifting. It is not multitasking. It is multiplication.
I had watched the opposite failure a week earlier on a local box, where a 32K serving window kept compacting agents out of their own context until they forgot what they were building. The fix there was VRAM. The fix here was delegation. Same disease, treated at a different layer. And the ledger is what made the invisible 90 percent auditable: every claim, every lease, every piece of evidence appends to a log, and replaying that log from an empty database reconstructs the exact state. The model churned. The record kept the count.
The Five Things It Woke Me For
Here is the part that matters most, and it is short, which is the point.
Across 23.7 hours, the agent escalated to me five times for an actual decision:
- Merge the risky held PR.
- Merge T022.
- Merge PR #93.
- Merge PR #101.
- Cut the release.
Look at that list. Every one is irreversible, or close enough that the difference does not matter at 3 a.m. A merge to main is a public fact. A tagged release goes to PyPI, and somewhere a stranger runs uv tool install anvil-state against it. These are exactly the calls a machine should not make alone, not because the machine is dumb, but because the cost of being wrong is borne by people who did not consent to the autonomy. Reverting a bad commit on a branch is cheap. Un-publishing a release that others have already installed is not a thing you get to do.
Everything reversible, the machine did alone. Writing code, running tests, opening PRs, re-running the suite, fixing what the suite caught: all of it is undoable, so all of it ran unattended. The boundary was not “hard versus easy.” It was the same “this is Git, I can revert” instinct that sent me to bed, formalized. Reversible versus not. That distinction is the whole operating manual for letting an agent run while you sleep.
The Merge That Was Clean and Broke Anyway
The sharpest lesson came from one of those five wake-ups. PR #93 had been held a long time for a dedicated adversarial review while the loop kept merging later work on top of it. By the time I approved it, GitHub reported the merge as clean. The textual check was green. I merged it. It broke main.
Here is the mechanism, because the mechanism is the lesson. PR #93 and an earlier task’s PR, #96, had both edited the same function, serialize_state, on independent bases. GitHub merges by comparing text. The lines #93 touched and the lines #96 touched did not overlap, so GitHub reported no conflict and offered the green button. That report was accurate. It was also useless. Because the conflict was not textual. PR #96 had added a test stub, a placeholder standing in for a real collaborator, and that stub did not implement methods that #93’s new serialize_state now called. Against #93’s own base, those methods existed. Against the base that already included the stub, they did not. Neither change was wrong in isolation. Both passed their own CI. The defect lived only in the union of the two, and the union was the one state nobody had tested until both were on main. It needed a hotfix, PR #97.
This is what the merge button quietly does at machine scale: it reverts to taking a claim. “Mergeable” is GitHub asserting “it works,” and the green is so clean and so confident that it is easy to accept as evidence. It is not evidence. It is a claim about text. The exit code of the full suite on the real merged tree is the evidence. The only reason that night ended in a working release instead of a broken one is that the orchestrator re-ran the full suite against the merged main after every single merge, all 21 of them, instead of trusting the button. Evidence over claims does not get to take the night off because GitHub drew a green circle.
And now the honesty marker, because this is where I have to resist my own narrative. We were lucky the conflict was loud, not that we caught it. The break surfaced as a stub missing a method, a hard, immediate failure the suite caught on the first run. Had those same two edits to serialize_state produced output that was subtly wrong but still passed every assertion, the suite would have gone green and I would have shipped the bug to PyPI in my sleep. I caught it because it was loud, not because my net is fine enough to catch the quiet version. It is not. A long-held PR accumulates semantic drift against a moving main, and no textual conflict check, not git’s and not GitHub’s, can see a semantic conflict. I am telling you the system worked, and in the same breath telling you it worked partly by chance.
There is a harder version of this that complicates the neat boundary I just drew, and I have to sit with it. The hold was the problem. I had configured #93 to wait for my sign-off while the loop moved on to the next tasks, and several of those tasks branched off it, so by the time I looked, #93 was carrying a stack of descendants on top of a base that had quietly moved underneath all of them. The git state got genuinely weird, and the semantic break was a direct product of that wait. The honest reading is that if I had not inserted the gate at all, and had instead spent that same effort writing the verification steps to close #93 out cleanly, it most likely would have merged itself, unattended, without the mess. The human checkpoint did not only slow things down. It manufactured the exact drift it was meant to guard against. So the rule needs a second clause: keep the irreversible calls, yes, but never let dependent work pile up behind a held one, because a gate with a queue stacked behind it is how you turn a safe pause into a moving target.
Where Luck Did the Work I Wanted Skill to Do
Since I am counting the luck, let me count the rest of it, because a retro that only lists wins is marketing.
API 529 overloads killed review-workflow subagents mid-run more than once. Retryable, annoying, wasted wall-time. But notice where they struck: review work, which you can simply run again. Never a merge, never the release, the operations you cannot take back. If a 529 had landed during the push of a release tag instead of during a recall pass, this would be a different and worse post. I did not engineer that isolation. The dice just did not come up that way.
Then my favorite, because it is so dumb. The version bump from 0.1.2 to 0.3.0 went in as a blunt sed across files. That string, 0.1.2, also lived inside an unrelated dependency. The sed happened to hit exactly one occurrence per file, and the dependency’s copy dodged it by pure coincidence of layout. It worked the way a coin lands heads. I would not write it that way on purpose.
The cleaner failures were about visibility, not correctness, and they are the ones I will fix first. Four times during the night, in my own check-in messages, I asked some version of “how is it going, is it stuck?” Four. The autonomy was real; the legibility was not. The work was happening, and I had no window into it without interrupting to ask. That is the honest tax on delegation: you trade visibility for scale. Separately, some workflows ran longer than fifteen minutes, outlived their claim leases, and Anvil did exactly what it is built to do: it reaped the expired claim and quietly reverted the task to ready, as if no one were working it. Correct behavior punishing an incorrect setting. The fix was a longer lease; the lesson is that the safety mechanism cannot tell a dead agent from a slow one, and you have to tell it. Two of about 28 workflow launches never ran at all. And the whole session opened on a stale handoff note and spent its first two turns just reorienting to reality. The first thing a 23-hour run did was get its bearings, badly.
None of this is fatal. All of it is the texture of what “unattended” actually looks like once you stop romanticizing it.
The Release Arc Was the Boundary Moving Outward
It is worth saying how Anvil reached a v0.3.0 I would let run overnight, because the release history is the same boundary lesson in slow motion. v0.1.0 was the initial public beta. v0.1.1 made it installable as a normal package, uv tool install anvil-state, dropping the git-checkout dance (the bare name anvil was taken on PyPI, hence anvil-state; the install puts both anvil and anvil-mcp on your PATH). v0.1.2 made the Claude Agent SDK the default provider, so the tool rides a flat-rate subscription instead of a per-token key, because Anvil is capacity-bound, not per-token-cost-bound, a distinction I learned the hard way against a rate limit. v0.3.0, the thing the overnight run produced, added the multi-PRD revisable state I had stumbled into needing that morning.
Each release moved the trust boundary outward, and made it safer to hand the next one more rope. v0.3.0 is still labeled beta, still pre-1.0, and I am keeping that label honest: the core loop works today, the surfaces may still change. I would not let it cut its own release. But I would, and did, let it build one and wake me to sign off.
The Boundary Is Reversibility, and That Does Not Change
I keep being asked whether agents are “ready” to run unattended, and it is the wrong question, because it treats autonomy as a property of the model. It is not. It is a property of the loop around the model and the line you draw through it.
A day of runtime taught me the line is not “hard versus easy” and it is not “trusted versus untrusted.” It is reversible versus irreversible. Let the machine do everything it can undo, and gate everything it cannot on a human awake enough to own the consequence. The five times Anvil woke me were the five places the work left the realm of things you can take back. That is not a limitation of today’s models that next year’s will erase. A release that strangers have installed is irreversible no matter how good the model gets, so that boundary survives every upgrade. The models will get better, and the reversible territory you can trust them with alone will grow. The other side of the line, the part you keep for the human, is defined by consequence, not capability, and it does not move.
Build the loop so it verifies its own reversible work. Keep the irreversible calls for yourself. Then you can write “report in the morning,” and mean it, and sleep.
Co-authored with AI, based on the author's working sessions, dictations, and notes.
Explore the source
fakoli/anvil
This article discusses an open-source project. Star it, fork it, or open an issue.