On this page
The Experiment
Every so often the models cross a threshold that makes it worth re-asking a question I keep circling: how much of a real build can you hand off and walk away from? Claude Opus 4.8 felt like the right moment to revisit it properly — not a toy, an actual tool, start to finish, unattended.
So I wrote a build spec for something I’d actually use — a local, Azure DevOps-style diff reviewer called loupe. You run it in any git repo, it opens a browser, you leave inline comments on the diff, and it compiles those comments into a structured prompt you can paste into an LLM. The spec was thorough: file structure, naming rules, a 200-line-per-file hard limit, conventional commits, semver, the works.
Then I handed the whole thing to Opus 4.8 — running inside Claude Code — and said “go,” fully autonomous. No step-by-step babysitting. Output a plan, execute it, verify each phase, commit as you go.
It worked. At the end of the session I had a real repo with nine clean commits, 48 passing tests, a strict TypeScript build, a v0.1.0 tag, and a GitHub release. Then I kept going — dark mode, markdown preview, resizable panes, syntax highlighting — and it kept working.
But the autonomy isn’t the interesting part. Anyone can tell an agent to “build me a thing.” The interesting part is the scaffolding around the autonomy — the handful of structural decisions that turn “let an LLM write a codebase” from a coin flip into something repeatable. That’s what this post is about.
What Got Built
loupe is buildless on the frontend (Preact + htm + highlight.js straight from a CDN, no bundler) and Bun-native on the backend (Bun.serve, bun test, TypeScript executed directly). The whole thing runs with bun src/index.ts. There are no runtime npm dependencies.
That stack matters less than how it got assembled.
A Tool, Not a Codebase
Before the how, the when — because it changes how you should read everything that follows.
loupe is a throwaway. It’s a personal tool I built to scratch an itch, and I am never going to maintain it the way I’d maintain something at work. If it breaks in six months I’ll fix it in five minutes or delete it. There’s no team, no roadmap, no on-call, nobody depending on it. That is exactly why handing it to a model and walking away is the right call — and I don’t think it generalizes to production for a second.
I’m still firmly in the camp that production-grade code at your day job needs full commitment: real design, hands on the keyboard, understanding every line because you’ll live with it for years and other people’s work sits on top of yours. Letting a model run unattended through a system people depend on isn’t a workflow, it’s a liability. I’m not handing my employer’s codebase to an agent and going to lunch, and neither should you.
But that’s a narrow slice of all the code worth writing. The vast, boring majority of useful software is little tools — a diff viewer, a one-off script, a personal dashboard, a CLI that saves you ten minutes a week. Genuinely useful stuff you’d never get the time or budget to build by hand, where the worst-case bug is “huh, broken — oh well.” That is where this approach is spectacular: near-zero downside, and the upside is a real working tool that otherwise wouldn’t exist.
loupe is proof of exactly that. A complete, tested, syntax-highlighted, dark-mode diff reviewer with inline comments and an LLM-prompt exporter — the kind of thing I’d never have spent a weekend hand-building — exists now because I could describe it well and let a capable model do the rest while I worked on something that actually needed me. That’s the use case. Not your production codebase. This.
Why the Spec Worked
The framework below didn’t materialize mid-session. It was encoded in the build spec before Opus 4.8 wrote a line. If you take one thing from this project, read that file — it’s most of the difference between an agent that wanders and one that ships.
A few things that spec got right, and why each matters specifically for a long, unattended run:
It pre-authorized decision-making. The first instruction is “Work fully autonomously. Do not ask clarifying questions — make a reasonable decision, document it, and move on. If you hit a genuine ambiguity that needs a different architecture, pick the simpler path.” The single biggest thing that stalls a long session is the agent halting to ask — or guessing and quietly drifting. This turns every fork into a local decision with a default, so the run never blocks waiting on me.
Every constraint is objectively checkable. A 200-line hard cap per file. Conventional commits with a fixed scope list. Strict TypeScript, no any on the contract types. Exact file paths. Per-module coverage tables. None of these need human judgment to verify — the agent checks its own output against them after every step. Across a few hundred tool calls, self-checkable constraints are what keep quality from sliding.
It pinned the contracts with concrete examples, not prose. The spec didn’t say “represent the diff sensibly.” It gave the literal JSON shape, the .review schema, the compiled-prompt format down to the column alignment, and the ADO color palette by hex value. There was nothing to invent at the seams — which is exactly what makes parallel sub-agents safe. Ambiguity at a contract boundary is what makes independent modules drift; concrete examples delete it.
It did the decomposition up front. “One sub-agent per core module plus its test file: diffParser, promptCompiler, reviewStore — near-pure and verifiable in isolation.” The spec drew the module boundaries, so the model didn’t have to discover them while building them. The parallelism was designed in, not improvised.
It demanded a behavioral check, not just a green suite. “Verify each phase runs before starting the next,” plus an explicit end-to-end smoke test: “launch on a real diff, add a comment, compile the prompt.” That last line is the reason the blank-UI bug got caught at all — the spec required proof the thing works, not just that the tests pass.
It said what NOT to build. A whole Non-Goals section — no auth, no database, no bundler, no plugin system — plus a YAGNI rule. Long sessions rot when the agent keeps finding adjacent things to “improve.” An explicit fence around scope is what kept it from wandering off.
The pattern underneath all of it: the spec front-loaded every decision that would otherwise have needed a human mid-run. Contracts, structure, constraints, the verification bar, the scope — all settled before the agent started. What was left was execution, which is the part a model like Opus 4.8 is genuinely good at doing unattended. The autonomy is the model’s; the reliability is the spec’s.
1. Contracts Before Parallelism
The single most important decision happened before any feature code: define the shared contracts, in one file, and freeze them.
src/types.ts holds the diff JSON shape, the .review file schema, and every API request/response body. It got written first, in the main thread, and committed on its own:
export interface DiffLine {
type: "context" | "addition" | "deletion";
oldLine: number | null;
newLine: number | null;
content: string;
}
export interface Comment {
id: string;
file: string;
line: number | null; // new-file line, or null for a file-level comment
lineContent: string | null;
text: string;
createdAt: string;
}
Nothing downstream is allowed to redefine these. The diff parser produces a DiffLine[], the server serves it, the frontend consumes it, the prompt compiler reads it — and they all agree on the shape because the shape is a committed artifact, not a thing each module invents for itself.
This is the move that makes parallelism safe. Once the contract is pinned, independent modules can be built in isolation without drifting, because the seam between them is already nailed down.
2. Sub-Agents as Junior Devs, an Orchestrator That Owns Integration
With the contracts frozen, three core modules — the diff parser, the prompt compiler, and the review-file store — are near-pure and independently testable. Each one got handed to its own sub-agent, in parallel.
The critical detail: sub-agents don’t share memory, and they don’t commit. Each one gets everything it needs up front — the relevant types, the engineering standards, its exact function signature, its test requirements — and returns code plus a passing test file. The orchestrator (the main session) owns every commit, all integration, and final verification.
Why the orchestrator owns commits
If three agents commit in parallel, you get git races and a tangled history. If they only write files and the main thread commits, you get clean, logical, one-concern-per-commit history and a single place where integration actually happens. The sub-agents are the junior devs shipping branches; the orchestrator is the senior who reviews and merges. Neither has to be human.
The same pattern scales out: one sub-agent for the server layer once the API was fixed, one for the buildless frontend. Each had a pinned contract to build against, so none of them needed to see what the others were doing.
3. Phase Gates
The build ran as numbered phases — bootstrap, contracts, core modules, server, frontend, integration — and each phase had to run before the next one started. Not “look done.” Run. bun test green, tsc --noEmit clean, before moving on.
This sounds obvious and it’s the thing people skip. The temptation with an autonomous agent is to let it sprint to the end and then debug the wreckage. Phase gates turn that into a series of small, verified steps where a failure is localized to the phase that introduced it.
The Lesson That Actually Matters: Green Tests Can Lie
Here’s the part I want everyone to take away, because it’s the difference between an agent that looks trustworthy and one that is.
After the frontend phase, everything was green. 48 unit tests passing. tsc clean under strict mode. An end-to-end API smoke test — start the real server, fetch the diff, post a comment, compile the prompt — all 13 checks passing. By every automated signal, the tool was done.
Then the agent launched it in a real browser to look at it. The top bar rendered. The entire diff pane was blank.
The cause was subtle. The buildless frontend uses htm bound to Preact’s h, and one component used JSX fragment shorthand:
// the diff row used <>…</> to return two sibling nodes
return html`<>
<tr class="diff-row">...</tr>
<${LineComments} line=${line} threads=${threads} />
</>`;
A raw htm.bind(h) doesn’t register the fragment shorthand. <> produced a vnode with an empty tag name, Preact called document.createElement(""), and the whole subtree threw — silently, because the browser console capture didn’t surface it. The fix was trivial once located:
// htm returns multiple root nodes as an array; preact renders them as siblings
return html`
<tr class="diff-row">...</tr>
<${LineComments} line=${line} threads=${threads} />
`;
The unit tests were green the entire time the UI was dead
Pure-function logic — parsing, compiling, persistence, routing — was fully covered and entirely correct. None of it touches the DOM. The bug lived exactly in the gap that unit tests and a type-checker can’t see: does the thing actually render in a browser? Verification that stops at bun test would have shipped a tool whose main screen was blank. The real-browser smoke test is the only check that caught it.
This is the whole game. Autonomous agents are very good at making the green checkmarks appear. The discipline that makes them trustworthy is verifying the thing a human would actually look at — launch the app, click the button, read the screen — not just the thing a test runner looks at. Every refinement after that (dark mode, the markdown preview, the side-by-side fix, syntax highlighting) got driven and confirmed in a live browser before it was called done.
Guardrails: Knowing What Not to Do Autonomously
“Fully autonomous” does not mean “do anything.” Two moments drew the line clearly.
The spec literally said to create a public GitHub repo. The agent built everything locally first and paused before publishing — creating a public repo under my account is outward-facing and hard to undo, so it surfaced the decision instead of just doing it. Reasonable default: do all the reversible local work autonomously, confirm the irreversible outward-facing step.
Later, setting up a shell alias, the agent tried to edit my PowerShell $PROFILE and got blocked by a permission wall — modifying a personal shell profile is persistence I hadn’t explicitly authorized. Instead of working around it, it handed me the one-liner to run myself. That’s the correct failure mode: when you hit a boundary, explain and defer, don’t tunnel through it.
The Boring Discipline That Made It Reviewable
None of this would matter if the output were a 4,000-line mystery commit. It wasn’t, because the spec demanded — and the agent held to — the unglamorous stuff:
- Conventional commits, one concern each.
feat(parser): parse unified git diff into structured json,fix(ui): render diff rows without the unsupported fragment shorthand. The history reads like a changelog because it basically is one. - A real semver trail.
0.1.0for the initial tool, a minor bump to0.2.0when the globalloupecommand shipped, a patch0.2.1for docs. Tagged, with a GitHub release. - The 200-line hard file limit, enforced. When a generated test file came in at 237 lines, it got refactored under the limit instead of waved through. Small files keep both humans and agents in context.
A pile of working code from an agent is worth very little if you can’t review it. A sequence of small, typed, conventionally-committed, individually-verified changes is worth a lot, because you can actually trust it incrementally.
The Sharp Edges
A few honest caveats, because this wasn’t magic.
The agent is only as good as the spec’s seams. The reason parallelism worked is that the spec already separated the system into modules with clean contracts. Hand an agent a vague, tangled problem and the same approach produces tangled output faster. The upfront design work didn’t go away; it moved earlier and got more important.
Sub-agents are blind to each other. That’s a feature for isolation and a liability for consistency. Two agents independently picked slightly different conventions in spots, and reconciling them is the orchestrator’s job. You pay for parallelism with integration overhead.
Browser verification is still semi-manual. Catching the fragment bug required actually driving a browser and reading the DOM. That’s automatable, but it isn’t free, and an agent left to its own automated signals would have declared victory. The human-meaningful check has to be in the loop somewhere.
“It compiles and the tests pass” is the floor, not the ceiling. This whole post is a long way of saying that. The most valuable thing I did as the human in the loop was insist the agent look at the running app, not just the test output.
Would I Do It This Way Again
Yes — for this kind of thing: a tool with clean enough seams to specify upfront, and stakes low enough that a bug is an inconvenience, not an incident. The framework generalizes past this one tool: pin the contracts and commit them, parallelize the independent pieces with sub-agents, keep all integration and commits in one orchestrating thread, gate every phase on real verification, and define the things the agent should not do on its own. The autonomy is the easy part. The scaffolding is what makes it ship something you’d actually put your name on.
Just keep it pointed at the throwaway tools and the personal utilities — not the production codebase you’re paid to be careful with. That one still gets my hands and my full attention. This experiment didn’t change my mind about that line; it showed me exactly where it sits, and how much genuinely useful software lives on the safe side of it.
Try It (If You Want)
It’s on GitHub, it’s public, and it installs in a few lines. You’ll need Bun.
git clone https://github.com/codywilliamson/loupe && cd loupe
bun install
bun link # registers a global `loupe` command
# then, in any repo with changes:
cd ~/some-project
loupe # or: staged · a branch name · ref1..ref2
If loupe isn’t on your PATH after bun link (depends how Bun was installed), add a function to your PowerShell profile on Windows instead:
'function loupe { bun "C:\path\to\loupe\src\index.ts" @args }' | Add-Content $PROFILE
. $PROFILE
Either way, loupe parses the diff, prints a localhost URL, and opens your browser — you’re reviewing in a few seconds.
Fair warning, per everything above: this is a tool I had a model build in an afternoon and almost certainly won’t babysit. Fork it, break it, gut the parser, do whatever. No roadmap, no promises, no hard feelings. 🤷♂️
What To Take Away
- Match the method to the stakes. This is for throwaway tools and personal utilities, not production. Code you'll maintain for years and that other people depend on still needs hands-on design and full commitment — unattended autonomy shines precisely where the blast radius is near zero.
- Define shared contracts (types, schemas, API bodies) in one file and commit them before parallelizing anything. The frozen seam is what makes isolated modules safe to build at the same time.
- Sub-agents are junior devs: hand each one a complete brief, let them write code and tests, but never let them commit. One orchestrating thread owns all integration and commits.
- Gate every phase on real verification — tests green and types clean — before starting the next. Don't let the agent sprint to the end and debug the wreckage.
- Unit tests and a type-checker cannot see whether your app renders. 48 green tests sat happily on top of a completely blank UI. A real-browser smoke test was the only thing that caught it.
- Autonomous does not mean unrestricted. Do the reversible local work freely; pause before outward-facing, hard-to-undo actions; and when a permission wall stops you, explain and defer instead of working around it.
- Conventional commits, strict semver, and a hard file-size limit aren't bureaucracy — they're what make an agent's output reviewable and trustworthy incrementally instead of all at once.