需要 Node.js 22.19+,用 bun 全局安装时运行时仍依赖 Node;本地 provider 即使被忽略也必须设置 LLAMACPP_API_KEY 等环境变量为 noop;全局 pi install 的扩展默认不会加载,需放进 ~/.config/little-coder/extensions/ 或用 --with-pi-extensions;本地模型需自行下载 GGUF,图像附件还需额外下载 mmproj。
little-coder
itayinbarrApache-2.0活跃维护面向小型本地大模型的编码智能体测试框架,基于终端基准评估代码生成与工具调用能力,方便对比不同小模型的实际表现。
A harness optimized to smaller LLMs
curl -fsSL https://raw.githubusercontent.com/itayinbarr/little-coder/main/install.sh | bash
npm install -g little-coderREADME 未给出统一显存要求;示例用 llama.cpp 以 MoE 方式把 22 GB 的 Qwen3.6-35B-A3B 跑在 8 GB 显存上(专家放内存、注意力放 GPU),并给出 CUDA 编译参数(如 Blackwell 用 120)。也可用 Ollama、LM Studio 或云端模型。
专为小型本地模型调优的编码 agent,基于 pi 构建,让笔记本上的小模型也能可靠改代码
面向小型本地大模型的编码智能体测试框架,基于终端基准评估代码生成与工具调用能力,方便对比不同小模型的实际表现。
核心能力
- 以 pi 为依赖叠加 30 多个扩展和 30 个技能文件
- 启动时用 --no-extensions 只加载内置扩展集,冷启动上下文约 7k token
- Plan Mode 先调研提问出计划,再切换执行模型实现
- dispatch 派生子会话做只读调研,默认串行避免争抢本地模型
- 按阶段分配模型,规划用大模型、实现用小模型
容易踩的坑
- v1.20.0最新正式版2026-09-18 · 2026-09-18 22:16 UTC
Fixed
- A multi-line
editno longer deadlocks the run (#127 by @brlucasdx). pi already recovers aneditsargument sent as a JSON string instead of an array, and that recovery is a bareJSON.parsein atry. It fails on the single most common way a small local model writes one: raw newlines insideoldText/newText, which is what multi-line code is.JSON.parsethrowsBad control character in string literal, the emptycatchswallows it,editsstays a string, and schema validation refuses the call withedits.0: must be object. brlucasdx counted six in one session. The consequence is worse than a failed call:writeis refused for a file that already exists, so a model that had diagnosed the bug correctly had no way left to deliver the patch at all. This cannot be fixed from an extension (pi's loop runsprepareArgumentsthenvalidateToolArgumentsthenbeforeToolCall, so validation has already rejected the call before anytool_callhook sees it), so it is apatch-pi.mjspatch, applied to both copies of the edit tool pi ships. Control characters inside string literals are escaped and the parse retried; structural whitespace between tokens is left alone, and an escaped\"does not flip the scanner out of the string, which is exactly the case in code being edited. - Behind a router, each model keeps its own context window (#121 by @araujoigor). Two bugs, one line apart. The probe asked
/v1/modelsaboutentry.models[0].id(array position, not the model the user declared as default), and thenwithContextWindowstamped whatever came back onto every model. araujoigor's llama-swap setup has 64k/128k/256k presets of one model with the 64k listed first and the 128k declared default: all three registered as 64k, and the default compacted at half its real window. Nothing was logged, because the probe "succeeded". The window is not a readout: it drives read-guard truncation and the whole context budget. The declareddefaultis now threaded throughmodels.json(splitting on the first/only, since llama-swap preset ids contain slashes), and a router listing is stamped per model from its ownmeta.n_ctx/--ctx-size, with no blanket fallback: a model the router cannot describe keeps the number the user chose rather than another model's measurement. A direct llama.cpp server is unchanged: it serves one model, so its/propsn_ctxreally is every model's window. The swap-time re-probe (#54) now re-stamps only the model being selected, and a selection of a model we never registered is ignored instead of emitting a "context window updated" notice for a change that did not happen. One/v1/modelsfetch now answers all three questions the startup path used to ask separately. - A tool call in prose is no longer "recovered" (#96 by @cal101). Working on JS, cal101 got
harness intervention: the model wrote 1 tool call(s) as text [myCustomAction], followed, delightfully, by the model replying that it had not done anything. It had not. #117 narrowed the nested shape by also requiring an argument key, but a flat object still matched on"name"alone, which is the shape of a config blob or an array of records. The reliable discriminator is not the object's shape, it is whether the tool exists: the nudge asks the model to re-issue the call natively, and a tool that is not registered cannot be issued natively by anyone, so for an unknown name the intervention was not merely a false positive but an instruction the model could not carry out. Names are now checked againstctx.getAllTools()(t
- A multi-line
- v1.19.02026-08-29 · 2026-08-29 15:48 UTC
查看更新说明
Fixed
little-coder -psurvives a compaction instead of losing the run (#115 by @Or1j1n). v1.18.0 stopped the crash in #108 but not the thing it was a symptom of: under-p, a long task that the TUI completes exitedRequest abortedwith code 1 and empty stdout. pi'scompact()is the manual path and its first act isawait this.abort(), and print-mode reads the run's verdict straight off the last message (if (stopReason === "error" || stopReason === "aborted")), so aborting the run discards the answer. There is no safe way to call it from inside a headless run at all: pi holds_isAgentRunActivetrue for the whole of_runAgentPrompt, soabort() -> waitForIdle()cannot resolve while the run is on the stack. Calling it fromturn_startaborts the run; calling it fromagent_enddeadlocks instead, which is not a guess (a run hung past 400s before that approach was abandoned). pi's own threshold compaction has neither problem, because it runs inside_handlePostAgentRunand ends withreturn this.agent.hasQueuedMessages(), which the caller turns intoagent.continue(). So headless no longer compacts at all: it queues the continuation fromsession_compact, and pi carries the run forward inside print-mode's own await. The mid-run watchdog stays TUI-only, which is where #59 needed it and where Or1j1n confirmed it already works. Measured end to end against Qwen3.6-35B-A3B: before, the answer was lost; after, one compaction, two user messages (the prompt and the queued continuation), exit 0, correct answer.- The compaction resume no longer errors with "Agent is already processing a prompt" (#114 by @guppy42). The resume went out as a bare
sendUserMessage, which routes throughprompt()and throwsAgent is already processing. Specify streamingBehavior ('steer' or 'followUp') to queue the message.whenever the agent has not fully stopped. pi catches that and surfaces it as an error, which is what showed up mid-compaction. It is sent as afollowUpnow, so it queues; and a queued message is exactly what the #115 fix needs, so the two were one change. - A bare-JSON tool call whose arguments are an object is recognised again (#117 by @mth-farias). The recovery pattern was
/\{[^{}]*"name"\s*:\s*"(\w+)"[^{}]*\}/g, and[^{}]*excludes braces outright, so it could never match{"name": "read", "arguments": {"path": "foo.py"}}, the single most common way a model writes a call as text. Because the "re-issue this natively" nudge is driven off this list, the miss was silent: nothing ran and nothing was reported. It now scans brace-balanced objects (the same scanner the #102 fix introduced), and the argument key may beinput,parameters,argumentsorargs, including OpenAI's JSON-string form. The nested shape is only accepted when the object also carries an argument key, so prose containing a config blob with anamefield is still not read as a tool call (#96's failure direction). - A sub-coder tracker can no longer crash the agent after a session is replaced (no issue; found while fixing #119).
SubCoderTrackerholds actxand toucheshasUI/ui.setWidgetfrom an animation timer and fromend()in afinally, and every accessor on an invalidated ctx throws./new,/clearand (since v1.18.0)/implementcan all replace the session while a dispatch is in flight. This is the third instance of one bug shape in a month, after #108 and #119, so it is now a shared helper:_shared/safe-ctx.tsswallows exactly pi's stale-ctx error and rethrows everything else, so a real bu
- v1.18.02026-08-22 · 2026-08-22 11:39 UTC
查看更新说明
Fixed
- A compaction that settles after the session is gone no longer takes the process down (#108 by @heinrichI). The context watchdog reported its result through the
ctxcaptured at the turn that fired the compaction, and pi invalidates the whole extension runtime ondispose(), soctx.uiandpi.sendUserMessage()throwThis extension ctx is stale after session replacement or reloadfrom that point on. pi invokes the compaction callbacks from a floatingvoid (async () => …)(), which turned that throw into an unhandled rejection and a hardexit 1. heinrichI hit it in a sub-coder, where the headless child disposes the moment the agent settles, so a compaction fired near the end of a run was racing teardown by construction. The UI handle is now captured while the ctx is known-good and every post-compaction use of it is best-effort: nothing the watchdog wants to tell you is worth crashing over. - "Already compacted" no longer halts the run (#109 by @guppy42, #91 by @charly1r, also reported by @cal101). pi's
compact()reports through one promise with two outcomes, and the #68 loop guard treated every rejection as "compaction is futile, pause". Two of the three rejection shapes are not that.Already compactedmeans another compaction landed first and pi'sprepareCompactionfound the branch's last entry is a compaction: the context is compacted, only our call lost the race, so pausing there stranded a mid-task run at the prompt behind a "could not proceed" warning until the user typed "resume".Compaction cancelledmeans the run was aborted or the session is being torn down, which is not a failure to report at all. Outcomes are classified now:Already compactedresumes the run exactly like a successful compaction, a cancellation is silent and leaves the watchdog armed, and only a real failure (Nothing to compact (session too small), a provider error) still pauses. - A turn boundary during an in-flight compaction can no longer start a second one (found while fixing the above; no issue). The
compactingflag was cleared at everybefore_agent_startso a lost callback could not wedge the watchdog off permanently. But pi'scompact()aborts the run and reconnects the agent while it is still summarizing, which makes a genuine in-flight compaction one of those boundaries. Clearing the flag there let a secondcompact()fire on top of the first, and the one that lost is exactly theAlready compactedabove. An outstanding-call counter now means only a stale flag is dropped. - A
/dev/nullredirect butted straight up against a chain operator is no longer read as a file write (#107 by @ashalliants, a regression of #87). The write guard's word splitter broke on whitespace and redirects but not on control operators, sofind … 2>/dev/null; find …parsed its target as/dev/null;(not a path the device exemption knows), and an ordinary two-findone-liner was refused as an unsafe write. ashalliants' diagnosis was exact, and the same hole was open for2>/dev/null&&,||,|, and a closing subshell paren, none of which need a space in front of them.;,&,|,(and)now end a word like>and<already did, with fd duplication (2>&1,>&2,>&-) checked on the raw text so it is still recognised for what it is.
Changed
- Approving a plan saves it;
/implementruns it (#105 by @dcazrael, for [#98](https://github.co
- A compaction that settles after the session is gone no longer takes the process down (#108 by @heinrichI). The context watchdog reported its result through the
- v1.17.02026-08-15 · 2026-08-15 22:45 UTC
查看更新说明
Added
- Qwen3.8-27B (dense + MTP) in the shipped registry. Its NextN head is in the GGUF (
qwen35.nextn_predict_layers=1,blk.64), so MTP speculative decoding works — measured draft acceptance ~0.87. Measured on an RTX 5070 Laptop (8GB) withUD-Q4_K_XL: 6.42 tok/s at 32k context,-ngl 18, 7042MB; 6.72 tok/s at 16k with-ngl 20. Being dense, it has no experts to park in RAM, so there is no--n-cpu-moetrick and it runs about 7× slower thanQwen3.6-35B-A3Bon the same card — the quality option, not the fast one. One trap worth knowing: at 32k,-ngl 20passes/healthand then generates zero tokens. It fits in VRAM with nothing left to compute, so a server that started is not a config that works. Launch script:run/qwen38-dense.sh. - README sections for background jobs and per-phase model selection, both shipped in v1.16.0 without docs.
- Qwen3.8-27B (dense + MTP) in the shipped registry. Its NextN head is in the GGUF (
- v1.16.02026-08-15 · 2026-08-15 22:06 UTC
查看更新说明
Added
-
Background shells that wake you on job events, not on a timer (new).
bashblocks the turn until a command exits, so a long job either freezes the session or gets polled — and polling a six-hour fine-tune every five minutes is 71 wasted turns on a machine where each one costs real seconds.ShellStartruns a command in the background and returns immediately; you declare what is worth interrupting you for and the harness stays silent until it happens:{"name": "ShellStart", "input": {"command": "python train.py", "label": "finetune", "wake_on": {"match": ["Traceback", "CUDA out of memory", "val_loss="], "every_n_matches": 10, "silence": "15m"}}}Wake rules are
exit(default on),match(regex, falling back to literal text),silence(stalled after producing output), andevery_n_matches(throttle a chatty pattern). Urgency picks the delivery lane: a crash or an error-ish match interrupts the current turn, a clean exit or a milestone waits for the tool calls in flight, routine output rides along with the next turn. Wake payloads are bounded — an excerpt plus the exit code, never the whole log — withShellLogto page deeper on demand. AlsoShellList,ShellSend(stdin, for a REPL or a prompting installer), andShellStop. A footer line shows what is running.Jobs may outlive a turn, never the session:
session_shutdownand every catchable signal reap them, and because SIGKILL is catchable by nobody, each job additionally carries a watchdog that kills its own process group when little-coder's pid disappears. Jobs run in their own process group and are signalled as a group, sopython train.pyunder a shell dies with it rather than being orphaned holding the GPU. -
Per-phase model selection (#61 by @cndjonno, with @cal101). Plan on a big model, implement on a small one.
/plan-modeland/action-modeltag them (with autocomplete and fuzzy matching, so/action-model 9bresolves),/phase-modelsshows the state, andmodels.jsonsupplies defaults viaplanModel/actionModel. The tags are live session state rather than launch config, because the use case that motivated this is swapping planners mid-session to A/B them. Entering Plan Mode switches to the plan model; approving a plan hands over to the action model./model-handover manualturns the automatic switching off — worth knowing that on a single local backend a handover evicts and reloads weights, so "never switch for me" is a performance choice as much as a taste one. Untagged phases use the active model, so an unconfigured session behaves exactly as before.
Fixed
- A refused shell command now says what to do instead of just what was refused (#94). Observed live: a refusal on
./build.shwas followed bybash ./build.sh, thensh ./build.sh, then a successfulpython3 -c "subprocess.run(...)"— three wasted turns and the guard defeated anyway, since interpreters are themselves whitelisted. The refusal now names the evasions not to attempt, points atedit/writefor anything that does not need a shell, and gives the user the actual remedy (LITTLE_CODER_BASH_ALLOW="<cmd>"). Same scenario after the change: refused once, the rest of the task completed, the remedy reported, no bypass attempted. This is the token-burn half of #94; the whitelist's porousness is unchanged and still open there. ShellSessionno longer claims a persistence it does not have. It advertised "cd, env vars, and shell state persist across calls", which is true only under Terminal-Bench's tmux backend. Locally the backend isexecSync— one process per call — so acdappeared to work and silently did not apply to the next call. The description is now computed per backend, and points atShellStartfor anythin
-
用户点评
登录用户的真实使用记录,可按版本和硬件筛选还没有人点评。第一个说出真实体验的人最有价值。
little-coder
A coding agent tuned for small local models, built on top of pi.
The research story behind all this — why scaffold–model fit matters, how a 9.7 B Qwen beat frontier entries on Aider Polyglot, and what the load-bearing mechanisms actually do — is written up on Substack: Honey, I Shrunk the Coding Agent. Start there if you want the "why"; stay here for the "how".
How it relates to pi
pi is the minimal substrate — agent loop, multi-provider API, TUI, session tree, compaction, extension model. Four built-in tools (read / write / edit / bash) and a ~1000-token system prompt.
little-coder is pi + 30-odd extensions + 30 skill markdown files + a Python benchmark harness. It doesn't fork pi or shadow its CLI — pi is a plain dependency in package.json, and everything little-coder-specific lives under .pi/extensions/, skills/, and benchmarks/. It ships no npm install scripts; the launcher does everything at launch time.
The launcher runs pi with --no-extensions and wires in exactly the bundled set. That's what keeps the cold-start context around 7k tokens and makes behavior predictable — the set that loads is the set that ships, and nothing in your working directory changes it mid-task. The consequence is that a globally pi install'd package won't load inside little-coder by default, because pi install registers into pi's settings and --no-extensions skips those.
You have three opt-in ways around that, none of which change the default: drop your own extensions in ~/.config/little-coder/extensions/, point LITTLE_CODER_EXTRA_EXTENSIONS at files anywhere, or relaunch with --with-pi-extensions to let pi discover its own. Run /extensions to see what's loaded. Full guide: Extending little-coder. (Themes are unaffected — pi themes have always loaded.)
If you've never used pi, it's useful to skim pi.dev first — the rest of this doc assumes pi's model of --agent-import-path, --mode rpc, and .pi/extensions/ auto-discovery.
Install
One-line install (Node.js 22.19+ required):
curl -fsSL https://raw.githubusercontent.com/itayinbarr/little-coder/main/install.sh | bash
Or with npm directly:
npm install -g little-coder
Or with bun:
bun add -g little-coder
That's the whole install. No clone, no npm install in a workspace, no PATH fiddling. little-coder is now on your PATH and works from any directory.
Note for
bun add -gusers. The launcher (bin/little-coder.mjs) is a Node.js script with#!/usr/bin/env nodeat the top, so Node ≥ 22.19 still has to be on your PATH for the binary to start — bun is fine for installing/updating the package, but the runtime is Node. If you want a fully node-less setup, replace the shebang in$(bun pm bin -g)/little-coderwith#!/usr/bin/env bun.
Run
cd ~/your-project
little-coder # launches the default model (see below)
little-coder --model llamacpp/qwen3.6-35b-a3b # or name one explicitly
This is the canonical setup little-coder is tuned for: a local llama.cpp server hosting Qwen3.6-35B-A3B. See Local model setup (optional) below for how to serve it.
Bare little-coder (no --model) launches the default model declared in models.json ("default": "llamacpp/qwen3.6-35b-a3b" out of the box), printing its friendly name at startup. This only kicks in on a first run — once you pick a model in-session, that choice sticks and the default never overrides it. Change the default with a default key in your user override file. See Configuring models.
Cloud models work the same way:
little-coder --model anthropic/claude-haiku-4-5
little-coder --model openai/gpt-4o-mini "What does this codebase do?"
little-coder --model ollama/qwen3.5 # local Ollama
little-coder --model lmstudio/local-model # local LM Studio (whatever model you have loaded)
little-coder --list-models # see everything pi knows about
The agent uses the directory you launched it from as its working directory — Read / Write / Edit / Bash operate on your project, not on little-coder's install path.
Interactive features
- Plan Mode — press ctrl+q to toggle (a
◆ PLAN MODEindicator shows below the input), or launch with--plan-mode(LITTLE_CODER_PLAN_MODE=1) to start there. Submit a request and little-coder researches it with sub-coders, asks you 1-3 clarifying questions (each with suggested answers and a free-text option), then writes a plan in the chat instead of editing anything. Approving it saves the plan to.pi/approved-plan.mdand stops there;/implementis what switches to the action model, opens a fresh session seeded with the plan, and starts the work, so the research and Q&A that produced the plan do not eat the context the implementation needs (#98). Esc cancels a plan mid-run. (shift+tab stays pi's thinking-level cycle.) - Deep Research — press f2 (or run
/deep-research <topic>) to scope a topic into a research brief, fan out read-only research sub-coders, and get back one cited markdown report, saved next to your working directory. The research children run in an ephemeral scratch directory, not your project, so f2 is for external and online research, not for exploring the code you are sitting in (#100); use a normal session ordispatchfor that. Esc cancels mid-run. - Keyboard shortcuts — press ctrl+h for a panel of the keys worth knowing;
/hotkeysis the full reference. ctrl+o expands tool output ("more"), ctrl+t toggles thinking blocks, ctrl+p cycles models. - Tool skill cards: little-coder injects a short usage card for the tools a turn is likely to need, chosen by error-recovery > recency > intent.
/skillslists what is loaded and what each costs,/skills <tool>pins one when the selector keeps picking a different card, and/skills offreturns to automatic selection (#118). - Prompt history — from an empty input, ↑ recalls your recent prompts (most-recent first), ↓ walks forward. History persists across sessions, so a fresh session can recall prompts from earlier runs.
- Sub-coders (
dispatch) — little-coder can spawn isolated child sessions to research a question (read the repo + browse online, read-only) and report back concisely, without cluttering the main conversation. A live panel above the input tracks them. Sub-coders run serially by default (two of them contend for the same local model server and finish slower than one); opt into parallelism withLITTLE_CODER_SUBCODER_CONCURRENCY=2or more. They are read-only by design, which is what makes fanning them out safe, since their answers come back as text and two of them cannot race on the same file, butLITTLE_CODER_SUBCODER_ACCESS=writegives themedit/writewhen you want the work done rather than reported (#93). Neither level ever grantsdispatch: a child that can spawn children is a fan-out bomb. - Background jobs (
ShellStart) — long commands (training, builds, servers, watchers) run in the background instead of blocking a turn, and little-coder wakes the model on events in the job rather than on a timer. A footer line shows what's running. See Background jobs below. - Per-phase models — plan on a big model, implement on a small one, with
/plan-modeland/action-model. See Per-phase model selection below. - Sessions — each session is auto-named from your first prompt (rename with
/name) and shown in the terminal tab title. Use/resumeto list and reopen past sessions for the current directory. - Read-before-edit — editing a file requires reading it first, so edits match the file's exact current text.
- Your own extensions — drop them in
~/.config/little-coder/extensions/and they load on the next launch. Run/extensionsto see what's loaded and where it came from. See Extending little-coder.
The status line
The footer at the bottom of the screen looks like this:
↑26k ↓5.4k R447k CH99.8% 9.3%/262k (auto) qwen3.6-35b-a3b • medium
| Field | Meaning |
|---|---|
↑26k | Cumulative input tokens billed as fresh across the whole session — not your current context size |
↓5.4k | Cumulative output tokens generated |
R447k | Cumulative tokens read from cache (the prefix your server didn't have to reprocess) |
W… | Cumulative cache-write tokens; only shown when non-zero |
CH99.8% | Cache-hit rate of the latest response alone — cacheRead / (input + cacheRead + cacheWrite). Not a session average, so it moves turn to turn |
9.3%/262k | Current context usage against the window size. Amber above 70%, red above 90% |
(auto) | Automatic compaction is enabled |
| right side | Active model, and its thinking level if it's a reasoning model |
A low CH on a long conversation means your server is reprocessing history it should have been able to reuse — worth investigating.
For local providers (llama.cpp, Ollama, LM Studio) pi expects some value in the API-key env even though local servers ignore it:
export LLAMACPP_API_KEY=noop
export OLLAMA_API_KEY=noop
export LMSTUDIO_API_KEY=noop
LLAMACPP_BASE_URL, OLLAMA_BASE_URL, and LMSTUDIO_BASE_URL override the defaults (http://127.0.0.1:8888/v1, http://127.0.0.1:11434/v1, http://127.0.0.1:1234/v1).
For cloud providers, set the standard env (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) and pi will discover it.
Local model setup (optional)
Skip this section if you're using a cloud model.
Option A — llama.cpp (fastest for local; supports Qwen3.6-35B-A3B MoE):
# One-time: build llama.cpp with CUDA (sm_XXX = your GPU arch; Blackwell = 120)
git clone https://github.com/ggml-org/llama.cpp && cd llama.cpp
cmake -B build -DGGML_CUDA=ON -DCMAKE_CUDA_ARCHITECTURES=120 -DLLAMA_CURL=ON
cmake --build build --config Release -j
# Fetch the model GGUF and the matching vision projector.
# The mmproj (~900 MB) is what lets the model see attached screenshots.
pip install -U "huggingface_hub[cli]"
hf download unsloth/Qwen3.6-35B-A3B-GGUF Qwen3.6-35B-A3B-UD-Q4_K_M.gguf --local-dir ~/models
hf download unsloth/Qwen3.6-35B-A3B-GGUF mmproj-F16.gguf --local-dir ~/models
# Serve it (MoE trick: experts in RAM, attention on GPU → 22 GB model on 8 GB VRAM)
build/bin/llama-server -m ~/models/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf \
--mmproj ~/models/mmproj-F16.gguf \
--host 127.0.0.1 --port 8888 --jinja \
-c 16384 -ngl 99 --n-cpu-moe 999 --flash-attn on
If you only need text and want to skip the projector download, drop the second hf download line and the --mmproj flag — little-coder still works text-only, but the TUI's image attachment will be rejected by the server with a 4xx.
Context window. -c sets the server's context (-c 16384 = 16K above — a conservative default for 8 GB VRAM). little-coder auto-detects the live n_ctx from llama.cpp's /props at startup and registers the model with it, so whatever you pass to -c is what the TUI shows and budgets against — no models.json edit needed. To run larger, relaunch the server with e.g. -c 131072 (128K) or -c 262144 (256K); the KV cache grows with it, so size it to your RAM/VRAM. (--list-models reflects the detected window.)
Option B — Ollama (simpler, but slower on MoE):
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen3.5 # 9.7B — the paper's model
# or: ollama pull qwen3.6:35b-a3b
Option C — LM Studio (GUI; OpenAI-compatible server on port 1234):
- Install LM Studio and download a model (e.g. Qwen3.6 35B A3B GGUF).
- Open the Developer / Local Server tab, load the model, and click Start Server (default
http://127.0.0.1:1234). - Run little-coder:
The shippedexport LMSTUDIO_API_KEY=noop little-coder --model lmstudio/local-modellmstudio/local-modelid routes to whatever model LM Studio currently has loaded — no extra config needed for the single-model case. If you serve on a non-default port, setLMSTUDIO_BASE_URL=http://127.0.0.1:<port>/v1. To target a specific model when you have several loaded, add an entry to~/.config/little-coder/models.json(see Configuring models below).
Serving from another machine on your LAN. Each provider's *_BASE_URL env var accepts any host, not just 127.0.0.1, so you can run inference on a beefier box and connect from a laptop or another device on the same WiFi.
On the server (the box with the GPU):
- llama.cpp: start
llama-serverwith--host 0.0.0.0(or your specific LAN interface) instead of127.0.0.1. Everything else from Option A unchanged. - LM Studio: in the Server tab, enable Serve on local network so it binds
0.0.0.0:1234instead of127.0.0.1:1234. - Ollama:
OLLAMA_HOST=0.0.0.0:11434 ollama serve(or setOLLAMA_HOST=0.0.0.0in the user systemd unit). - If
ufw/firewalldis active, allow your LAN subnet to the relevant port (e.g.sudo ufw allow from 192.168.0.0/16 to any port 8888 proto tcp). - Find the LAN IP with
hostname -I(Linux) oripconfig getifaddr en0(macOS).
On the client (the machine running little-coder):
# Pick the env vars matching whichever provider is running on the server
export LLAMACPP_API_KEY=noop
export LLAMACPP_BASE_URL=http://<server-lan-ip>:8888/v1
# Sanity check reachability before launching the agent
curl -s http://<server-lan-ip>:8888/v1/models | head
little-coder --model llamacpp/qwen3.6-35b-a3b
The streaming chat-completions adapter works over a local network the same way it does over loopback — no client code change, no proxy needed. The per-model profile in .pi/settings.json (context/thinking-budget/temperature) still applies because it's keyed by <provider>/<model-id>, which the client picks regardless of where the server lives.
All small-model-specific extensions auto-disable for large/cloud models so they don't interfere.
Configuring models
The shipped model list lives in models.json at the package root. The llama-cpp-provider extension reads it at startup and registers each provider via pi's registerProvider(). Editing this file in your global install does take effect — but it's overwritten on npm install -g little-coder@latest, so for anything you want to keep, use a user override file instead.
User override resolution (first match wins):
$LITTLE_CODER_MODELS_FILE— explicit path, useful for ad-hoc tests.$XDG_CONFIG_HOME/little-coder/models.json~/.config/little-coder/models.json
Merge semantics: each top-level provider key in your override file fully replaces the same key in the shipped models.json. Providers only in your file are added; providers only in the shipped file are kept. (We don't deep-merge per-model fields — you redeclare the whole provider entry, which avoids "your override silently inherited new fields from a future package release" surprises.)
Default model. A top-level "default": "provider/id" key names the model bare little-coder launches when you don't pass --model and pi has no saved selection yet (shipped default: llamacpp/qwen3.6-35b-a3b). Your override file's default wins over the shipped one, so {"default": "llamacpp/qwen3.6-27b"} in ~/.config/little-coder/models.json makes the dense 27B your first-run default. It's first-run-only: once you switch models in-session, pi remembers that and the default stops applying.
Per-phase models. /plan-model and /action-model let planning and implementation run on different models — see Per-phase model selection below.
Qwen3.8-27B (dense + MTP). Added to the shipped registry in v1.17.0. It's the quality option on a small card, not the fast one — measured on an RTX 5070 Laptop (8GB) with UD-Q4_K_XL:
| context | -ngl | tok/s | VRAM |
|---|---|---|---|
| 16k | 20 | 6.72 | 7200 MB |
| 32k | 18 | 6.42 | 7042 MB |
| 32k | 20 | — | loads, then generates nothing |
Compare ~44 tok/s for Qwen3.6-35B-A3B (MoE) on the same box: the MoE is roughly 7× faster because its experts live in RAM (--n-cpu-moe), a trick a dense model has no equivalent for. Its NextN head is in the GGUF (qwen35.nextn_predict_layers=1, blk.64), so MTP speculative decoding works — measured draft acceptance ~0.87. The one caution worth repeating: at 32k, -ngl 20 passes /health and then produces zero tokens. It fits in VRAM but has no room left to compute, so "the server started" is not evidence the config works. Raise the context and you must lower -ngl.
Community-recommended models. The shipped models.json stays intentionally small and stable — it doesn't track the fast-moving world of community fine-tunes (which get re-uploaded and disappear from Hugging Face constantly). If you want to try one that's doing well in the community — e.g. Qwen3.6-35B-A3B-REAM-192, which topped both a community tournament and a little-coder pilot (#63) — add it to your own override file rather than waiting for it to ship. Load the GGUF on your llama.cpp server, then drop an entry in ~/.config/little-coder/models.json:
{
"providers": {
"llamacpp": {
"api": "openai-completions",
"baseUrl": "http://127.0.0.1:8888/v1",
"apiKey": "LLAMACPP_API_KEY",
"models": [
{ "id": "ream-192", "name": "Qwen3.6-35B-A3B REAM-192 (community)", "reasoning": true, "input": ["text"] }
]
}
}
}
Then pick it with little-coder --model llamacpp/ream-192. (llama.cpp serves whichever GGUF you loaded regardless of the id, so the id is just your handle for it.)
Example — switch the llama.cpp port and bump qwen3.6-35b-a3b to a 150K context, leave ollama untouched:
{
"providers": {
"llamacpp": {
"api": "openai-completions",
"baseUrl": "http://127.0.0.1:1234/v1",
"apiKey": "LLAMACPP_API_KEY",
"models": [
{
"id": "qwen3.6-35b-a3b",
"name": "Qwen3.6-35B-A3B (local llama.cpp, 150K)",
"reasoning": true,
"input": ["text"],
"contextWindow": 150000,
"maxTokens": 4096,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
}
]
}
}
}
Then verify with little-coder --list-models — you should see your overridden entry.
LLAMACPP_BASE_URL, OLLAMA_BASE_URL, and LMSTUDIO_BASE_URL env vars still beat both files for those three providers.
Any OpenAI-compatible server (e.g. MLX / omlx)
little-coder registers providers from models.json — it doesn't pick up pi's standalone "picker" extensions. So a server isn't added by installing its pi picker; you add it by declaring a provider. Any OpenAI-compatible endpoint works this way, including Apple's MLX server (mlx_lm.server, often surfaced as omlx). Drop this into ~/.config/little-coder/models.json and pick it with little-coder --model omlx/<id>:
{
"providers": {
"omlx": {
"api": "openai-completions",
"baseUrl": "http://127.0.0.1:8000/v1",
"apiKey": "IGNORED",
"models": [
{
"id": "Qwen3-32B-4bit",
"name": "Qwen3.6-35B-A3B (local omlx, 150K)",
"reasoning": true,
"input": ["text"],
"contextWindow": 150000,
"maxTokens": 4096,
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 }
}
]
}
}
}
Set id to whatever model your server reports, and baseUrl to its /v1 endpoint. Verify with little-coder --list-models.
.pi/settings.json is a separate concern: it controls per-model profiles (context_limit, thinking_budget, temperature, benchmark_overrides) referenced by the <provider>/<id> key. Profiles don't register or describe models — they only tune how little-coder runs against models that are already registered.
Background jobs
bash blocks the turn until the command exits, which makes it the wrong tool for anything long. The usual workaround is worse: background the job and then poll it, spending a turn, a slice of a small context window, and seconds of local inference to learn that training is still on epoch 3. A six-hour job checked every five minutes is 71 wasted turns.
So ShellStart inverts it. The model says up front what is worth being interrupted for, and the harness stays quiet until one of those things happens:
{"name": "ShellStart", "input": {
"command": "python train.py --epochs 50", "label": "finetune",
"wake_on": {"match": ["Traceback", "CUDA out of memory", "val_loss="],
"every_n_matches": 10, "silence": "15m"}}}
wake_on | fires when |
|---|---|
exit | the job exits (default on) |
match | a line matches — regex, falling back to literal text |
silence | it produced output, then went quiet this long (e.g. "10m") |
every_n_matches | only every Nth match, to throttle a chatty pattern |
Urgency decides how the news arrives: a crash or an error-ish match interrupts the current turn; a clean exit or a milestone waits for the tool calls already in flight; routine output rides along with the next turn. Six hours of progress bars cost nothing; a traceback at minute 40 costs one turn, immediately. What the model receives is a bounded excerpt plus the exit code — never the whole log — with ShellLog to page deeper on demand. ShellList, ShellSend (stdin, for a REPL or a prompting installer) and ShellStop round it out.
Lifetime. A job outlives a turn but never the session. Jobs run in their own process group and are signalled as a group, so python train.py under a shell dies with the shell rather than being orphaned holding your VRAM. Session shutdown and every catchable signal reap them — and because SIGKILL is catchable by nobody, each job also carries a watchdog that kills its own group the moment little-coder's pid disappears.
Permissions. ShellStart goes through the same whitelist as bash, so build and test commands usually need LITTLE_CODER_BASH_ALLOW (see Permissions).
Your project's AGENTS.md
little-coder launches pi with --no-context-files, so its AGENTS.md is the system prompt rather than whatever happens to sit in the current directory. That is why the small-model adaptations hold in any repo, and until v1.20.0 it also meant your project's own AGENTS.md was ignored outright, so the model globbed the whole tree at the start of every run to work out where it was (#104).
Now the nearest AGENTS.md (or CLAUDE.md, if there is no AGENTS.md) is found by walking up from the launch directory, and injected as project instructions. Three things keep it from undoing the reason --no-context-files is there:
- It adds. little-coder's own prompt still governs behaviour, and the injected block says so, so a project file cannot quietly re-specify the harness.
- It is capped at 4000 characters (~1k tokens) and truncation is reported, to you and to the model, rather than being silent. The lean cold start is the product.
- It is injected once, as a hidden message at the conversation tail rather than a system-prompt append, so the cached prefix survives (#73).
/project-context shows which file loaded, its size, and its hash.
| Env var | Values | Effect |
|---|---|---|
LITTLE_CODER_PROJECT_CONTEXT | 0 / off / false | Don't load a project file at all, i.e. the pre-v1.20.0 behaviour. |
LITTLE_CODER_PROJECT_CONTEXT_MAX_CHARS | integer | Raise or lower the cap. 0 means no cap. |
Per-phase model selection
Plan on a big model, implement on a small one, without retyping ctrl+P at every transition (#61).
| command | does |
|---|---|
/plan-model <name> | model for Plan Mode (autocompletes; /plan-model 35b resolves) |
/action-model <name> | model for implementation |
/phase-models | show both, plus the active model and handover mode |
/model-handover auto|manual | whether little-coder switches models for you |
/implement | run the last approved plan in a fresh session on the action model |
Defaults come from models.json ("planModel" / "actionModel" / "handover") or the environment (LITTLE_CODER_PLAN_MODEL, LITTLE_CODER_ACTION_MODEL, LITTLE_CODER_MODEL_HANDOVER), but the tags are session state — settable mid-session, because the case that motivated this is A/B-ing two planners against the same brief.
With auto (default), entering Plan Mode switches to the plan model and /implement hands over to the action model. The handover happens when implementation actually begins rather than at approval, so approving a plan you then decide to rewrite costs you nothing. With manual, the tags are shown but nothing switches on your behalf. That toggle matters more locally than it would on a hosted provider: on a single llama.cpp backend a handover evicts and reloads weights, so an automatic switch can mean a 15-second stall mid-thought. A handover where both phases resolve to the same model is a no-op rather than a reload, and a switch that fails (model unavailable, no key) degrades to staying put and says so. Leave both unset and nothing changes — each phase uses the active model.
Planning in a batch job
Plan Mode's middle is interactive: research, then one to three clarifying questions, then the plan. Under -p there is nobody to ask, so headless runs used to refuse to plan at all, which is awkward for the workflow it matters most in, where a local model is a slow shared resource and jobs are queued and checked later (#95).
little-coder -p --plan-mode "add rate limiting to the ingest endpoint"
The questions are still generated; they are answered from the research instead of from you, and the plan opens with an Assumptions section saying what was decided for each and why, which is the part a batch user actually needs to review. The plan is printed and written to .pi/approved-plan.md, so the next job in the queue can run /implement against it.
Of the three policies that make sense here (skip the questions, pre-supply the answers from a file, or stop after generating them) this is the first. Pre-supplied answers need the question set to be stable across runs, and it is not; a two-phase run is by definition not unattended.
Permissions
little-coder gates shell tool calls — Bash and ShellSession alike — against a built-in safe-prefix whitelist (ls, cat, head, tail, git log/status/diff, find, grep, cp, mv, mkdir, touch, etc.) before pi's own confirmation flow ever sees them. rm and sudo are intentionally not on the list — add them via LITTLE_CODER_BASH_ALLOW per deployment if you really need them.
Three rules beyond the prefix match:
- Every command in a chain is judged, not just the first (#70).
ls && rm -rf /is refused on therm, not allowed on thels. - A command that writes to a file through the shell is refused (#70), whatever it starts with.
cat > main.py << 'EOF'is the same write as theWritetool and gets the same answer — useWritefor a new file,Editfor an existing one. Redirects (>,>>),tee, anddd of=all count;2>&1and a>inside quotes don't. - Matching a prefix is not enough (#94). The interpreters are on the list so that scripts can be run, so
python3 solution.pypasses andpython3 -c "..."does not. The same goes fornode -e/-p,perl -e/-E,ruby -e,find -exec/-delete,sed -i, andenv <command>: each is a way to reach an effect the whitelist just refused.
What the whitelist is and isn't. It is a speed bump against a model that takes a wrong turn, not a sandbox. python3 script.py is one Write away from python3 -c, and no prefix list can close that. If you need a real boundary, use a container or a VM, or put a human in the loop with LITTLE_CODER_PERMISSION_MODE=manual.
In accept-all mode the whitelist is skipped, but the write guard still refuses a shell redirect that would clobber an existing file or a reserved device name — so the "small models don't rewrite whole files" guarantee holds in benchmark runs too.
Two env vars control the gate:
| Env var | Values | Effect |
|---|---|---|
LITTLE_CODER_PERMISSION_MODE | auto (default) / accept-all / manual | auto: block any shell command not on the whitelist. accept-all: skip the gate entirely, every shell call passes (the benchmark runner sets this). manual: prompt before every shell command (execute y / cancel n) and before every write/edit — you choose Apply, Deny, or Apply all (this session) to stop re-prompting for the rest of the session. write-guard's safety checks (reserved names, whole-file rewrites of existing files) still run even when you Apply. |
LITTLE_CODER_BASH_ALLOW | comma-separated prefixes | Extra allow-prefixes merged with the built-in list. Trailing whitespace is meaningful: "make " allows make test but not makefoo; "make" allows both. |
LITTLE_CODER_BASH_ALLOW only adds prefixes; it does not lift the inline-code rule. To run python3 -c you want accept-all (or manual, if you would rather see each one).
Examples:
# Add 'make' (with word-boundary) and 'docker compose ps' on top of the defaults
export LITTLE_CODER_BASH_ALLOW="make ,docker compose ps"
# Skip the gate entirely (use this only inside controlled environments)
export LITTLE_CODER_PERMISSION_MODE=accept-all
In auto and accept-all, write/edit confirmations are pi's own; manual adds little-coder's prompt in front of them.
Paper / benchmark results
| Release | Model | Benchmark | Result |
|---|---|---|---|
v0.0.2 (commit 1d62bde) — the paper | Qwen3.5-9B via Ollama | Aider Polyglot (225 exercises) | 45.56 % mean of two runs; matched-model vanilla Aider baseline 19.11 %. Paper: Honey, I Shrunk the Coding Agent on Substack. |
| v0.0.5 — pre-pi Python | Qwen3.6-35B-A3B via llama.cpp | Aider Polyglot | 78.67 %. Full narrative. |
| v0.1.4 — on pi | Qwen3.6-35B-A3B via llama.cpp | Terminal-Bench-Core v0.1.1 (80 tasks) | 40.0 % in 6 h 50 min. Write-up. |
| v0.1.13 — on pi, TB 2.0 leaderboard | Qwen3.6-35B-A3B via llama.cpp | Terminal-Bench 2.0 (89 tasks × 5 trials = 445) | 24.6 % ± 3.2 — accepted to the Terminal-Bench 2.0 leaderboard (rank 120). |
| v0.1.24 — on pi, TB 2.0 leaderboard, smaller model | Qwen3.5-9B (Q4_K_M) via llama.cpp (5.3 GB on GPU, 2× faster per-token than the 35B-A3B) | Terminal-Bench 2.0 (89 tasks × 5 trials = 445) | 9.2 % ± 2.4 — accepted to the Terminal-Bench 2.0 leaderboard (rank 142). |
| v0.1.27 — on pi, GAIA validation | Qwen3.6-35B-A3B via llama.cpp | GAIA validation set (165 tasks) | 40.00 % (66 / 165). L1 60.4 % / L2 37.2 % / L3 7.7 %. Test-split run pending. |
All runs used a consumer laptop: i9-14900HX, 32 GB RAM, 8 GB VRAM on RTX 5070 Laptop (Blackwell). No cloud inference at any point.
Roadmap
Phase 1 — wide benchmark baseline: complete. The paper established that scaffold–model fit moves a 9.7 B model from 19 % to 45 % on Aider Polyglot, and the goal of Phase 1 was to find out how wide that impact radius is. We now have a four-benchmark baseline on a single laptop-class GPU:
- Aider Polyglot — 45.56 % (paper, Qwen3.5-9B) and 78.67 % (v0.0.5, Qwen3.6-35B-A3B).
- Terminal-Bench-Core v0.1.1 — 40.0 % (v0.1.4).
- Terminal-Bench 2.0 — accepted to the official leaderboard: Qwen3.6-35B-A3B at 24.6 % ± 3.2 (rank 120) and Qwen3.5-9B at 9.2 % ± 2.4 (rank 142). The v0.1.24 prompt-repetition fix (re-add tool descriptions + concision guideline, validated by a 4 / 4 pilot on the previously-regressing
prove-plus-commtask) was the prompt for both submissions. - GAIA — validation set at v0.1.27: 40.00 % (66 / 165) on Qwen3.6-35B-A3B. Per-level L1 60.4 % / L2 37.2 % / L3 7.7 %.
That spans short coding exercises (Polyglot), interactive shell-bound tasks (Terminal-Bench), and tool-using research (GAIA), all on the same scaffold. The data needed to choose what to fix next is now in hand.
Phase 2 — operating real knowledge bases as day-to-day work: the current focus. The motivating question shifts from how wide is the impact radius? to can a small local model reliably operate and traverse a large, messy knowledge base? little-coder's day-to-day target is now real work over many markdown files at once — reading, cross-referencing, and updating sprawling note/log collections in the most token-efficient way a small local model can manage. Features are being implemented and tested across several real pipelines in parallel:
- Domains — medical, athletic, and educational knowledge bases, each with its own structure, vocabulary, and citation needs.
- Scale — 10+ years of logs, tens of thousands of entries of varied kinds, stressing retrieval, compaction, and the context-budgeting extensions on histories far longer than any single benchmark task.
- Messy real-world inputs — validation against conflicting OCR extractions of the same source, and multilingual content where the same fact recurs across languages.
This is where the scaffolding work now compounds: knowledge injection/selection, evidence handling, compaction fidelity, and the harness-intervention behaviors. Expect smaller, more frequent releases driven by what little-coder actually struggles with on this work rather than by a benchmark suite.
Benchmarks (deferred). The four-benchmark baseline above stands as the scaffold-fit reference point. Fresh runs — ProgramBench, SWE-bench Verified (multi-file real-world patches), a GAIA test split — come back into scope once the knowledge-base work has produced enough scaffolding signal to make a new measurement worth running.
Troubleshooting
--update flag — pass little-coder --update to force an immediate version check, bypassing the 12-hour cache. Useful right after a release. The flag is stripped before pi sees argv so it won't produce an "Unknown option" error.
Auto-update fails on Windows (≤ v1.9.5): npm exit null — the updater in those versions can't locate npm.cmd. Fixed in v1.9.6, but the broken updater can't deliver its own fix — run npm install -g little-coder@latest once to get there, then auto-update works normally.
little-coder: command not found — npm's global bin directory isn't on your PATH. Run npm config get prefix to see where it installed; add <prefix>/bin to your PATH. Or reinstall with sudo if your prefix needs root.
ECONNREFUSED 127.0.0.1:8888 — llama.cpp isn't running. Start llama-server first, or switch --model to an Ollama/cloud ID.
LAN client times out (no RST, just hangs) — the inference box's firewall is dropping the SYN. The usual cause is ufw with a default-deny policy that allow-lists only SSH / a few dev ports. From the server: sudo ufw status verbose to confirm; sudo ufw allow from <your-lan-subnet>/24 to any port 8888 proto tcp to fix (scoped to the LAN so you're not exposing the box). Docker-published ports bypass ufw via PREROUTING NAT, which is why a Docker container can be reachable while a plain llama-server on the same host isn't.
Image attachment is accepted but the request returns 4xx — your llama-server is running without a vision projector. Re-launch it with --mmproj ~/models/mmproj-F16.gguf (or another mmproj variant from the same GGUF repo). The --list-models images column reflects what the client will attempt to send, not what the server can answer; the projector is what gives the model eyes.
Failed to parse input at pos N: SomeTool(arg='…')]<|tool_call_end|> (LFM2 / Liquid models) — the model is emitting its native Pythonic tool calls (<|tool_call_start|>[Read(path='…')]<|tool_call_end|>), but llama.cpp's tool-call parser is choking on them — usually because the chat template doesn't match the parser. The GGUF's embedded template often renders tools as a plain List of tools: […] blob without the <|tool_list_start|> / <|tool_call_start|> special tokens the parser expects. Fix: serve with --jinja and the model's proper chat template, e.g. llama-server -m LFM2.5-8B-A1B-Q4_K_M.gguf --jinja --chat-template-file LFM2-8B-A1B.jinja (templates ship under llama.cpp/models/templates/). With the matching template, llama.cpp parses the calls into native tool_calls and tools execute normally — verified end-to-end with LFM2.5-8B-A1B. If your build still leaks the calls as plain text, little-coder's output-parser recognizes the format and surfaces this same diagnostic instead of a cryptic error (issue #42).
Context overflows on a long task before compaction kicks in — fixed in v1.9.12. pi only re-checks compaction when the model goes idle at the end of a turn sequence, so a single long autonomous run (dozens of tool calls) could grow context all the way to an overflow error before that check ever ran (issue #59). little-coder now watches context usage at every turn boundary and triggers pi's compaction mid-run once usage crosses 80 % of the window. Tune the trigger with LITTLE_CODER_COMPACT_AT_PERCENT=<n> (e.g. 70 to compact earlier; values ≤0 or ≥100, or LITTLE_CODER_NO_COMPACT_WATCHDOG=1, disable it and fall back to pi's end-of-run behavior). This is independent of pi's own reserveTokens / keepRecentTokens, which still govern how much is summarized vs. kept. As of v1.11.0 the watchdog also guards against a compaction loop (issue #68): if a mid-run compaction frees too little (usage stays near the threshold, e.g. a context window too small for the task), it pauses automatic compaction with a notice instead of firing a doomed second compaction that pi would reject with Nothing to compact — which previously left the session unrecoverable. It re-arms once usage drops back below the threshold (a /clear, /compact, or a smaller turn). If you hit the pause a lot, the real fix is a larger-context model or -c window.
No API key env var warning — pi expects some key even for local providers. Export LLAMACPP_API_KEY=noop (or OLLAMA_API_KEY=noop) before launching.
Update prompt hangs on launch / want it in the UI instead — when a new version is published the launcher asks Update now? [Y/n] before starting. As of v1.9.12 it auto-continues without updating after 10 s so an unattended terminal is never blocked; tune with LITTLE_CODER_UPDATE_PROMPT_TIMEOUT=<seconds> (0/off waits forever). If you dismiss or time out of the prompt, little-coder still shows a one-line "update available" notice inside the TUI, and you can run /update any time to install the latest and end the session for a clean restart (issue #64). As of v1.11.0, answering Y at the launcher prompt auto-relaunches into the new version with your original arguments (issue #66) — no manual re-run — printing Relaunching little-coder… (with a manual-relaunch fallback if the re-exec can't start). The in-app /update still ends the session for a manual restart, since it runs inside the pi child process where an in-place re-exec isn't safe.
No pi "Update Available" banner — that's intentional. little-coder defaults PI_SKIP_VERSION_CHECK=1 so the bundled pi runtime doesn't nag about updating itself; little-coder pins pi to a known-good version per release. If you actually want the banner back, export PI_SKIP_VERSION_CHECK=0 before launching.
Running little-coder from Zed's agent panel — there's no built-in ACP server, but a community pi-acp bridge works well; see docs/zed-acp.md for the full setup (issue #58).
Extension load failures on startup — run /extensions inside the TUI: it lists what loaded, where each one came from, and anything that failed. A user extension that can't be resolved also raises a notification at session start. little-coder --list-models --verbose surfaces pi's own load errors. If the install looks corrupt: npm uninstall -g little-coder && npm install -g little-coder.
My pi extensions / themes don't load — themes do load; extensions don't, by default. --no-extensions gates extensions only, so pi themes in ~/.pi/agent/themes work as normal. For extensions, relaunch with --with-pi-extensions, or put your own in ~/.config/little-coder/extensions/. See docs/extensions.md (issue #67).
A malware alert on npm install -g little-coder — Socket's AI scanner flagged the postinstall script in v1.10.0/v1.11.0 as suspicious. It was a false positive on a visible, dependency-free patcher, but as of v1.12.0 little-coder ships no install scripts at all — the launcher does that work at launch time instead, which is also the only path that ever ran for upgrading users, since /update installs with --ignore-scripts (issues #75, #50).
ctrl+r does nothing — it isn't bound at the prompt; pi binds "expand / more" to ctrl+o. Versions up to v1.11.0 advertised ctrl-r in the startup header, which was simply wrong (issue #74). Press ctrl+h for the current key list, or /hotkeys for the full reference. Note that during a Deep Research run there is nothing for ctrl+o to expand: research sub-coders are separate child processes, so their tool output never enters this session's transcript — the progress bar is the view of that work. While a dialog is open (the max-agents or clarifying-question prompts), keys belong to the dialog.
llama.cpp reprocesses the whole conversation every turn — fixed in v1.12.0. little-coder's per-turn skill and knowledge blocks used to be appended to the system prompt, which sits at the front of every request, so changing them invalidated the entire cached prefix and your server re-read the full history (issue #73). Those blocks now arrive as a message at the end of the conversation instead, leaving the prefix byte-identical. Watch the CH field in the status line to confirm cache reuse. LITTLE_CODER_INJECT_MODE=system restores the old placement, which is what the whitepaper scaffold numbers were measured against.
Node version too old — little-coder needs Node ≥ 22.19.0 (matching the minimum of the bundled @earendil-works/pi-coding-agent v0.75+). Check with node --version. Easiest fix: nvm install 22 && nvm use 22.
Developing little-coder locally
If you want to hack on the extensions or skills:
git clone https://github.com/itayinbarr/little-coder.git
cd little-coder
npm install
npm link # makes the local checkout available as `little-coder`
little-coder --model llamacpp/qwen3.6-35b-a3b
To unlink: npm unlink -g little-coder.
The benchmarks harness (benchmarks/) is dev-only and not shipped with the npm package. Run it from a clone with python3 benchmarks/aider_polyglot.py … etc.
Architecture
little-coder/
├── .pi/
│ ├── settings.json # per-model profiles + benchmark_overrides (terminal_bench, gaia)
│ └── extensions/ # 27 TypeScript extensions, auto-discovered by pi
│ ├── branding/ # little-coder startup header + terminal title + session auto-naming
│ ├── plan-mode/ # alt+p "research → ask → plan" flow (sub-coders + clarifying questions → written plan)
│ ├── subagent/ # `dispatch` tool: isolated read/browse-only sub-coders + live tracker (spawn.ts engine)
│ ├── prompt-history/ # up-arrow recall of recent prompts (from an empty input)
│ ├── llama-cpp-provider/ # data-driven provider registration from models.json — ships llamacpp, ollama, lmstudio (+ user override file)
│ ├── write-guard/ # Write refuses on existing files; rewrites root-bare /foo.md paths to cwd
│ ├── read-guard/ # trims a Read that would overflow the context window to its first 30 lines + a search-instead directive
│ ├── read-guard-edit/ # Edit refuses until the file has been Read this session
│ ├── extra-tools/ # glob, webfetch, websearch (pi ships grep/find)
│ ├── skill-inject/ # per-turn tool-skill selection (error > recency > intent)
│ ├── knowledge-inject/ # algorithm cheat-sheet scoring (word=1.0, bigram=2.0, threshold=2.0)
│ ├── output-parser/ # repair malformed ```tool, <tool_call>, bare JSON
│ ├── quality-monitor/ # empty / hallucinated / loop detection + correction follow-up
│ ├── thinking-budget/ # cap thinking tokens per turn, retry with thinking off
│ ├── permission-gate/ # bash whitelist (ls, cat, git log/status/diff, etc.)
│ ├── checkpoint/ # snapshot files before Write/Edit
│ ├── tool-gating/ # enforces _allowed_tools at exec + schema levels
│ ├── turn-cap/ # max_turns abort (Polyglot unbounded, TB 40, GAIA 30)
│ ├── benchmark-profiles/ # reads settings.json → systemPromptOptions + sets temperature
│ ├── shell-session/ # ShellSession[Cwd|Reset] — tmux-proxy + subprocess backends
│ ├── browser/ # Playwright BrowserNavigate/Click/Type/Scroll/Extract/Back/History
│ ├── evidence/ # EvidenceAdd/Get/List — per-session store, 1 KB snippet cap
│ └── evidence-compact/ # preserves evidence across pi's auto-compaction
├── skills/ # 30 markdown files the extensions inject on demand
│ ├── tools/*.md # 14 tool-usage cards
│ ├── knowledge/*.md # 13 algorithm cheat sheets
│ └── protocols/*.md # 3 research/cite/decomposition workflows
├── benchmarks/
│ ├── rpc_client.py # PiRpc — spawns `pi --mode rpc`, demuxes events + UI requests
│ ├── aider_polyglot.py # Polyglot driver with per-language transforms
│ ├── tb_adapter/ # Terminal-Bench 1.0 BaseAgent (tmux-proxy)
│ ├── harbor_adapter/ # Terminal-Bench 2.0 BaseAgent (async env.exec proxy)
│ ├── tb_pilot.sh / harbor_pilot.sh
│ ├── tb_status.sh / harbor_status.sh
│ └── test_rpc_client.py
├── AGENTS.md # project system prompt (pi discovers it automatically)
├── models.json # canonical provider registration (loaded by llama-cpp-provider; user override at $XDG_CONFIG_HOME/little-coder/models.json)
└── docs/
├── benchmark-*.md # per-benchmark narratives
└── architecture.md # v0.0.5-era Python architecture (historical)
Key invariant. pi is a minimal base by design. Every little-coder mechanism ships as a pi extension that hooks pi's lifecycle events (before_agent_start, context, before_provider_request, tool_call, tool_result, turn_end, session_compact). Extensions are independent: the launcher discovers every .pi/extensions/*/index.ts and loads it explicitly with --extension, and pi runs with --no-extensions, so the bundled set is exactly what loads — no more, no less. If you don't want one, delete its directory; if you want to add another, drop it next to the existing ones (or pass -e <path> at launch).
Reproducing the paper (v0.0.2)
git clone https://github.com/itayinbarr/little-coder.git
cd little-coder
git checkout v0.0.2
# Follow that version's README for its Python setup (pip install -e .)
The paper ran ollama/qwen3.5 through the Python little-coder at commit 1d62bde (tag v0.0.2). The 45.56 % mean figure is the average of two full 225-exercise runs on that exact codebase. For the 78.67 % headline, check out tag v0.0.5 — both are pre-pi Python and follow the pre-pi setup.
Citation
@misc{inbar2026littlecoder,
title = {little-coder: A Coding Agent Optimized for Small Local Language Models},
subtitle = {Architectural Adaptation Lets a 9.7B Model Outperform Frontier Models on Aider Polyglot},
author = {Inbar, Itay},
year = {2026},
month = apr,
howpublished = {\url{https://open.substack.com/pub/itayinbarr/p/honey-i-shrunk-the-coding-agent}},
note = {White paper}
}
Attribution
little-coder v0.0.x was a derivative work of CheetahClaws / ClawSpring by SafeRL-Lab, Apache 2.0. That upstream provided the Python agent substrate, tool system, multi-provider support, and REPL.
little-coder v0.1.0+ replaces that substrate with pi by Mario Zechner — Apache 2.0 / MIT. The npm package was renamed from @mariozechner/pi-coding-agent to @earendil-works/pi-coding-agent in upstream's 0.74 release; little-coder v1.4.2+ ships with the new package. pi provides the agent loop, provider abstraction, TUI, and extension model. little-coder rebuilds its small-model adaptations on top of pi as extensions.
All little-coder-specific mechanisms — Write-vs-Edit invariant, skill / knowledge injection, thinking-budget cap, output-parser, quality-monitor, per-model profiles, per-benchmark overrides, ShellSession / Browser / Evidence tool families, evidence-aware compaction — are preserved across versions.
License
Apache 2.0 — see LICENSE for details. NOTICE tracks upstream attribution.