Skip to content

Latest commit

Β 

History

173 Commits

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

SLMCode

⚑ SLMCode

A coding harness built for small local models.
Constrained decoding Β· a real tool interface Β· memory that compounds Β· terminal + web UI
Defaults to oMLX Β· works with any OpenAI-compatible endpoint

UnicoLab release CI go license


πŸŒ… The pitch

LLMs are incredible. Coding with them β€” inside a well-adapted harness β€” feels like magic.

And the industry noticed. Claude Code, Antigravity, Pi, and a growing wave of specialized coding agents were all designed around frontier models: huge context windows, strong tool-calling, and enough judgment to survive messy repos.

That is fantastic… until you run out of tokens. And eventually, you will.

Then you try the same harness on an SLM β€” a 7B–32B local model β€” and the magic evaporates. The model wanders. JSON breaks. Context overflows. Reviewers hallucinate green lights.

SLMCode exists to fill those gaps β€” and to stay useful when you plug a bigger model back in.

Fork it. Break it. Point it at whatever LLM you have. πŸš€


What makes it different

πŸ”’ Constrained decoding, negotiated per endpoint Every structured role has a hand-written JSON Schema and a generated GBNF grammar. At startup the harness probes your endpoint and picks the strongest mechanism it actually supports β€” json_schema β†’ vLLM guided_json β†’ llama.cpp grammar β†’ json_object β†’ prompt-only β€” caching the result and silently demoting if the server changes its mind.
🧰 A tool interface designed for small models ws_edit tries five progressively more tolerant match strategies and only ever applies a unique match, telling the model which rung hit. ws_patch anchors each hunk on its @@ line numbers within ±20 lines and reports per-hunk status. An edit that breaks a file that previously parsed is reverted, in-band, on the same turn.
πŸ“ Context budgeted in tokens, not bytes The pack budget is derived from the model's real context window minus system prompt, tool schemas and response reserve. Assembly is byte-deterministic with a stable prefix so local KV-cache prefixes actually hit. A tree-sitter-free repo map ranks files by PageRank over a symbol reference graph.
🧠 It gets better at your repo Four memory layers, failure fingerprinting, and a repair-rule store: a given failure mode costs an LLM round-trip once. A Thompson-sampling bandit learns which harness settings work for your model family and language. All of it is plain JSON under .slmcode/, and deleting it is supported.
πŸ›‘οΈ Gates that fail closed Disk state is authoritative β€” a claimed edit that is not on disk does not pass. Truncated reviewer JSON fails closed. The QA gate cannot report green when tests fail. A HITL gate with a human attached blocks instead of expiring into an auto-approval.
πŸ–₯️ Two front ends A non-blocking terminal REPL (Esc to interrupt and redirect mid-run, / fuzzy command picker, real unified diffs, interactive slmcode apply) and Studio, an offline React SPA with a live SSE feed, a pending-change review UI and run traces.

⏱️ 60-second start

Install

# macOS / Linux / WSL
curl -fsSL https://raw.githubusercontent.com/UnicoLab/smlcode/main/scripts/install-remote.sh | bash

# Windows (PowerShell)
irm https://raw.githubusercontent.com/UnicoLab/smlcode/main/scripts/install.ps1 | iex

# Homebrew
brew install --formula https://raw.githubusercontent.com/UnicoLab/smlcode/main/Formula/slmcode.rb

Full matrix (CMD, pinned versions, uninstall): docs/install.md

Locked-down work machine? If brew, go and release downloads all 403 on you, clone and install from the binaries carried in the repo β€” no Homebrew, no Go, no downloads:

git clone --depth 1 https://github.com/UnicoLab/smlcode.git
cd smlcode && ./scripts/install-offline.sh --add-to-path

Details: docs/install-offline.md

Or from a fresh clone

git clone https://github.com/UnicoLab/smlcode.git && cd smlcode
make bootstrap          # needs Node 18+: installs web/ deps and builds the Studio UI in
make install-user       # β†’ ~/.local/bin/slmcode

make bootstrap is the one step that needs Node. It installs web/'s npm dependencies and runs the Vite build into cmd/slmcode/ui/, which is go:embeded into the binary. Note that web/package-lock.json is currently out of date with web/package.json, so npm ci cannot run; make bootstrap says so and falls back to npm install, which regenerates the lock β€” commit the regenerated web/package-lock.json. Full story: CONTRIBUTING.md.

No Node? go build ./cmd/slmcode works on its own β€” everything except the Studio SPA. The binary then serves a built-in placeholder page that tells you to run make bootstrap, and slmcode studio says the same on startup. The CLI, the TUI and the Studio API are unaffected.

Run it

cd your-project
slmcode init                          # scaffolds .slmcode/ (memory, board, config)
                                      # detects your language and applies the matching pack
slmcode doctor                        # provider, model, endpoint, workspace β€” run it if init
                                      # reported that nothing answered at your endpoint
slmcode run -v "add JWT validation"   # full pipeline, live stream
slmcode                               # or: interactive TUI
slmcode studio                        # or: web UI β€” open the tokenised URL it prints

init is first on purpose: every other command answers from built-in defaults until a workspace exists, and says so. slmcode run on a terminal pauses at the plan gate for a single keystroke; headless it stops with exit 6 and prints the flag that lets it run unattended.


πŸ”Œ Any OpenAI-compatible endpoint

You have… Use
Apple Silicon, local provider=omlx (default, http://127.0.0.1:8000/v1)
Ollama --provider ollama --model qwen2.5-coder:14b
LM Studio / vLLM / llama.cpp --provider lmstudio --endpoint http://127.0.0.1:1234/v1
OpenAI / Groq / DeepSeek / Google / Mistral / Together / Fireworks built-in endpoint presets; slmcode stack list for shipped stacks
OpenRouter / a corporate gateway any provider name + --endpoint + API key
slmcode run --provider ollama --model qwen2.5-coder:14b \
  --endpoint http://127.0.0.1:11434 "fix the flaky test"

export SLMCODE_PROVIDER=openrouter SLMCODE_MODEL=… SLMCODE_API_KEY=…
slmcode run -v "…"

slmcode stack list && slmcode stack apply deepseek

Capability negotiation means a llama.cpp server gets GBNF grammars, vLLM gets guided_json, OpenAI gets strict json_schema, and a bare endpoint falls back to prompt-only JSON plus the repair ladder. Details: docs/decoding.md Β· docs/providers.md


🧬 The pipeline

16 phases in 5 groups. context+explore and architect+clarify run concurrently.

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ Prepare ──────────┐ β”Œβ”€β”€β”€β”€β”€ Design ─────┐ β”Œβ”€β”€β”€ Build ───┐ β”Œβ”€β”€ Verify ──┐ β”Œβ”€ Finish ─┐
β”‚ init β†’ skills β†’ context     β”‚ β”‚ architect        β”‚ β”‚ coord       β”‚ β”‚ polish     β”‚ β”‚ memory   β”‚
β”‚   β†’ explore β†’ docs          β”‚ β”‚   β†’ clarify      β”‚ β”‚   β†’ execute β”‚ β”‚   β†’ test   β”‚ β”‚   β†’ done β”‚
β”‚                             β”‚ β”‚     β†’ plan       β”‚ β”‚     β†’ learn β”‚ β”‚            β”‚ β”‚          β”‚
β”‚   context βˆ₯ explore ⚑      β”‚ β”‚       β†’ split    β”‚ β”‚             β”‚ β”‚            β”‚ β”‚          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The dynamic_pipeline composer (on by default) selects a task-specific subset before workers run. slmcode compose "…" previews that selection without calling the LLM.


✨ Features

Tool layer (ACI)

Tool Notes
ws_read 120-line window with offset/limit; reports total line count; line-number gutter is display-only
ws_edit 5-strategy match ladder (exact β†’ trailing-whitespace β†’ indentation-normalized β†’ blank-line-insensitive β†’ first/last-line anchored); unique matches only; empty old_str refused
ws_patch Unified diff (@@ anchored, Β±20 lines, per-hunk report, all-or-nothing) or SEARCH/REPLACE blocks
ws_write New files; overwriting an existing file requires a prior read; catastrophic-shrink guard
ws_grep Real RE2 regex, falls back to literal substring and says so
ws_glob, ws_list, ws_mv, ws_delete Path tools; ** globs; git mv when available
ws_shell One command, 2-minute default timeout, process-group kill, bounded output buffer
ws_todo Short checklist echoed back, so the plan stays in recent context
ws_skill Pull a skill's full body on demand (progressive disclosure)
git_status, git_diff Read-only git

Every result is hard-capped with steering text on truncation. Post-edit syntax checks run for Go, Python, JavaScript and JSON and return in-band. Full reference with failure messages: docs/tools.md

Specialists

20 built-in roles: coordinator, orchestrator, context, explorer, docs, architect, planner, splitter, worker, deep, reviewer, reviewer-strict, corrector, tester, placeholder, escalate, memory, composer, describer, editor.

describer/editor is the architect/editor split: the describer reasons in prose with no tools and no format constraints, the editor only formats, with constrained decoding and tools. Their models are independently selectable, so a 32B can reason and a 7B can apply. Custom and per-language specialists come from YAML blocks. β†’ docs/agents.md

Building blocks

Kind Purpose Built-in
Pack Composes pipeline + quality + agents + skills 13
Pipeline Phase graph with language-specific slots 13
Agent Custom specialist or built-in override 35 (go-worker, ts-reviewer, kotlin-tester, …)
Quality Lint/test/build commands per language 13

The thirteen packs: go, python, react, typescript, web, rust, java, kotlin, dotnet, ruby, php, swift, cpp. Also shipped: 29 skills and 13 provider stacks.

slmcode init picks the pack for you. Detection is scored, not first-match: a marker file in the root counts, a detect.contains proof of the file's content counts more, stray source files count least, and a nested sub-project's files do not count at all β€” so a Go module with a Vite app in web/ stays Go, and a package.json is react or typescript depending on whether it actually declares React. Apply one explicitly with slmcode blocks apply <pack>. β†’ docs/blocks.md

Safety model

Knob Default Effect
permission auto auto writes Β· dry-run never writes Β· review stages diffs to .slmcode/pending/
shell_permission allow allow Β· ask (records, does not execute) Β· deny
shell_whitelist true Read-only and build/test commands auto-run; interpreters and file mutators are refused unless allowlisted
write_guard, read_before_edit, claims_gate, static_quality, over_edit_guard true Scope, evidence and stub guards

The whitelist is tiered: ls/cat/grep/go test/pytest/npm test run; python, node, make, npx, sh, awk (executors) and sed, cp, mv, rm, tee (mutators) are refused with an explanation and a suggested allowed equivalent β€” because a shell that can run anything makes every other guard decorative. Allowlist them with shell_allow or SLMCODE_BASH_ALLOW. Flags that smuggle a second program past the list (env python -c, find -exec, go test -exec, cmake -P, go generate, pytest -p) are refused per binary. β†’ docs/permissions.md

Residual risk β€” what is not enforced

The guards above are real, and they are not a sandbox. Two things remain true after every one of them, by design rather than by oversight:

  • ws_shell is a command allowlist, not a filesystem jail. The ws_* file tools are jailed to the project root; the shell is not. It decides which command may run, not which files that command may touch β€” an allowed cat, grep or find reads anything the user account can read (~/.ssh/id_rsa, ~/.aws/credentials, another project's .env) and the contents go to the model, and therefore to whatever endpoint you configured. The write side is narrow (mkdir and touch are refused outside the root, mutators are refused entirely, redirection onto an existing file is refused), so the honest description is read exfiltration, not out-of-tree modification β€” but it is real. What the harness does enforce here is narrower and worth knowing: every tool result is scrubbed of the credential values it knows about (configured keys, .slmcode/auth.json, provider env vars), so those specific values do not reach the model even via cat. Any other secret in reach of the account does.
  • Verifying a project runs the project's own code. npm test executes package.json scripts, pytest imports conftest.py before a single test runs, go build honours #cgo, cargo build compiles and runs build.rs, ./gradlew runs a script committed to the repo. Pointing slmcode at an untrusted repository is equivalent to running that repository's build. If you would not run npm install && npm test in that checkout by hand, do not point an agent at it either.

What is not on that list, because it is closed: a repository cannot make slmcode run execute code of its own choosing before the model says anything. .slmcode/hooks.json fails closed β€” it needs hooks_enabled: true and an explicit per-content approval (slmcode hooks trust, recorded in your user config, never in the repo) β€” and mcp_servers is honoured only from your user config layer, because each entry is spawned as a child process at startup. Both refusals name the exact command that did not run.

These are inherent to what the tool does; no addition to the allowlist removes them. The enforcing boundary, if you need one, is the operating system's: run slmcode as a user that can only reach the project (container, VM, dedicated account), or set shell_permission: ask to approve each command, or shell_permission: deny to keep only the jailed ws_* tools. Full detail, including every refused flag and why: docs/permissions.md

Human-in-the-loop

Gate Default Asks about
clarify_mode ask language / stack / framework before planning
plan_approve ask the plan, before any worker runs
continue_ask ask another wave or stop, when retries are exhausted
escalate_ask ask retry / re-scope / abort for a task at max retries
shell_permission allow shell commands, in ask mode

With a TTY attached these render inline and block until answered. Headless, the decision is taken at run start, before the first model call, and logged: with --on-gate-timeout unset the four convenience gates answer themselves with "yes", while an explicit stop/reject refuses the run at the door (exit 6) instead of planning for minutes and discarding the result. shell_permission=ask is a safety gate and never auto-approves β€” headless it refuses up front. A run that does stop names the retained .slmcode/queries/<runID>/ board and the slmcode session resume <runID> command.

Studio

slmcode studio β†’ the URL it prints, http://127.0.0.1:7420/?t=<token>. Live SSE feed with resumable event ids, kanban board, pending-change review with per-file diffs and apply/reject, run traces, pipeline and agent editors, file inspector, skills, markdown memory, settings.

Loopback-only, same-origin enforced, no permissive CORS, and a per-launch session token that guards everything, the HTML shell included β€” a bare http://127.0.0.1:7420/ gets a 401 page telling you to go back to the terminal. Presenting the token once mints an HttpOnly, SameSite=Strict cookie, so it stops travelling in URLs. Being honest about what that buys: the token is printed to your stdout and lives in the server process, so it bounds other origins and local listeners that are not you β€” it is not a sandbox against something already running as your user. --no-auth drops it entirely. β†’ docs/studio.md

Self-improvement

.slmcode/memory (episodic, semantic), ~/.slmcode/memory (procedural, per model family + language), .slmcode/evolve (repair rules, regression checks), ~/.slmcode/evolve (bandit posteriors), .slmcode/metrics/runs.jsonl (per-run metrics + Compare). Everything is readable JSON; rm -rf on any of it is supported. β†’ docs/self-improvement.md


⌨️ CLI

Command Purpose
init Β· doctor Β· readiness Workspace scaffolding, provider health, SLM-readiness score
run Β· chat Β· tui (bare slmcode) Full pipeline Β· classic REPL Β· interactive TUI
apply Β· reject Β· diff Β· commit Review and land agent changes
status Β· board Β· watch Β· compose Β· task Β· plan Inspect a run
config Β· stack Β· agent Β· blocks Β· skills Β· hooks Configure (hooks list/trust/untrust approves .slmcode/hooks.json)
studio Web UI + SSE API
session Β· context Β· docs Sessions and markdown memory
memory Β· evolve Β· metrics Inspect what the harness has learned
update Β· version Β· completion Maintenance

--json on status, doctor, readiness, board, version, apply, compose, task show, blocks list, hooks list, auth list, and every config (except set) / memory / evolve / metrics subcommand. Colour is off outside a TTY. Exit codes: 0 ok Β· 1 failure Β· 2 usage or missing TTY Β· 3 no workspace Β· 4 provider unreachable Β· 5 failing tasks Β· 6 unanswerable gate Β· 130 interrupted (a genuine cancellation β€” a provider error that merely says "interrupted" does not get 130). β†’ docs/cli.md

TUI: /help, /compact, /models, /permission, /apply, /reject, /diff, /rewind, /sessions, /stats, /stop, /resume β€” / opens a fuzzy picker; ↑/↓ and Ctrl-R search history; Esc interrupts a run so you can redirect it.


🎯 Why this loop exists

🐘 Large-model habit 🐭 SLMCode approach
Stuff the repo into chat Token-budgeted packs + a ranked repo map
Hope the model emits valid JSON Negotiated constrained decoding, then a repair ladder
One free-form agent Plan β†’ atomic tasks β†’ 20 specialists
Trust "I fixed it" Disk is authoritative; hallucinated edits do not pass
Re-learn the same failure every run Fingerprint it once, store the repair, apply it for free
Opaque progress Append-only transcript + sticky footer, or Studio's SSE feed

πŸ“š Docs

unicolab.github.io/smlcode β€” MkDocs Material.

Section Pages
Getting started Install Β· Quick start Β· Concepts Β· Providers
Handbook Guide Β· TUI Β· Skills Β· Studio Β· Agents Β· Blocks Β· Customization Β· Pipeline Β· Recipes
Reference CLI Β· Config Β· Tools (ACI) Β· Constrained decoding Β· Context engineering Β· Permissions Β· Testing Β· Troubleshooting Β· FAQ
Internals Architecture Β· Conventions Β· Self-improvement & memory
Project Migration notes Β· Changelog Β· Contributing

Local preview: make docs-serve β†’ http://127.0.0.1:8000


πŸ§ͺ Develop

git clone https://github.com/UnicoLab/smlcode.git && cd smlcode
make bootstrap        # install web/ npm deps + build the Studio UI into cmd/slmcode/ui/
make check            # the one gate: fmt, vet, lint, tests, race, web lint+build β€” same as CI

Studio is a Vite + React + TypeScript SPA in web/, built to cmd/slmcode/ui/ and embedded with go:embed all:ui. Everything in cmd/slmcode/ui/ except .gitkeep is gitignored build output, so building the UI never dirties a tracked file; with none of it present the server serves a placeholder page compiled into pkg/server. For UI work: make bootstrap && cd web && npm run dev. See CONTRIBUTING.md β€” including why web/package-lock.json needs regenerating and committing.

import "github.com/UnicoLab/slmcode/pkg/harness"

h, _ := harness.New("/path/to/project")
_ = h.Init()
res, err := h.Run(ctx, "refactor pkg/auth")

Contributing guide, lint ratchet and package ownership: CONTRIBUTING.md. Agents working on this repo: AGENTS.md.


πŸ“œ License

MIT β€” use it, remix it, ship with it.


Made with β™₯ by UnicoLab
Coding with SLMs should feel like a superpower, not a compromise. β˜€οΈ

About

Small but powerful coding harness optimized for SLMs

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages