Apprentice: A Slim, Extensible Coding Harness (Written in Common Lisp) Developer skarnati20 released Apprentice, an open-source coding harness written in Common Lisp and built around four abstractions — Models, Tools, Anchors, and Loops — that let users customize agent loops, model providers, and repository pre-processing. The harness ships with commands including (available-models), (set-model "claude-sonnet-5"), (add-allowed-dir "~/projects/my-app"), (set-loop :apprentice), and (add-anchor "file-tree"), and supports Anthropic, OpenAI, Gemini, OpenRouter, and local llama.cpp models via environment variables. Apprentice's author argues Lisp macros, REPL-driven development, and an image-based runtime make harness abstractions native to the language and changes instantly visible. A Common Lisp coding harness designed around extreme configurability. Designed to be hacked, modified, and expanded. Most harnesses today aren't that configurable. They determine what agent loops can run, what models are available, the tools agents can use, and what context they receive. The goal of this project is to define abstractions which make harness customization easy, while still giving users ultimate freedom. Why Common Lisp? Because Lisp macros allow us to make harness abstractions native to the language. Lisp's REPL-driven development perfectly fits a chat interface. And finally, Lisp's image-based runtime makes it easy to modify the harness and see changes instantly. Apprentice is based on four key harness abstractions: 1. Models - The LLM provider and source of intelligence 2. Tools - The capabilities provided to the model 3. Anchors - Any pre-processing happening on a repository or directory 4. Loops - The orchestrating code handling model output and tool requests See the Extending The Harness extending-the-harness section on how to define these abstractions on your own. First, clone the repository and load and enter the apprentice package: git clone https://github.com/skarnati20/apprentice.git ~/quicklisp/local-projects/apprentice ql:quickload :apprentice in-package :apprentice Look at the available models and choose one: js available-models ; = "llama-cpp" "claude-sonnet-5" "gpt-5.6-terra" ... set-model "claude-sonnet-5" Make sure you have the proper environment variables set: export ANTHROPIC API KEY=... export OPENAI API KEY=... export GEMINI API KEY=... export OPENROUTER API KEY=... Or setf uiop:getenv "ANTHROPIC API KEY" "..." setf uiop:getenv "OPENAI API KEY" "..." setf uiop:getenv "GEMINI API KEY" "..." setf uiop:getenv "OPENROUTER API KEY" "..." Or llama-server -m /path/to/model.gguf --port 8080 Then add an allowed directory: add-allowed-dir "~/projects/my-app" And now, chat chat "What does the main function in src/main.rs do?" | Command | Description | Output | |---|---|---| | available-models | Returns all available models | "llama-cpp" "claude-sonnet-5" "gpt-5.6-terra" ... | | set-model "claude-sonnet-5" | Sets the active model | "claude-sonnet-5" | | model | Returns the active model name | "claude-sonnet-5" | | add-allowed-dir "~/projects/my-app" | Adds a directory to the allowed directories list | "/Users/you/projects/my-app/" | | allowed-dirs | Returns all allowed directories | "/Users/you/projects/my-app/" | | clear-allowed-dirs | Clears all allowed directories | NIL | | available-loops | Returns all available coding loops | :STANDARD :LITTLE-CODER :APPRENTICE | | set-loop :apprentice | Sets the active coding loop | :APPRENTICE | | current-loop | Returns the active coding loop | :APPRENTICE | | available-anchors | Returns all available anchors | "dense-vector-search" "file-tree" | | add-anchor "file-tree" | Enables an anchor by name | "file-tree" | | set-anchor-dir "~/projects/my-app" | Sets anchor directory and processes/indexes files | "/Users/you/projects/my-app/" | | anchors | Returns currently enabled anchors | "dense-vector-search" "file-tree" | | clear-anchors | Clears all enabled anchors | NIL | | save-anchors | Saves currently loaded anchor state to disk | "/Users/you/projects/my-app/.apprentice" | | add-option :temperature 0.2 | Adds or sets an option for the coding loop | :TEMPERATURE . 0.2 | | options | Returns currently set options | :TEMPERATURE . 0.2 | | clear-options | Clears all currently set options | NIL | | chat "Summarise this repository" :standard :max-tokens 2000 | Runs a conversation turn using the specified loop and options | Model response text | | show-turns | Prints conversation history turns | Formatted turn output | | show-turns 3 | Prints turn at specific index | Formatted turn output | | show-turns -1 | Prints the most recent turn | Formatted turn output | | show-turns 2 :limit 500 | Prints turn with custom preview character limit | Formatted turn output | | set-preview-limit 300 | Sets the preview character limit for show-turns | 300 | | drop-turns 3 | Drops turn at specified index from history | Updated chat history | | drop-turns ' 2 . 4 | Drops inclusive range of turns from history | Updated chat history | | clear | Clears conversation history | NIL | Create a new model with defmodel : defmodel openrouter :endpoint "https://openrouter.ai/api/v1/chat/completions" :headers "Content-Type" "application/json" "Authorization" format nil "Bearer ~a" uiop:getenv "OPENROUTER API KEY" "HTTP-Referer" uiop:getenv "OPENROUTER REFERER" "X-Title" uiop:getenv "OPENROUTER TITLE" :params model-id "model" :default "anthropic/claude-sonnet-5" max-tokens :default 4096 temperature :default 0.2 top-p stop stream :default nil :as if value t :false :format-message openai-format-message msg :format-tool tool- openai tool :parse openai-parse raw | Argument | Description | |---|---| | name | Symbol naming the model. Binds to