Session postmortem · 2026-08-14

Building localagent

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.

01

The pitch

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.

02

The division of labor

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:

  1. Nothing the worker does touches your real checkout. Every run gets a fresh git worktree on its own branch. No auto-commit, ever. The deliverable is an uncommitted diff plus a transcript.
  2. The review gate is the real safety boundary. Runtime guardrails — path confinement, a bash denylist, a scrubbed environment — block accidents, not adversaries. What actually protects you is a competent reviewer reading the diff and re-running the tests before anything merges.

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.

03

What localagent is

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)

Key mechanics

04

How it was built

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

5
R1
3
R2
1
R3
1
R4
1
R5
2
R6
3
R7
0

Review heuristics worth stealing

05

The first production run

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."

12turns, 38 seconds of loop time
$091k tokens, all local
25tests written, all passing
0guardrail events

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.

And then the kicker

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:

  1. Asymmetric negative rounding. Math.round resolves ties toward +∞, so a credit didn't mirror its charge: round2(0.125) → 0.13 but round2(-0.125) → -0.12.
  2. The epsilon trick silently fails past ~$10. 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.
06

Build one yourself

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:

  1. Transcript first (JSONL, flushed per line). Observability before behavior — you will debug the loop by reading this file.
  2. Guardrails second, as pure functions. Path resolution that follows symlinks and compares path components — not string prefixes; /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.
  3. Six tools are enough: read (numbered, windowed), write, exact-match edit, list, grep, bash. Tools never raise — every failure returns an ERROR: string the model can read and self-correct from. Enforce it with hostile tests: binary files, broken symlinks, read-only files.
  4. Worktree lifecycle: salted-slug branches, provenance-aware cleanup, and git rev-parse --git-path info/exclude (correct even in linked worktrees) to hide your worktree dir.
  5. The loop, with all three bounds from day one, and defensive parsing of every response field — assume message can be null, tool_calls can contain garbage, usage can be missing. Canonicalize accepted tool calls before resending them.
  6. A machine contract: one JSON object on stdout, statuses as an enum, exit codes, everything else on stderr — wrapped in one exception boundary so the contract survives even your own bugs.
  7. Two test suites: a fast one with a scripted fake client, and a live-marked one against the real server, including one true end-to-end run.

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.

07

Honest limitations, and what's next

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.

1 daydesign → build → review → production
133tests green at merge (130 unit + 3 live)
16review findings — all verified, fixed, re-gated
the rounding bug that made it all worth it

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.

08

Update — the same evening

The "sample size is one" caveat aged fast. By evening, four production runs:

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).