Skip to content

v1.12.0

Choose a tag to compare

@github-actions github-actions released this 24 Jul 17:58
24dea1c

Fixed

  • little-coder no longer ships an npm install script, so Socket's malware scanner has nothing to flag (#75 by @modemlooper, diagnosed as a false positive by @abhisek). npm install -g little-coder was producing Potential malware detected with AI scan from Socket Firewall, pointing at the postinstall hook. The hook ran scripts/patch-pi.mjs — a visible, dependency-free file in the repo that re-applies two cosmetic source edits to the bundled pi — so the alert was a false positive on the shape of the code (an install script touching node_modules) rather than on anything it did. Rather than just explain that, v1.12.0 removes the postinstall entry entirely: the launcher already calls applyPiPatches() on every launch, and every self-updating user was skipping the postinstall anyway because /update and the launcher's auto-update both install with --ignore-scripts (#50). Launch-time patching is now the only path, which also means it self-heals if pi is reinstalled underneath. scripts/patch-pi.mjs itself is unchanged in behavior and still shipped — it's imported by the launcher, no longer executed by npm.

  • The launcher's pi patching and pi-changelog suppression were both silently dead (found while investigating the above; no issue). bin/little-coder.mjs resolved pi's package root inside a for (const piPkgRoot of piPkgCandidates) loop and then referenced piPkgRoot twice further down — at the patch call and at the lastChangelogVersion pin. A for (const …) binding is scoped to the loop body, so both references threw ReferenceError, and because both sit inside deliberately best-effort try/catch blocks the failure was completely invisible. Two real consequences: our pi runtime patches never re-applied at launch (so a --ignore-scripts upgrade left pi unpatched), and lastChangelogVersion was never written — which meant pi's own upstream "What's New" changelog rendered inside the little-coder TUI after every bundled-pi bump. The binding is now declared at module scope and assigned on the successful candidate. Two end-to-end regression tests (bin/launcher-pi-root.test.mjs) run the real launcher and assert both effects land; both fail against the old code.

  • ShellSession no longer routes around the write guard and the permission gate (#70 by @rvanswieten). Qwen3.6-35B-A3B, refused a whole-file write, switched to cat > backend/main.py << 'ENDOFFILE' and got the same bytes anyway — 5 times in one session (main.py ×3, App.jsx, pyproject.toml). rvanswieten's diagnosis was correct on all three counts: write-guard matched only toolName === "write", permission-gate matched only bash/Bash, and ShellSession hit neither, landing straight in execSync. The bash whitelist wouldn't have saved it either — isSafeBash was startsWith on the raw string and cat is whitelisted, with no awareness of the > immediately after it. Fixed with a shared, pure command analyzer (_shared/shell-write.ts) that strips heredoc bodies (so a > or an apostrophe in the payload can't confuse it) and reports the paths a command writes to via >, >>, tee, or dd of=, ignoring fd duplication (2>&1, >&2), process substitution, input redirects, and anything inside quotes. Three behavior changes follow: ShellSession is gated exactly like bash; every command in a &&/||/;/| chain must pass the whitelist independently (ls && rm -rf / is refused on the rm, not admitted on the ls); and any command that writes through the shell is refused in auto/manual mode, with a message naming the path and pointing at Write/Edit. In accept-all mode (benchmark runs) the whitelist is still skipped, but write-guard now inspects shell writes too, so a redirect that would clobber an existing file gets the same Edit recipe the write tool gives, and a reserved Windows device name (#60) is refused even as an append. A >> append to an existing file is allowed — nothing is destroyed, so it isn't the whole-file-rewrite failure mode the guard exists to prevent. Deliberately not matched on the heredoc delimiter string, which any other delimiter would have defeated.

  • Per-turn skill and knowledge injection no longer destroys the KV cache (#73 by @manueloverride, with @charly1r). manueloverride caught this with cache-hunter: llama.cpp suddenly re-churning 120k of message history "for no reason" mid-conversation, because the harness had changed the beginning of the prompt. Exactly right. skill-inject and knowledge-inject appended their selected blocks to the system prompt, which is the first thing in every request — and since both blocks are recomputed per turn from the user's prompt, they changed on most turns and invalidated the entire cached prefix each time. The fix uses a hook pi already had: before_agent_start may return a message instead of a systemPrompt, which pi appends after the user's message and converts to a user-role message on the way to the provider. The guidance now lands at the tail of the conversation with every preceding byte untouched, so the prefix stays cached and only the new tokens are processed. The recency argument that put these blocks last in the system prompt gets stronger rather than weaker — the conversation tail is as late as placement gets. All four injectors moved onto one shared helper (_shared/inject.ts): skill-inject, knowledge-inject, plan-mode's synthesis instructions, and deep-research's report brief (the largest of the four, and the one where a system-prompt rewrite cost the most). Blocks are hidden from the transcript (display: false), and a block identical to the previous turn's is not re-sent — the earlier copy is still in the conversation, so repeating it would only spend context. LITTLE_CODER_INJECT_MODE=system restores the old placement, which is what the whitepaper scaffold reproduction was measured against. A regression fence now fails the build if any extension returns a rewritten system prompt directly, since the symptom is invisible from inside little-coder.

    Measured against a live llama.cpp server (Qwen3.6-35B-A3B, 4 turns, same prompts in both modes, counting the prompt tokens llama.cpp actually evaluated per request):

    Turn context old: system prompt new: tail message
    1 ~10.6k 6,525 6,529
    2 ~16.5k 12,311 6,235
    3 ~23k 18,488 6,377
    4 ~29k 24,586 6,438
    total 61,910 25,579

    The old numbers climb with the conversation — each turn re-evaluates almost everything, which at manueloverride's 120k context is the 120k re-churn he reported. The new numbers are flat: only the genuinely new tokens are evaluated, whatever the history length. 58.7% fewer prompt tokens over four turns, and the gap widens with every additional turn.

    Note for anyone who arrived from the same thread: the llama.cpp/Qwen KV-cache bug charly1r linked is a genuinely separate problem, upstream of little-coder.

  • The startup header no longer advertises a key that does nothing (#74 by @heinrichI). The header's hint row listed ctrl-r more, but pi binds "expand / more" to app.tools.expand = ctrl+o; ctrl+r is bound only inside the session-tree overlay, so at the prompt it genuinely did nothing. Corrected to ctrl-o, and f2 (deep research) was missing from both the header and the ctrl+h shortcuts panel — the one flow people ask about was absent from the panel whose whole job is discoverability. Both now list it, and buildHeader is covered by tests that assert every advertised key is a real binding. The ctrl+o half of the report is expected behavior rather than a bug, now documented: during a Deep Research run the research sub-coders are separate child processes, so their tool output never enters this session's transcript and there is nothing for ctrl+o to expand — the progress bar is the view of that work — and while the max-agents or clarifying-question dialogs are open, keys belong to the dialog.

  • Two panels were silently losing their last rows to pi's widget height cap (found while verifying this release against a live TUI; no issue). pi renders a string-array widget through content.slice(0, MAX_WIDGET_LINES) — the cap is 10 — and appends ... (widget truncated), so anything past ten lines is dropped from the end. Two places were over it: the ctrl+h shortcuts panel had grown to eleven rows plus a header, which quietly ate /hotkeys — the row whose entire job is pointing at the authoritative keybinding reference — so it now lays out in two columns and shows every shortcut in seven lines; and the sub-coder tracker appends a row per sub-coder in begin() and never resets, so a session with several dispatch turns accumulated rows without bound and pi's truncation then hid the sub-coders that were actually running behind a backlog of finished ones — exactly backwards for a live progress view. The tracker now always shows every running sub-coder, fills the remaining rows with the most recently finished, and accounts for the rest on one … +N earlier sub-coders line, with the header still reporting the true total. Both are covered by tests that assert the ten-line ceiling.

Added

  • A first-class, opt-in way to add your own extensions (#67 by @thegausiantheory; #69 by @johnzan, with @charly1r). little-coder launches pi with --no-extensions and loads exactly its bundled set, which is what keeps the cold-start context near 7k tokens and makes behavior predictable — charly1r's write-up on #69 explains the tradeoff better than the docs did. But "I want to add my own" is a fair ask, and the only answer was an env var nobody found (LITTLE_CODER_EXTRA_EXTENSIONS) or forking the installed package (#46). Three additions, all opt-in, all no-ops when unused:
    • A user extension directory. ~/.config/little-coder/extensions/ (or $XDG_CONFIG_HOME/…, or LITTLE_CODER_EXTENSIONS_DIR). Each direct child is one extension — a .ts/.js/.mjs file, or a directory with an index.ts/index.js. Loaded after the bundled set, so yours can override ours. The directory is never created for you and nothing is written to it; an install that ignores it behaves exactly as before. Survives npm install -g little-coder@latest.
    • /extensions — a panel showing what's loaded, split by where it came from (bundled / yours / LITTLE_CODER_EXTRA_EXTENSIONS), whether pi's own discovery is on, and anything that failed to load. Because little-coder forces quietStartup to keep launch clean, --verbose used to be the only window into this, and a user extension that didn't resolve warned once on stderr moments before the TUI painted over it — those warnings now also fire as a notification at session start. Rendered as a widget rather than a chat message on purpose: a custom message would be sent to the model, spending context on a diagnostic it has no use for.
    • --with-pi-extensions (or LITTLE_CODER_PI_EXTENSIONS=1) — omits --no-extensions so pi discovers its own extensions from ~/.pi/agent/extensions and ./.pi/extensions. Off by default and it prints a notice when on, because the guarantee it gives up is real: the extension set is no longer fixed, cold-start context grows, and a cloned repository can contribute extensions from its .pi/ directory (pi's own trust prompt still gates that code).
  • New guide: docs/extensions.md — how to write an extension, where to put it, which bundled extension to read as a reference for each kind of change, the two things to know before writing one (don't rewrite the system prompt per turn; cap rendered lines to the terminal width), and a community list: BMorgan1296's Zed/pi-acp bridge (#58), johnzan's Telegram bridge (#69), and charly1r's llama.cpp configs and local-model tournament results (#63). Community projects are linked, not vendored.

Docs

  • The status line is documented (#71 by @johnzan, first answered by @charly1r). A new README section explains every field, read off pi's footer.js rather than inferred. Three corrections to the reasonable-looking guess in the thread: the first number is cumulative session input tokens, not current context size; CH is the cache-hit rate of the latest response alone (cacheRead / (input + cacheRead + cacheWrite)), not a session average, which is why it swings; and (auto) specifically means automatic compaction is enabled. A low CH on a long conversation is a real signal — it's exactly what #73 above was causing.
  • Three stale README facts corrected. Plan Mode is ctrl+q, not alt+p (it moved in v1.9.0 and the README never followed). Sub-coder concurrency defaults to 1, serial, not 2 (changed in v1.9.10 by #57). And the claim that pi themes don't load was wrong: --no-extensions gates extensions only — pi's theme discovery is governed by a separate --no-themes flag that little-coder never passes, so pi themes in ~/.pi/agent/themes and ./.pi/themes have always worked. That correction also applies to my earlier answer on #67.
  • New Troubleshooting entries for the malware alert, ctrl+r, pi extensions and themes, extension load failures, and the llama.cpp reprocessing symptom.