A small auto-fix loop that drives an agent CLI (Qwen Code, Claude Code, aider, etc.) against a local or remote LLM, with a deliberately lean per-task context — fresh state per call, no history bloat, only the files you list. That's what makes it usable with a tiny local model that would otherwise choke on the chat history a typical agent framework piles up.
Define bite-sized tasks in TOML, point it at your test runner, and let it iterate until the tests pass.
This is my answer to subsidizing cloud API costs with my local LLM with a workflow I can trust. The workflow I got nailed down is that I would talk with the godfather cloud AI about a feature, say something like full stack notifications for a web app. Cloud AI will read through and create the task toml file and it would kick off the lean-loop script. The local AI would do the bulk of the work autonomously leading to a 5x cloud saving expenditure according to Claude code. The savings scale linearly with feature and task length.
Assumes you already have an OpenAI-compatible LLM server reachable at
http://127.0.0.1:8080/v1 (e.g. llama-server from llama.cpp serving a
local GGUF) and the Qwen Code CLI on
your PATH.
git clone <this repo> lean-loop && cd lean-loop
chmod +x leanloop.py leaners/qwen.sh
cd playground/demo
../../leanloop.py -c leanfile.toml
Watch it iterate through three tasks against the bundled playground/demo/
project until pytest goes green. If the server is on a different host/port
or the model name differs, edit [lean] in config.toml.
for each task in leanfile.toml:
1. read the listed files + git diff
2. send task + context to the wrapped CLI (fresh context, no history)
3. wrapped CLI edits files directly
4. run the test command
5. if tests pass -> next task
if tests fail -> enter fix loop (up to max_iters):
a. compress the error output (tail N lines, strip dep noise)
b. ask the wrapped CLI for a one-sentence diagnosis
(skippable — set `[defaults] summarize_errors = false`
to pass the trimmed output from (a) straight to (c))
c. ask the wrapped CLI to fix it
d. re-run tests; loop
Each model call is self-contained — no chat history is carried between tasks
or iterations. The wrapped agent CLI does the actual file writes;
leanloop.py only orchestrates.
- Python 3.11+ (stdlib only — no
pip installneeded) - An agent CLI on your
PATHthat can edit files autonomously. The shipped wrapper targets Qwen Code CLI; see Other wrappers for swapping in claude / aider / gemini / etc. - A running LLM endpoint that your chosen CLI can talk to (e.g.
llama-serverfrom llama.cpp serving a Qwen GGUF, or a hosted API).
Two options:
Clone and run
git clone <this repo> lean-loop && cd lean-loop
python3 leanloop.py --help
pipx install (gets you a lean-loop command on PATH):
pipx install /path/to/lean-loop
mkdir -p ~/.config/lean-loop && cp /path/to/lean-loop/config.toml ~/.config/lean-loop/
lean-loop --help
- Start your LLM server (e.g.
llama-server -m Qwen3.6-35B-A3B.gguf --host 127.0.0.1 --port 8080). - Edit
config.toml(next toleanloop.py, or~/.config/lean-loop/config.tomlif you installed via pipx) — set[lean] model,base_url,[health] check_urlto match your server. - In your project, run
lean-loop --init leanfile.toml(orpython /path/to/leanloop.py --init leanfile.toml) to drop a sample task config, then edit it: point[runner]at your test command and add[[tasks]]blocks. Seeexamples/for samples. lean-loop -c leanfile.toml- (Optional) Set up the MCP server for file-aware queries against your local LLM — see lean-mcp server below.
Two files, deep-merged at load time:
config.toml(ships with the tool, alongsideleanloop.py) — static, per-user defaults: wrapper binary path, model, server URL, base behavior knobs. Customize once for your environment.leanfile.toml(per-project) —[runner],[[tasks]], and any per-project overrides (e.g.[defaults] source_prefix = "src/"). Anything set here wins overconfig.toml.
Static config discovery: --global-config flag -> $LEANLOOP_CONFIG env var ->
config.toml next to leanloop.py -> ~/.config/lean-loop/config.toml.
An optional MCP (Model Context Protocol) server ships at lean-mcp/server.py.
It serves both sides of the lean-loop workflow:
- The local model (the one lean-loop drives) gets code-intelligence
tools — ripgrep-powered search and a structured test runner — so it can
understand code without reading whole files into its small context.
config.toml'sglobal_messageinstructs it to use them. - The cloud/frontier AI (the one authoring leanfiles) gets authoring tools so it never hand-writes TOML and never runs a broken config.
| Tool | For | What it does |
|---|---|---|
rg_search(query, path, file_glob?) |
local model | regex search across the repo, capped results |
rg_files(query, path) |
local model | matching filenames only |
rg_context(query, path, before, after) |
local model | matches with surrounding source lines |
run_tests(command, cwd?, timeout?) |
local model | run a test command, structured pass/fail counts |
leanfile_spec() |
cloud AI | the authoritative leanfile schema + contract workflow (serves CLAUDE.md) |
create_leanfile(path, runner_command, tasks, ...) |
cloud AI | write a leanfile from structured task data; escaping and syntax are handled for you, result is auto-validated |
validate_leanfile(path) |
cloud AI | report schema errors, missing files, bad runner (same checks as lean-loop --check) |
The authoring loop for a cloud AI is: leanfile_spec() →
create_leanfile(...) → fix any reported errors → lean-loop -c leanfile.toml.
The server needs the mcp Python package (its only external dependency):
cd /path/to/lean-loop
python3 -m venv lean-mcp/.venv
lean-mcp/.venv/bin/pip install mcpOr use the pinned list:
lean-mcp/.venv/bin/pip install -r lean-mcp/requirements.txtdeepseek mcp add lean-mcp \
--command /path/to/lean-loop/lean-mcp/.venv/bin/python3 \
--arg /path/to/lean-loop/lean-mcp/server.py
deepseek mcp validate # confirm it's alive
deepseek mcp tools # should show rg_search, run_tests, leanfile_spec, ...The server reuses config.toml from lean-loop. An optional [mcp] section
tunes the search tools:
[mcp]
rg_max_results = 50 # default; cap on search results per queryAll tools return structured JSON (matches with file/line, test summaries
with pass/fail counts, validation results with errors/warnings lists).
- Python 3.11+
pip install mcp(or use the venv setup above)- A running LLM server on
http://127.0.0.1:8080/v1(or whicheverbase_urlis configured inconfig.toml)
leaners/qwen.sh is a six-line wrapper that execs the qwen binary with
LEAN_* env vars. leanloop.py populates those vars from the merged
[lean] table before each call, so you don't normally touch this file.
The contract for any wrapper is tiny:
- Accept
-p "<prompt>"as a CLI arg - Read connection settings (model, base URL, API key, etc.) from
LEAN_*env vars - Have the wrapped CLI edit files autonomously (it must have native
edit_file/write_filetools — text-only output won't work) - Print non-empty stdout on success
Four sibling wrappers ship alongside qwen.sh for popular agent CLIs —
claude.sh, gemini.sh, aider.sh, opencode.sh. Swap with
leanloop --set-leaner <name> (run leanloop --list-leaners to see all).
Heads up: only
qwen.shhas been exercised end-to-end. The other four were written from each CLI's published docs and have not been smoke-tested against this loop. Flag surfaces move fast — expect to tweak the script for your CLI's exact version before relying on it in a long fix-loop run. PRs welcome once you've verified one.
- Task mode — at least one
[[tasks]]block inleanfile.toml. Tasks run sequentially; tests run after each task. - Direct mode — no
[[tasks]]. Runs the fix loop straight against whatever the test command spits out, fixing whatever breaks first.
- Test runner: any command. Set
[runner] commandandargsto whatever your project uses (pytest,go test,npm test,cargo test,make check, ...). Optionalshell = truerunscommandas a shell line. - Traceback parsing: tuned for Python (
File "x.py", line N). For other languages the script falls back to "compressed error tail + git diff + task files" — fixes are coarser but it still works.
Pure stdlib, no POSIX-only syscalls — runs on Linux, macOS, and Windows
(provided you have your chosen agent CLI and an LLM server reachable there).
The shipped wrappers in leaners/ are bash, so on Windows either run them
through Git Bash / WSL or point [lean] binary at a .bat/.ps1 equivalent.
lean-loop [-c CONFIG...] [-g GLOBAL_CONFIG] [--check] [--task NAME]
[--tasks INDICES] [--json-report PATH] [--init [FILE]]
[--list-leaners] [--set-leaner NAME]
-c, --config— per-project task config (defaultleanfile.toml); multiple configs run sequentially-g, --global-config— static config override--check— validate the config(s) and print the task plan without running anything: catches[[task]]-vs-[[tasks]]typos, unknown keys (with did-you-mean), missingprompt, nonexistentfiles/test_filespaths, and a missing runner. Every normal run performs the same validation up front and refuses to start on errors.--task NAME— run only the named task--tasks INDICES— run only these 1-based task indices, e.g.--tasks 2,3--json-report PATH— write a machine-readable JSON run report (per-task status, fix iterations, error tail on failure, token estimates), rewritten after every task so even an interrupted run leaves a readable report
Tasks (or [defaults]) can also list protected files — usually your
contract tests. lean-loop snapshots them before each task and fails the
task and restores them if the model modifies them, so tests stay a
trustworthy contract: a run can't go green by editing its own tests.
--init [FILE]— write a sample task config (default: stdout)--list-leaners— list wrapper scripts inleaners/and mark the active one--set-leaner NAME— set[lean] binaryin the active static config toleaners/NAME(e.g.--set-leaner claudeto switch toleaners/claude.sh)
Sample leanfile.toml files for different test runners live in
examples/ (pytest, go test, jest).
pip install -e ".[dev]"
pytest
Self-tests live in tests/ and cover the pure-function bits
(config merge, config validation, traceback parsing, frame picking,
preflight). End-to-end runs against a real LLM are exercised by the demo
playground in playground/demo/.
CLAUDE.md at the repo root is the authoring spec for AI drivers (leanfile
schema, contract workflow, gotchas) — it's also what the leanfile_spec()
MCP tool serves.
MIT — see LICENSE.
