Skip to content

About

A coding agent optimized to smaller LLMs

Resources

Stars

49 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

little-coder — a coding agent for the laptop in front of you

little-coder

This is a fork of the original little-coder by itayinbarr introduced in Honey, I Shrunk the Coding Agent. If you want to read some background, start there.

Highlights

  • Built on pi as the minimal core -- supports any pi extensions for customization
  • Efficient System Prompt -- standard system prompt AGENTS.md clocks in at only ~3000 characters, including dynamic steering. At ~4 characters per token, that's only ~750 tokens!
  • Subagent support -- Select the model (/subagent-model [subagent] [model] or for all subagents /subagent-model-all [model]) and improve your context efficiency
  • Dynamic Subagent Steering -- automatically injects a notice for subagent use depending on the selected level /subagent-level (off|low|medium|high|xhigh)
  • Dynamic Skill Steering -- reads frontmatter entries from skill files, including tools like bash, to remind the agent how to do things
  • Extra-tooling support -- Lots of agents will try to cd [cwd] at the start of bash commands. The bash tool supports an optional cwd parameter that some agents may use for a more structured approach
  • Dynamic Tool Loading -- Dynamic Skill Steering automatically loads the skill for a tool, but if the agent wants to know what's available, it can execute tools or skills to get a list and short descriptions for each loaded tool. No context bloat at start loading in 500 skills and tools that you installed "just in case"
  • LSP support -- building on top of pi-hooks/lsp with some minor updates like C#/.NET support
  • Breadcrumbs -- Search through old sessions
  • Efficient Browsing Tools -- Browsing Tools are disabled by default (except for webfetch/websearch) to not bloat context and confuse the agent. Can just be enabled by the agent if needed
  • Tuned for small models -- Failed tool call steering, Write Guards, CWD Guards, Looser Parameter Enforcement, Heuristical Improvements, etc.
  • Efficient Planning & Reviewing -- Two commands, /deep-plan and /review start subagent-focused pipelines that avoid many pitfalls
  • Efficient Context Compression (Experimental) -- Integration of pi-vcc for more efficient context compressions. Needs some tuning I think.
  • Fleet Web UI -- a loopback-only dashboard (/web-ui on) to watch and drive every little-coder session on the machine from your browser: live transcripts, owner controls (model/thinking/tools/compact/stop), message + stop + compact attached TUIs, and spawn/resume headless web-UI children with full control.
  • Bundled extensions -- Ponytail, pi-powerline-footer, pi-insights, pi-inspect, plannotator, pi-better-openai, pi-ask-user, pi-vcc
  • Audio clues -- two short tones that let you tell from across the room whether the agent needs you: an "attention" tone the moment a user-wait tool call starts (by default ask_user and plannotator_submit_plan — the tool list is the global little_coder.concurrency_user_wait_tools setting, the same single source of truth the concurrency extension uses; setting it to [] silences this cue by design) and a softer "done" tone when a run fully settles (exactly once per request, on every exit path). TUI only — headless web-UI children, --print/JSON runs, and subagents play nothing. On by default; manage it live with /clue (status), /clue on|off (writes the global settings — a trusted project-scope little_coder.audio_clues still wins), or /clue test (plays the attention, then the done cue — it ignores enabled, so it doubles as a player check); or configure it in settings with little_coder.audio_clues: { "enabled": false } or attention_command / done_command pointing at any player of your choice (whitespace-tokenized command strings, no shell — quoted arguments with spaces are not representable). Per-OS defaults: macOS afplay (Hero / Glass system sounds), Linux paplay (freedesktop alarm / ding — and, on minimal boxes without those files, e.g. WSL2 / Debian slim, a built-in tone synthesized on the fly), Windows PowerShell console beep — a missing player or sound file fails silently (audio never blocks or delays a run). The audio and the terminal bell (BEL) are coordinated by the bell setting (default "only" — the cue plays as a terminal bell with no tone of our own, which is what a host terminal's visual indicator reacts to, e.g. Windows Terminal's tab bell icon, driven by BEL, not by audio): "both" plays the tone plus a BEL, and "off" plays the tone without a BEL. pi-notify (ferologics) remains an optional complement for terminal notification banners.

Want to read more? The easiest way is to read the source code, but just open an issue with any questions you have. There's also still plenty of experimental features in this repo that aren't mentioned. Any suggestions are welcome :)

Install

If you've never used pi, it's useful to skim pi.dev first.

One-line install (Node.js 22.19+ required):

curl -fsSL https://raw.githubusercontent.com/L3tum/little-coder/v1.8.1/install.sh | bash

Or with npm directly from this fork:

npm install -g "github:L3tum/little-coder#v1.8.1"

Or with bun:

bun add -g "github:L3tum/little-coder#v1.8.1"

The commands above are pinned to release tag v1.8.1 so a one-liner never pulls an unreviewed main-branch merge; to upgrade, bump the tag in the command (see the repo's Releases page).

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 -g users. The launcher (bin/little-coder.mjs) is a Node.js script with #!/usr/bin/env node at the top, so Node ≥ 22.19 still has to be on your PATH for the binary to start — bun is fine for installing/updating this fork, but the runtime is Node. If you want a fully node-less setup, replace the shebang in $(bun pm bin -g)/little-coder with #!/usr/bin/env bun.

Run

cd ~/your-project
little-coder --model llamacpp/qwen3.6-35b-a3b

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.

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.

In the TUI you can use /tools to list loaded tools and /skills to list available skills. The agent can also call tools, skills, and enableBrowserTools directly.

Use /plan to enter browser-reviewed planning mode before implementation. Use /deep-plan for a subagent-focused planning pipeline: RESEARCH, COMPOSE (draft), dual parallel review (REVIEW-PLAN fact-check + REVIEW-PLAN-PONYTAIL lazy-engineering), and COMPOSE (final). After approval a fresh EXECUTION subagent implements the plan instead of the main agent. Use /review (changes only), /review-project or /review-focused for a subagent focused reviewing pipeline including 7 different subagents. Use /deslop (changes only) or /deslop-project (entire codebase) to scan for AI-generated “slop” (the tells a human coder would never write), plan a behavior-preserving cleanup, and apply it only after you approve the plan in the browser. Like the other pipelines, deslop phases run with NO time watchdog by default (a wedged phase is stopped only by cancelling the run); set LITTLE_CODER_PIPELINE_PHASE_TIMEOUT_MS to a positive value in milliseconds to enable one — /deslop-project (the whole-codebase scan) is the main beneficiary.

Ralph loop — --fresh multi-agent research & implementation runs

Use /ralph-research <question> for a long research question or /ralph-implement <task> for an implementation task that should run as a HARNESSED LOOP: the model works round by round, and after each round an INDEPENDENT read-only RALPH-EVALUATOR subagent judges the work against the target — the model never judges itself. A fail restarts the loop with the verdict (reason + evidence) and the progress-file tail; a pass stops it and a RALPH-SUMMARIZER subagent writes the final report (delivered in a fenced, untrusted block — the model relays it, never executes it).

/ralph-research "Which ORM should this codebase migrate to?" --target "a decision with 3+ named candidates, trade-offs per candidate, and a concrete recommendation with rationale"
/ralph-implement "Add pagination to the /issues endpoint" --verify "npm test" --target "endpoint returns page/total, tests pass" --max-rounds 15
/ralph-implement "big task" --fresh   # main agent stays idle; RESEARCH/EXECUTION child runs per round
/ralph-stop
  • --target <text> — what the evaluator judges against (default: "the question is answered with evidence" / "the task is complete"). --verify <cmd> — a shell gate the harness runs after each round via pi.exec (a non-zero exit fails the round WITHOUT the evaluator; with no --target an exit 0 passes it directly). --max-rounds N — the loop cap (default 10, hard cap 50).
  • In-session mode (default): the loop runs in the main session, one turn per round. The model signals round end with the ralphRoundDone tool (it must be the ONLY tool call in the message — mixed calls don't end the round; the tool refuses any out-of-run call via a tool_call guard). Consecutive LLM error turns get a steer nudge (budget 5, reset on a clean turn or compaction) before the harness stops the run; token-limit error turns never consume that budget (token-limit-guard owns them).
  • --fresh: the main agent stays idle and a fresh RESEARCH/EXECUTION child does each round (the round report comes from a RALPH_ROUND_REPORT: trailer in the child's final message). A failed worker spawn re-spawns the SAME round (consecutive-failure budget 5, reset by a clean spawn); the run survives crashes — the run ledger below records each round start.
  • Safeguards: an evaluator that fails to produce a valid verdict is an infra strike (3 consecutive = terminal stop, never counted as fail); 10 total error turns stop the run; /ralph-stop (or aborting the run) ends it as a graceful failed with a self-report wrap-up. Every run writes a progress file under ~/.little-coder/delegations/<session>/ (the harness's only writer — the durable rolling memory each round sees) and records pipeline:run-state ledger entries + a state.json marker, so a crashed run is detected on the next launch like any other pipeline run.
  • Both commands refuse to run inside a subagent (env guard) — a child process must never drive the parent's loop.

Every subagent run is recorded in the session itself as a compact ledger entry (persisted but not printed — the subagent tool's own result already reports the run, and the entry keeps the durable pointer to the child transcript), and every /deep-plan//review* pipeline phase is recorded as a ledger entry that stays visible in /resume. Spawn-mode child transcripts are persisted as sidecars under ~/.little-coder/delegations/<session>/ so a run that crashes mid-flight is detected on the next launch (a one-line notice points at the preserved outputs). A crashed or gracefully-failed pipeline run can then be CONTINUED with /pipeline-resume <run-id> — it re-runs only the phases that never completed, reusing the preserved outputs of the done ones under the same run — or restarted from scratch with the original argument (both recovery commands are named in every pipeline failure notice). The dir holds only your delegation artifacts, is not pruned automatically (like pi's own session files), and is safe to delete at any time.

Pipeline phases run without a time budget by default: a wedged phase (e.g. a local model that stops producing output) is stopped only by cancelling the run (Esc). If you want a per-phase watchdog, set LITTLE_CODER_PIPELINE_PHASE_TIMEOUT_MS to a positive value in milliseconds (e.g. 1800000 for the former 30-minute default): a phase over budget is aborted and reported as a phase failure — in failure-tolerant fan-outs (the parallel reviews) only the wedged phase is affected. Invalid values fall back to the default (disabled) with a warning.

Aborted subagent runs report the reason they were aborted instead of a bare "Subagent was aborted.": e.g. Subagent was aborted: RESEARCH exceeded the 1800000 ms phase timeout (watchdog) or Aborted while waiting for a global concurrency slot: user cancel (a run that was still queued for a slot when the cancel landed).

little-coder uses reflection-generated skills and breadcrumbs for reusable session learning. Use /reflect, /reflect-review, /breadcrumbs, /skills, and /promote-user-skill to draft, review, search, load, and promote reusable guidance. Reflection writes accepted drafts to user-level ~/.pi/skills/<skill>/SKILL.md; /promote-user-skill [skill] copies stable user skills into repo skills/user/<skill>/ after duplicate checks so they can be packaged.

Use /usage for the inline usage dashboard and /insights for the vendored Pi Insights report.

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.

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 the next npm install -g github:L3tum/little-coder, so for anything you want to keep, use a user override file instead.

User override resolution (first match wins):

  1. $LITTLE_CODER_MODELS_FILE — explicit path, useful for ad-hoc tests.
  2. $XDG_CONFIG_HOME/little-coder/models.json
  3. ~/.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.)

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. A remote host can be set via the provider's baseUrl in models.json (or a user override file).

.pi/settings.json is a separate concern: it controls per-model profiles (context_limit, temperature, max_tokens, 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. A profile's max_tokens is applied to the outgoing request (capped by the model's registered context window); max_tokens: 0 disables the output cap on local servers (llama.cpp/ollama/lmstudio — the server clamps to the remaining context). On a remote provider max_tokens: 0 simply omits the cap (the model catalog default applies — remote APIs reject the unlimited sentinel). Profiles resolve from the package default, your per-user ~/.pi/agent/settings.json, and the per-repo .pi/settings.json (repo wins). A profile's max_tokens caps TOTAL output tokens — unlike the removed thinking_budget field, it does not separately cap the reasoning stream: there is no thinking-specific guard anymore.


Max concurrency

little-coder caps how many pi agent instances (top-level session requests + delegated subagents) may run at the same time across the whole machine — each is a full process using a few hundred MB of RAM. The cap is little_coder.max_concurrency in the global ~/.pi/agent/settings.json (default 8); when the pool is full, a new request or subagent is queued with a one-line notice instead of starting.

Set and view it with /max-concurrency [n] (the command enforces a floor of 2; values above 100 set but warn). Practical guidance: keep max ≥ 2 — a single session holds one slot of its own, so max: 1 would deadlock a session that delegates to subagents. For sessions doing parallel fan-out, ≥ 5 (the default 8) is comfortable.

Queueing is bounded: if a request or subagent can't get a slot, it waits up to a configurable bound — default 10 minutes, set with /max-concurrency-wait [minutes|ms] (stored as little_coder.concurrency_queue_max_wait_ms in milliseconds; the bound must stay finite and is capped at 1 hour — there is no "wait forever") — and a request that times out of the queue then runs UNCOUNTED against the cap (no slot held) rather than hanging forever. It does not start silently: it announces itself ("runs uncounted against the cap") so the over-admission is visible, not silent. This means even a pathological nested-delegation deadlock (a slot-holder waiting on its nested child's slot) resolves on its own after the timeout — at the cost of an uncounted run, never a stall.

Slot borrowing: when a session launches a subagent and waits on it, it lends its own slot to the subagent, so the waiting pair costs one slot, not two (one slot per waiting pair).

User-wait release: while a session is blocked on a user-wait tool call — a tool that waits for human feedback, by default ask_user and plannotator_submit_plan — it holds no slot: the slot is released as soon as the call starts (with a one-line notice) and re-acquired when the call returns, using the same bounded queue wait as a fresh request. So a session parked on a question or a plan review no longer pins a slot while it does no work — and when its tool returns it may queue behind other agents for a while (the same "queued, waiting for a free slot" notice) before continuing. A slot that is on loan to a subagent is never released (the child keeps its accounting); a re-acquire that times out of the queue degrades the same way as any other (the session runs uncounted with a warning, never hangs). The tool list is configurable per user in the global settings as little_coder.concurrency_user_wait_tools (an array of tool names; set it to [] to disable the release).

Pipeline fan-out: the programmatic pipelines (/review, /review-project, /review-focused, /review-quick, /deep-plan) bound their own parallel phase groups separately from the machine-wide cap above — by default 4 phases at a time (the same ceiling the subagent tool's parallel tasks use; each phase is a full pi process). Set it with /max-pipeline-concurrency [n] (stored as little_coder.max_pipeline_concurrency in the global settings): 1 runs a pipeline's phases sequentially (kind on small machines), values above the largest current fan-out of 7 (the themed reviews) still set but never bind. The cap is re-read at the start of each parallel phase group (each fan-out), so an edit applies to the next fan-out — even one later in a pipeline that is already running — it limits how many of ONE pipeline's own phases start at once; the machine-wide cap still bounds total instances.


Web UI (fleet dashboard)

little-coder ships a loopback-only fleet dashboard at http://127.0.0.1:15151 — a single page to watch and drive every little-coder process on the machine from your browser. It binds to 127.0.0.1 only (never your LAN) and is opt-in.

Port override. The default port 15151 can be overridden with the WEBUI_PORT environment variable (highest precedence) or the little_coder.web_ui_port key in the global settings (per-user settings.json, not per-repo). Both must be an integer 0–65535 (0 = ephemeral, OS-assigned); a present-but-invalid value is ignored (with a one-time warning) and the next source is used, so an unset/invalid configuration always falls back to 15151. The override exists for shared machines: each user has their own agent dir and can therefore run their own fleet dashboard — without an override, two users' dashboards both try to bind 15151, and the loser warns "port busy" every tick. Give each user a distinct port and both dashboards coexist (each still only sees its own agent dir's fleet).

Enable it from the TUI:

/web-ui on                  # start the dashboard (claims ownership if no live owner)
/web-ui status              # is it on? who owns it?
/web-ui off                 # stop serving
/web-ui force-release       # break a wedged owner election (reused dead-owner pid); self-healing
/web-ui-password <password> # protect the API with a shared-machine password
/web-ui-password clear      # remove the password

You can also set little_coder.web_ui: true in the global ~/.pi/agent/settings.json so it starts automatically.

Ownership & failover. Exactly one process owns the server at a time (an atomic, tie-broken claim in the shared state file). The rest of the fleet still appear as members. If the owner dies, another member re-claims within ~15 s and the browser reconnects automatically (the dashboard streams updates over Server-Sent Events and re-attaches on (re)connect — no manual reload). A headless-chromium browser smoke test runs the dashboard against a port-0 server and covers load → tail pane → live entry delivery → the password flow; it is in CI and can be run locally with npm run webui:browser.

What you can do in the browser:

  • Fleet list — every member with its cwd, running / idle / uncounted (started without a slot after the queue wait timed out or the slot state failed — it shows whether it holds a concurrency slot), mode, and session name; the owner is badged.
  • Owner (the process serving the dashboard) — first-class in-browser controls for its session: model picker, thinking level, tool toggles, context-usage, plus Compact / Stop / Rename / Quit. A live transcript tail and a slash-command palette are shown too. Prompt the owner in its terminal (or spawn a child below).
  • Attached TUI members (sessions you're running in a terminal) — a live transcript tail, a Send composer, and Stop / Compact.
  • Web-UI children — sessions the web UI spawns as headless --mode rpc processes. These get full control: prompt / steer / follow-up, Stop, Stop-bash, Compact, set model / thinking, New session, Clone, Export HTML, Rename — plus a slash-command palette and any extension-UI prompts (select/confirm/input) surface in the browser. Spawn one with the "+ New session" button: pick a whitelisted directory, optionally a model / thinking level / initial prompt, or resume an existing session from that directory's history (newest first). Kill a child with close; it's reaped when it (or the owner) dies.

Spawn whitelist. Web-UI children may only be spawned into directories you've explicitly allowed (a defense against path/symlink escape), and listing a directory's session history for resume is restricted to the same list. Both are defense in depth against buggy clients, not a boundary against a local actor — the same authenticated API can add any existing directory to the allowlist. Manage the list in the TUI or in the browser:

/web-ui-dirs add ~/projects
/web-ui-dirs list
/web-ui-dirs rm ~/projects

Slash-command rule. Slash commands (/…) are only ever delivered by the process running them — the web UI can't push a / line into an attached TUI session. So a /-prefixed message to an attached TUI member is refused (you run it in that terminal), and the owner's terminal-only commands (e.g. /new) run in the owner's terminal. The web's "New session" / "Resume" buttons instead spawn a child — and web-UI children can drive their own /new / session-switch from the browser (those are RPC commands for a child, not terminal-only).

Security / trust model

  • Loopback ≠ private — the no-password default is an accepted risk of the single-user model. The server binds 127.0.0.1 only, but it is a local control plane: with no password (the default), any user or process on the machine can read session transcripts, prompt/steer/abort agents, and — because the spawn allowlist's add-dir op also answers the API — point the allowlist at any existing directory and spawn headless agents there (passwordless, add_dir refuses / and /home — paths that subsume other users' homes — but any other existing dir can be added; with a password set, all are allowed as an explicit owner choice). There is no lockout, and whoever can supply the password can act on the whole API surface — the server verifies it against a stored salted PBKDF2-SHA256 record (see the /web-ui-password item below). On a shared machine, set a password: /web-ui on prints this warning whenever no password is configured, but note the server can be started from settings alone (little_coder.web_ui: true), which prints no warning — check that before deploying to a shared machine.
  • Owner election is liveness-gated. The owner renews its entry in the shared state file every 5 s; an owner that has not renewed within 30 s is treated as dead and its slot becomes re-claimable — so a dead owner whose pid was reused by an unrelated process (which previously wedged the election forever) no longer looks like a live owner. /web-ui force-release clears the owner entry to break a wedged election if it ever happens; the current owner re-asserts on its next tick, so it is self-healing.
  • /web-ui-password <password> protects the whole API surface (/api/* + /events). The password is stored only as a salted PBKDF2-SHA256 record (100k iterations) in the global settings (little_coder.web_ui_password_hash: {v:2, salt, iter, hash}); only the salted KDF record is ever stored, and the password is verified server-side. The browser keeps the typed raw password in sessionStorage and sends it in the X-Web-UI-Password header over the loopback connection; the server re-derives the PBKDF2 digest and compares it in constant time (a legacy bare sha256 value is malformed and locks the dashboard fail-closed until it is re-set). The SSE event stream (/events) is opened with a short-lived one-shot stream ticket minted by an authenticated stream_ticket API op — a credential never appears in a URL (tickets expire in 60 s and are single-use, re-minted on every reconnect). Set/clear takes effect on the very next request — no server restart. Requests without a valid password get 401 password required.
  • Static assets stay unauthenticated — they contain no secrets.
  • The same-origin guard is not a local-process boundary. It stops cross-origin browser pages from driving the fleet (CSRF), but it is trivially bypassed by any local process (curl, other users). The password is that boundary.
  • Web-UI children inherit the owner's full environment — an accepted single-machine risk. A child is spawned with the owner process's entire environment (the owner's process.env plus the web-child identity vars), so it carries every API key, token, and credential the owner has in its environment. That environment is fully visible to the child's local agent tooling — a bash call in the child can read it — so a spawned child can act with the owner's full credential set. This is deliberate on a single-user machine (the owner is already trusted to run arbitrary agents in the allowed directories), but it means the dashboard should only be run where you'd trust a locally-spawned agent to hold the owner's secrets. There is no environment scrubbing in v1.

v1 limitations (web UI)

  • Loopback only — no remote access. Local-process auth is the optional shared-machine password (/web-ui-password, see the Security / trust model above); without it the surface relies on the 127.0.0.1 bind + the same-origin CSRF guard below. The auth prompt applies a short client-side submit backoff after failures, but the server has no lockout (an accepted v1 limitation on loopback).
  • Owner controls act on the owner's current session; the owner's own /new is terminal-only. To start fresh work from the browser, spawn a web-UI child.
  • Attached TUI members can be sent text + stop + compact, but not driven at the command level (that's what the terminal is for).
  • The spawn whitelist is empty by default — add a directory before spawning.
  • CSRF guard — the dashboard enforces same-origin checks against its ACTUAL bound port (default http://127.0.0.1:15151 / http://localhost:15151; a WEBUI_PORT / little_coder.web_ui_port override moves the origin with it): requests carrying an Origin/Referer from any other origin are rejected (browser must be opened at the dashboard URL; curl/scripts without Origin still work). This is why drive-by requests from other web pages can't drive the fleet. It is explicitly not a local-process boundary — the password feature is.
  • Instance-token membership (pid + per-process token) — fleet membership, the ops queue, tailers, ownership claims, and a child's owner-liveness check are validated against a per-process instance token (a randomUUID minted once per process at extension load, carried in the process's member entry and owner entry). Delivery/claim paths are token-validated under the shared state lock at delivery time: a recycled pid that re-registered with a fresh token cannot be misdelivered ops (the op is refused, not misrouted), cannot consume the dead original's queued ops, cannot silently retarget a live tailer (the tailer STOPS; the pane re-POSTs to re-arm), and cannot ride the dead original's startedAt in the owner tie-break (same-pid re-registration under a foreign token replaces the entry wholesale; a foreign-token owner entry is taken over on re-assert). Reaping rules stay pid/staleness-based by design — there is no way to query "is THIS token alive?" — so a recycled pid within the ~5-10 s reap window is still misread as live in the fleet view (the same accepted pid-reuse window as the concurrency slot file). Residual accepted risk: registration itself remains pid-keyed within one agent dir (same user) — a local same-user process can still hand-edit the state file, and the cross-machine trust model is unchanged.
  • Newer state files are read-only — a state file written by a newer little-coder (schema v > 1) is left untouched by this version: it reads as an empty fleet but is never downgraded or wiped.
  • Orphaned web-UI children — a child self-terminates when its owner process dies (parent-pid check on its 5 s tick), but a child spawned in the last moments of the owner's life can outlive the owner by up to one tick.
  • Owner step-down is immediate — if the current owner's event loop stalls for ~30 s a successor may win ownership, and the stepped-down owner's web-UI children are terminated on the spot; a grace period is a tracked follow-up.

Permissions

little-coder gates Bash tool calls 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.

Two env vars control the gate:

Env var Values Effect
LITTLE_CODER_PERMISSION_MODE auto (default) / accept-all / manual auto: block any bash command not on the whitelist. accept-all: skip the gate entirely, every bash call passes (the benchmark runner sets this). manual: same as auto but with a different rejection message.
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.

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

Write/Edit confirmations are pi's responsibility; little-coder doesn't intercept those.

Settings-file allowlist

The same allowlist can live in the existing pi settings files, which is usually better than an env var: it's per-user or per-repo instead of per-shell.

File Scope When it's honored
~/.pi/agent/settings.json (or $PI_CODING_AGENT_DIR/settings.json) per-user always
<repo>/.pi/settings.json per-repo only after the project is trusted (pi's project-trust flow) — an untrusted repo's settings file can't widen the shell allowlist
{
  "little_coder": {
    "bash_allow": ["make ", "docker compose ps "]
  }
}
  • Same word-boundary convention as the env var: entries are prefix-matched, and the trailing space is the word boundary ("make " allows make test but not makefoo; "docker compose ps" without the trailing space would also match docker compose psfoo — add the space). Project-scope entries (the repo's .pi/settings.json) are auto-normalized to word-boundary form — "make" is stored as "make " (allows make …, not makefoo); global-file and env entries keep their exact form (your own values, your caveat).
  • Merged additively with the built-in list and LITTLE_CODER_BASH_ALLOW (pure union — settings can only add, never remove built-ins).
  • In a repo's .pi/settings.json, bash_allow accepts the array form only (object/per-repo maps are a user-file concept; an object there is silently ignored).
  • Project trust for this gate follows pi's trust.json: a parent directory marked trusted also trusts every repo under it — so a repo that lives under a trusted parent inherits that trust, and its own .pi/settings.json bash_allow is honored. Warning: trusting a parent directory auto-approves shell commands (bash_allow) for every repo under it — the trust decision is stored per agent dir in trust.json, not per repo.
  • Session-only trust is not honored by this gate. pi's interactive trust prompt offers "trust for this session only"; that session-level grant is not persisted to trust.json, so little-coder's trust gate does not see it — per-repo settings (bash_allow, model profiles, the token_limit_auto_continue opt-out) stay off until the project is persistedly trusted (/trust) or a new session starts.
  • The settings-file allowlist is cached for the session (keyed on the canonical repo path); a mid-session edit to a settings file applies after the next /allow//deny/--reload or the next session, not the next turn. (This is deliberate — it keeps the one-time notices stable — while other little_coder.* settings are re-read on every use.) Trust revocation is one-way mid-session: un-trusting a project immediately drops its project-scoped prefixes (re-checked per call), but re-trusting does not restore them until the next session_start//allow//deny/--reload.
  • When a per-repo file contributes prefixes, the session shows a one-time notification (bash allowlist: N project-scoped prefix(es) active from .pi/settings.json: "make ", "docker compose ps ", …) naming the active prefixes (first six, then a count) so the repo's auto-approval is visible, not silent. Same for the /allow-based repo-scoped notice.
  • Malformed entries (non-strings, empty) are ignored without failing startup.

Per-repo allowlists: /allow and /deny

/allow <command> and /deny <command> manage an allowlist for the current repo only, so you can approve make test in one project without widening the list everywhere. By default they write to your user settings file (~/.pi/agent/settings.json) — never into the repo's own .pi/settings.json, which is deliberate: a repo must not be able to auto-approve its own shell commands (that's exactly what the trust gate prevents), and nothing gets committed into your working tree.

bash_allow in the user file accepts two equivalent shapes:

{
  "little_coder": {
    "bash_allow": {
      // per-repo map (what /allow writes)
      "global": ["make "], // reserved key = applies everywhere
      "/home/tom/workspace/little-coder": ["docker compose ps "]
    }
  }
}

The plain-array form above stays fully supported (purely global). When /allow runs against an array-form file, it converts it to the map form, preserving existing entries under the reserved "global" key. Once a repo's list is emptied again the value collapses back to an array.

  • Repo key = the directory you launch little-coder from (canonicalized with realpath, the same convention as trust.json). Because the key is the canonical launch path, moving a repo (or adding a new symlink alias) orphans its /allow entry in your global settings — re-run /allow from the new path.
  • Input is normalized to a word-boundary prefix: /allow make test stores "make test " (allows make test …, not make build); a single pair of surrounding quotes is stripped (/allow "make test" stores "make test ").
  • Changes apply immediately in the running session and persist across launches; a one-time notice names the active repo-scoped prefixes (first six, then a count).
  • /deny only removes settings-file entries. If the command is still allowed via the built-in safe prefixes, LITTLE_CODER_BASH_ALLOW, or your global list, the reply says exactly which source keeps it allowed — built-ins can never be removed (use a different LITTLE_CODER_PERMISSION_MODE or remove the global entry instead).
  • The writer is fail-safe: a malformed settings.json is never clobbered; the command reports the error and leaves the file untouched.

Historical paper / benchmark results (source project)

Fork benchmark reruns under L3tum: TBD. Table below preserves source-project results for attribution and context.

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.


Troubleshooting

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.

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.

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.

Extension load failures on startup — run little-coder --list-models --verbose; extension errors surface there. If the install looks corrupt: npm uninstall -g little-coder && npm install -g github:L3tum/little-coder.

Launch feels slow / measure it — LITTLE_CODER_TIMING=1 little-coder prints launcher phase timings, the child preload cost, and pi's own per-extension startup timings on stderr. The update check never blocks startup on the network anymore (cache-only + background refresh: the "update available" notice reflects the last successful online refresh — offline launches can see an arbitrarily stale latest); the llama.cpp context probe is disk-cached (refreshed by a background re-probe on each launch; a warm launch uses the cached value immediately) and defaults to a 500 ms timeout. See docs/startup-performance.md for the full breakdown and profiling recipe.

Output cut off repeatedly on a local server — the token-limit auto-continue loop nudges the model to resume (up to 3 times, then a conciseness correction, then it backs off at 5; with compaction enabled (default) recovery is compaction, without it the turn aborts). Disable it with little_coder.token_limit_auto_continue: false in your settings file or LITTLE_CODER_TOKEN_LIMIT_AUTO_CONTINUE=0 — the off-switch then intervenes (and aborts when compaction is disabled) on every token-limit turn. The per-repo token_limit_auto_continue is honored only for trusted projects (like bash_allow); an untrusted repo cannot disable the safety net, so its per-user (global) value applies.

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/L3tum/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.

Testing. npm test runs the vitest suite. npm run test:all is the local one-shot for a push: vitest + both typecheck tiers (typecheck, typecheck:checkjs) + the web-UI child E2E (npm run webui:e2e → scripts/webui-child-e2e.mjs, real owner + real child processes — the dead-child retention/diagnostics path included). CI (.github/workflows/test.yml) runs that core plus lint, the concurrency E2Es, the browser smoke, and the owner-failover script.


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


Attribution

This fork is maintained by L3tum.

Source project: Itay Inbar's little-coder, Apache 2.0. Historical paper claims, benchmark figures, and release links in this README refer to that source project unless noted otherwise.

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, output-parser, quality-monitor, per-model profiles, per-benchmark overrides, Browser / Evidence tool families, evidence-aware compaction — are preserved across versions.


Dependency policy

little-coder's dependencies are hand-picked for the pi runtime: they must load under pi's ESM extension system, stay out of the launch critical path, and not bloat the node_modules that ships inside the npm package. Do not add a dependency without verifying it loads under pi's ESM extension system and measuring its startup cost — see docs/startup-performance.md for the budget and the profiling recipe.


License

Apache 2.0 — see LICENSE for details. NOTICE tracks upstream attribution.

About

A coding agent optimized to smaller LLMs

Resources

Stars

49 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages