Session postmortem · 2026-08-14
How a frontier model designed, built, and shipped a harness that delegates coding work to free local LLMs — and how the first production run found a real money bug. One day, end to end.
$ localagent run --repo ~/repos/invoicr "Add Vitest unit tests for format.ts…" { "status": "completed", "turns": 12, "usage": { "prompt_tokens": 89294, "completion_tokens": 1824 }, "final_message": "All tests pass (npm run test). Type checking passes." }
91k tokens of real agentic work. API cost: $0.
I run Claude Code on frontier models. They're excellent — and expensive. Meanwhile my Mac Studio sits there with 128 GB of RAM running LM Studio, serving qwen3-coder-next (65k context) and Devstral Small (32k context) for free, four concurrent requests each.
The question that started this: can the expensive model delegate to the free ones?
Claude Code's own subagent machinery only spawns Claude models — there's no "point this subagent at localhost." But nothing stops Claude from driving a local model as a tool: build the prompt, POST it, check the work. The catch is that a bare chat completion can't explore a repo, edit files, or run tests. What was missing was a harness — an agent loop around the local model.
So we built one: localagent, a stdlib-only Python CLI that runs one coding task per process against a local model, inside a git worktree it can't escape, producing a reviewable diff and a full JSONL transcript.
Orchestrator — frontier model
Picks the task, writes a precise prompt, audits the transcript, reviews the diff, re-runs the gates, commits and PRs what survives review.
spends judgment · costs money
Worker — local model
Explores the repo, writes code, runs builds and tests — inside a sandbox worktree, with guardrails on every tool call.
spends electricity · costs nothing
Two rules make this safe and economical:
The economics only work when the review surface is small. If the worker emits 400 lines you must read line-by-line, you've paid most of the cost anyway. Bounded tasks — "write tests for this file, here's the style reference, verify with this command" — are the sweet spot.
About 600 lines of Python 3.9, standard library only — the system Python on macOS runs it with zero setup. One module per concern:
localagent/
__main__.py # CLI: preflight → worktree → run → one JSON object on stdout
runner.py # agent loop: tool dispatch, context trimming, strike limits
llm.py # urllib client for LM Studio's OpenAI-compatible API
tools.py # read / write / edit / list / grep / bash
guardrails.py # path confinement + bash denylist + minimal env
workspace.py # worktree lifecycle, CLAUDE.md injection
transcript.py # JSONL event log, flushed per line (tail -f friendly)
tools API —
send a schema array, get structured tool_calls back. No fragile
"parse XML out of prose" protocol. Verify this with one curl before building anything;
it's the load-bearing assumption.tail -f on the transcript.CLAUDE.md goes into
the worker's system prompt — read from the worktree's checked-out ref, not the caller's
possibly-dirty working tree.The build was the same delegation philosophy, one tier up: a brainstorm settled the safety model before any code; a plan specified ten tasks including the actual code and tests as bite-sized TDD cycles; cheap subagents transcribed while mid-tier subagents independently reviewed every diff against the spec.
The reviews earned their keep immediately — six fix rounds before the PR even opened:
two "never raises" contract violations reproduced with a read-only file and a dangling
symlink, exception-type holes in the HTTP client (IncompleteRead is not an
OSError!), a stale-branch leak on failed worktree creation, crash paths on
malformed server responses. A final whole-branch review then caught what task-scoped
reviews structurally can't: cross-module seams, like the timeout nothing actually enforced.
Then the pull request went through seven rounds of human review — sixteen findings, every one verified against the code before being fixed, several reproduced empirically first, every fix re-gated and answered in-thread.
Findings per review round — converging to clean
{"choices":[{"message":null}]}? Malformed-but-valid
JSON is a whole bug class distinct from invalid JSON.tool_call_id passes your fake-client tests and gets rejected by a strict real
server. The terminal fix was canonicalization: rebuild every accepted tool call into the
exact wire shape before resending.git branch -D after a failed worktree add would delete a branch that existed
before the call. Record provenance; only delete what you created.Task: "Add Vitest unit tests for the six functions in
web/src/lib/format.ts, colocated, following the style of
version.test.ts, verify with npm run test."
The transcript showed genuinely good behavior: the model read the source and the style
reference before writing, and when its negative-rounding expectation failed, it probed real
Math.round semantics with node -e and corrected itself rather than
guessing. Review cost on my side: one 125-line file to read, two gate commands to re-run,
one stray package-lock.json touch to revert.
The tests faithfully pinned existing behavior — which put a wrong behavior in
plain sight. Two review rounds later, round2 — the function that computes
invoice line amounts, subtotals, tax, and totals — had two real defects fixed:
Math.round resolves ties
toward +∞, so a credit didn't mirror its charge: round2(0.125) → 0.13 but
round2(-0.125) → -0.12.Number.EPSILON (2.2e-16) is smaller than the ULP at magnitude 10
(~1.8e-15), so the nudge vanishes in float addition: round2(10.075) → 10.07.
The fix rounds on the decimal the user sees, via a decimal shift —
Number(Math.abs(n) + "e2") reparses the shortest decimal representation, so
"10.075e2" is exactly 1007.5.A test-writing delegation turned into a shipped fix for invoice-money correctness. The delegation didn't create the bug — it dragged it into the light.
Prerequisites: LM Studio (or any OpenAI-compatible server) with a
tool-calling-capable coding model; git; any language with an HTTP client. Prove the
load-bearing assumption first — one curl with a tools array, per model.
Or don't build — clone. The finished harness is MIT-licensed at github.com/JimboSchneider/localagent — stdlib-only, pipx-installable, with the full spec and ten-task plan in docs/. The recipe below is for building your own anyway, which is where the fun is.
The recipe, in the build order that worked:
/repo-evil
starts with /repo. A bash denylist for accidents: sudo,
git push, absolute-path rm, cd .. escapes,
pipe-to-shell. Scrub the environment — your shell's tokens should never reach the
worker's subprocesses.ERROR: string the model can read and self-correct from. Enforce it with
hostile tests: binary files, broken symlinks, read-only files.git rev-parse --git-path info/exclude (correct even in linked worktrees)
to hide your worktree dir.message can be null,
tool_calls can contain garbage, usage can be missing.
Canonicalize accepted tool calls before resending them.Prompting the worker: name the file, name the style reference, name the verification command. Inject the repo's conventions. The plain-text reply is the loop's termination signal — tell the model that's how it finishes.
Reviewing the work: read the transcript summary, read the diff, re-run the gates yourself, revert incidental artifacts, and only then commit — to a PR you review like anyone else's.
Next up: opening it for others — un-hardcoding the launcher, a license, CI, and
packaging so anyone with LM Studio can pipx install their way to a free
local workforce.
The expensive model spent its tokens where judgment lives — design, review, verification — and the free model did the typing. That's the whole idea, and for one day at least, it worked exactly as drawn.
The "sample size is one" caveat aged fast. By evening, four production runs:
cd-escape guardrail blocked it seven times; it recovered
each time and kept its own 130-test suite green. The tool policed its own developer.Also since publishing: the project is MIT-licensed, CI-gated on three
platform/Python legs, and pip-installable — pipx install dirtsimple-agent
(every obvious name was taken or tripped PyPI's similarity filter; the installed
command is still localagent).
Addendum (August 14, 2026, later still): localagent is now dirtywork
(pipx install dirtywork, dirtywork.run). A competitive
survey found the pattern this post describes — a frontier model orchestrating
and auditing cheap local workers — is a lane nobody's driving in; the ecosystem
mostly bets on replacing the frontier model instead. New name, same bet:
frontier models do the thinking, local models do the dirty work.
Next: The Tool Renamed Itself — the same-evening sequel, in which the tool executes its own rename.
Written from the session record, 2026-08-14. Source and design docs: github.com/JimboSchneider/localagent (docs/superpowers/specs and docs/superpowers/plans hold the original spec and the ten-task implementation plan).