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 10x 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
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.
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] [--task NAME] [--init [FILE]]
[--list-leaners] [--set-leaner NAME]
-c, --config— per-project task config (defaultleanfile.toml)-g, --global-config— static config override--task NAME— run only the named task--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, traceback parsing, frame picking, preflight). End-to-end
runs against a real LLM are exercised by the BANDEEZY playground in
playground/BANDEEZY/.
MIT — see LICENSE.
