GPL-3.0 · Rust · on crates.io

dirge — The Batteries-Included
Rust Coding Agent

Everything on by default — MCP, LSP, ACP, a Janet plugin system, tree-sitter code intelligence, and self-improving memory. Yet it's native Rust: ~8 MB RAM idle, ~36 MB binary, no runtime. And an agent loop built to keep even small, cheap models on the rails.

8MBRAM idle
36MBBinary size
25+Built-in tools
8Providers
dirge — ~/src/voxel
CONTEXT

ctx [██░░░░░░░░] 1%

11.5k / 1.0M · cmp:0

ACTIVITY

edit voxel.cljs

read voxel.cljs

skill paren-check

bash cd …/voxel

AGENT LOG STREAM

<you> add FPS-style mouse-look + wheel height to voxel.cljs

<dirge> Here's the plan:

1. add :mouseX/:mouseY delta fields to input

2. mousemove + wheel listeners (pointer lock)

3. consume deltas in update-camera, reset per frame

BASH · clojure -M:dev -e "(require 'voxel)"
✓ compiled — 0 warnings
EDIT — voxel.cljs (+2 lines, validated)
- :lookUp false})
+ :lookUp false
+ :mouseX 0.0
+ :mouseY 0.0})

<critic> deltas reset each frame ✓ — looks complete

SYSTEM LOAD

CPU [██░░░░░░] 24%

MEM [████░░░░] 55%

MCP

chiasmus

rlm

wavescope

lattice

LSP

clojure-lsp

TODOS

input delta fields

pointer-lock listeners

minimap overlay

MODIFIED

voxel.cljs

[$_$]
░▌ add a minimap overlay to the HUD
voxel:main· deepseek-v4-pro· 11k/1.0M (1%)· 3 msgs· code· running
[*] ON THE NAME

A dirge is a song to keep the dead from losing their way. It turns grief into something that is remembered. Agents are like mayflies awoken for a moment to work and to forget, with every new session effacing the old one. Dirge keeps watch over things said and done, always folding context into memory to carry past mistakes and preferences across the gulf between sessions. It sings the past forward, so that no grave need be dug twice. Dirge grieves for nothing, since nothing is truly buried under its care, and its lament is a promise that what was built here once will be remembered.

[*] WHY DIRGE

Small footprint, serious reliability

Native Rust, no runtime — and an agent loop engineered so cheaper models stay productive instead of derailing.

Terminal-First TUI

ratatui + crossterm. Live token streaming, a branching session tree, and configurable info panes via /display — all keyboard-driven.

Robust Agent Loop

Repairs malformed tool calls, validates every write through tree-sitter before it hits disk, trips circuit breakers on non-progressing loops, and escalates to a stronger model on repeated failure.

One Permission Engine

A single Policy Decision Point with four modes, op-based rules, and session allowlists. The /why command traces exactly which policy decided — and why.

Role-Based Routing

Point the main loop, review, escalation, summarization, and subagent roles at different models. Mix DeepSeek, GLM, Anthropic, OpenAI, and Ollama in one session.

Self-Improving Memory

Persistent per-project memory plus a post-session orchestrator that extracts learnings, curates memory & skills, and promotes patterns recurring across sessions.

Code Intelligence

Tree-sitter semantic tools and inline LSP diagnostics for 10+ languages — surfaced in tool output so the agent fixes compile errors on the same turn.

Extensible at Runtime

A Janet plugin system hooks the full lifecycle — intercept tools, rewrite prompts, register commands — plus Claude-compatible skills loaded on demand.

MCP & ACP

Model Context Protocol for extra tools and servers. Agent Communication Protocol for editor integration with Zed.

Sandbox Mode

Run every bash command inside an isolated bubblewrap environment with --sandbox — defense in depth on top of the permission engine.

Phased Planning

An opt-in /plan workflow runs explore → plan → implement → review as context-isolated phases: a read-only agent maps the code, a second drafts the plan, then a write-disabled reviewer runs the code and feeds gaps back for a bounded retry.

Token-Efficient I/O

Tree-sitter read_minified/edit_minified collapse files to their skeleton; a hard read-before-edit gate blocks blind edits; and oversized bash/webfetch output is relayed to disk with a head+tail summary so the context window stays lean.

Step-Through Debugging

A built-in Debug Adapter Protocol client drives real debuggers — set breakpoints, step, inspect stacks and variables — straight from the agent loop and Janet plugins (debug tool, dap/* bindings).

[*] GET STARTED

Install from crates.io in one line

The crate is published as dirge-agent (the short name was taken); the installed command is still dirge. Set an API key and start coding.

▸ install
# Batteries included — MCP, LSP, ACP, plugins, and every
# tree-sitter language are on by default.
$ cargo install dirge-agent

# Leaner build — opt out of defaults, pick what you need:
$ cargo install dirge-agent --no-default-features \
    --features "loop,git-worktree,mcp,lsp,semantic-rust"
[*] PROVIDERS

Bring your own model

Works with all major providers, and any OpenAI-compatible endpoint. OpenRouter is the default — no setup for most. Switch with /model.

OpenAI

GPT-4o · o-series

Anthropic

Claude Sonnet · Opus

DeepSeek

V4 Pro · R1 · V3

Gemini

2.5 Pro · 2.0 Flash

Ollama

Llama · Qwen · Mistral

OpenRouter

Default · 200+ models

GLM

GLM-4 · ZhipuAI

Custom

Any OpenAI-compatible endpoint

[*] TOOLS

25+ built-in tools

Everything the agent needs — files, shell, search, code intelligence, and delegation — with zero configuration.

Files & edits

read · write · edit · apply_patch · list_dir · find_files

Shell

bash · bash_output · kill_shell — foreground & background

Search

grep · glob · session_search

Web

webfetch · websearch

Code intelligence

list_symbols · get_symbol_body · find_callers · find_callees · find_definition · lsp · repo_overview

Agent & memory

task · plan · write_todo_list · memory · skill · question

[*] COMMANDS

Drive it with slash commands

Control the agent from the TUI — switch models, manage sessions, inspect decisions. Type /help for the full list.

/model

Show or switch LLM model

/prompt

List or activate system prompts

/mode

Set security permission mode

/why

Trace the last permission decision

/display

Configure visible info panes

/compress

Compact history for context

/sessions

List, save, load sessions

/tree

Show session branch tree

/fork

Branch the conversation

/loop

Start an iterative coding loop

/mcp

List MCP servers and tools

/worktree

Create a git worktree

/allow

Manage the session allowlist

/reasoning

Toggle reasoning visibility

/btw

Quick question (no tools)

/help

Show all commands

[*] PLUGINS

Plugin system in Janet

dirge embeds Janet as a plugin language. Plugins are small scripts that hook into the agent loop, intercept tools, rewrite prompts, register slash commands, gate execution, and drive session navigation — all from a few lines of Lisp.

Your first plugin

Plugins live in ~/.config/dirge/plugins/ (global) or ./.dirge/plugins/ (project-local). A plugin is a single .janet file — no boilerplate, no exports required.

~/.config/dirge/plugins/hello.janet
;; Every time the user sends a prompt, print a line
(defn on-prompt [ctx]
  (harness/notify (string "user said: " (ctx :prompt)) :info))

That's it. Restart dirge, type a message, and the notification appears in chat. The plugin system is on by default.

Multi-file plugins: create a directory of .janet files that share one Janet environment and load in alphabetical order. Name files 00-state.janet, 01-hooks.janet to control load order.

Hook reference

Every hook takes a ctx table and returns nil or a string. Define them as top-level functions in your plugin file.

on-init

Once at session start. ctx: model, cwd, provider.

on-prompt

After user submits, before LLM call. Use harness/replace-prompt to rewrite.

on-turn-start

Start of one LLM call cycle. ctx: index.

on-message-update

Every ~16 streamed tokens. ctx: index, partial text.

on-turn-end

After tool results return. ctx: index, full message text.

on-response

After a full LLM response. Use for logging/notifications.

on-tool-start

Before a tool runs (after permission check). ctx: tool name, args. Use harness/block or harness/mutate-input.

on-tool-end

After tool returns. ctx: tool name, output. Use harness/replace-result.

on-error

A tool or LLM call errored. ctx: error message.

on-complete

Agent finished its multi-turn response.

Tool interception

Use on-tool-start and on-tool-end to gate, rewrite, or filter tool calls.

Block dangerous bash commands
(defn on-tool-start [ctx]
  (when (= (ctx :tool) "bash")
    (let [cmd (get-in ctx [:args "command"])]
      (when (string/find "rm -rf" cmd)
        (harness/block "denied: dangerous deletion")))))

Three APIs: harness/block — stop the tool (first-wins across plugins), harness/mutate-input — rewrite tool args (last-write-wins), harness/replace-result — replace tool output.

Register custom tools

Plugins can register tools the LLM calls directly — not just intercept built-in ones.

Custom tool registration
(defn echo-handler [args]
  (string "echo received: " args))

(harness/register-tool
  "plugin_echo"                  ;; name (LLM sees this)
  "Echoes args back verbatim"   ;; description
  "Echo"                        ;; UI label
  "{\"type\":\"object\",\"properties\":{\"msg\":{\"type\":\"string\"}},\"required\":[\"msg\"]}"
  "echo-handler"                 ;; handler fn name
  :parallel)                     ;; :parallel or :sequential

Execution mode: :parallel (read-only, default) or :sequential (mutating). One sequential tool forces the whole batch sequential. Optionally pass a prepare-arguments function (7th arg) to normalize LLM-supplied args before validation.

Slash commands & keyboard shortcuts

Register a slash command
(defn echo-cmd [args]
  (string "you said: " args))

(harness/register-command "echo" "echo-cmd")

Now /echo hello world prints you said: hello world in chat.

Register a keyboard shortcut
(defn refresh [key]
  (string "F5 pressed: " key))

(harness/register-shortcut "f5" "refresh" "Refresh chat")
(harness/register-shortcut "ctrl-s" "save-all" "Save")

Key spec format: (modifier "-")* key-name. Modifiers: ctrl, alt, meta, shift. Reserved keys (Ctrl+C, Esc, etc.) cannot be overridden.

Custom LLM providers

Register any OpenAI-compatible endpoint as a first-class provider, then switch with /model.

Register a local provider from a plugin
(harness/register-provider
  "local-openai"               ;; name surfaced in /model
  "openai"                     ;; provider type
  "http://localhost:8000/v1"   ;; base URL
  "LOCAL_OPENAI_API_KEY")       ;; env var for the key

Use /model local-openai/gpt-4 to switch. Config-declared providers in config.json win on name collision.

User dialogs

Block the plugin thread to ask the user for confirmation or a choice. Safe to call from any hook — the UI renders while the worker waits.

Confirm and select dialogs
;; Returns true on confirm, false on Esc/Cancel
(if (harness/confirm "Confirm" "Run migration?")
  (harness/notify "running..." :info)
  (harness/block "user said no"))

;; Returns selected string, or nil on cancel
(let [choice (harness/select "Pick model"
                   ["gpt-4" "claude-4" "deepseek"])]
  (when choice
    (harness/notify (string "using: " choice) :info)))

Dialogs use the UI's selection picker. Arrow keys navigate, Esc cancels. These are the only synchronous round-trip APIs.

Renderers & entries

Persist typed entries in the session timeline with custom formatting. Entries survive save/load. Use message renderers for live mid-conversation output.

Session entry renderer
;; Define a renderer for "bookmark" entries
(defn render-bookmark [data]
  (harness/render :cyan (string "★ " data)))

(harness/register-renderer "bookmark" "render-bookmark")

;; Append an entry from any hook
(harness/append-entry "bookmark" "milestone-1")
Message renderer (live mid-conversation)
(defn render-status [payload]
  (string "■ " payload))

(harness/register-message-renderer "status" "render-status")

;; From a prepare-next-run hook:
(harness/add-custom-message "status" "turn complete")
[*] THEMES

Custom themes

Switch between built-in themes or create your own with a simple JSON file. Override any color — the rest falls back to defaults.

Built-in themes

phosphor

80s CRT green on black. Errors red, warnings yellow. The default.

plain

White assistant text, cyan accents, gray dim. Clean and modern.

~/.config/dirge/config.json
{
  "theme": "plain"
}

Custom themes

Create ~/.config/dirge/midnight.theme.json and set "theme": "midnight" in config. All fields optional — unspecified values keep the phosphor default.

~/.config/dirge/midnight.theme.json
{
  "agent": "#88ccff",
  "user":  "#ffaa66",
  "system": "darkgray",
  "tool":   "darkcyan",
  "perm":   "yellow",
  "result": "darkgray",
  "error":  "red",
  "warn":   "yellow",
  "accent": "#ff66cc",
  "dim":    "darkgray",
  "header": "cyan",
  "divider": "darkgray",
  "banner_primary":   "#aa88ff",
  "banner_secondary": "darkmagenta",
  "label": "MIDNIGHT"
}

Colors accept: named colors (red, darkcyan), hex RGB (#88ccff), or 256-color palette index (208). Malformed files fall back to phosphor with a warning.

Color field reference

agent

Assistant chat text

user

User message prefix

system

System messages

tool

Tool chamber headers

perm

Permission prompts

result

Secondary result text

error

Hard errors

warn

Warnings

accent

Headers, focused picker rows, banner accents

dim

Placeholders, separators

header

Right-panel headers

divider

Horizontal divider line

banner_primary

Welcome banner primary stroke

banner_secondary

Welcome banner border / decorations

label

Name shown in banner

[*] CONFIGURATION

Declarative config

Configure providers, role routing, permissions, themes, MCP servers, and custom prompts in ~/.config/dirge/config.json.

~/.config/dirge/config.json
{
  "provider":    "openrouter",
  "model":       "deepseek/deepseek-v4-flash",
  "theme":       "phosphor",
  "permissions": {
    "mode":  "standard",
    "rules": [
      { "pattern": "git checkout *", "action": "allow" },
      { "pattern": "rm -rf **",     "action": "deny"  }
    ]
  },
  "roles": {
    "escalation": "anthropic",
    "review":     "glm"
  },
  "mcp_servers": {
    "lattice": {
      "command": "lattice-mcp",
      "args": []
    },
    "chiasmus": {
      "command": "npx",
      "args": ["-y", "chiasmus"]
    },
    "wavescope": {
      "command": "wavescope-mcp",
      "args": []
    }
  }
}

Four permission modes: standard (safe ops auto-approved), restrictive (every tool prompts), accept-all (auto-approve inside cwd), yolo (auto-approve everything). Pattern rules support glob matching, and the /why command explains any decision.

[*] FAQ

Frequently asked

Common questions about dirge.

Q What makes dirge different from other coding agents?
dirge is written in Rust — ~36MB binary, ~8MB RAM at idle, no runtime (vs ~300MB for Node.js agents). Beyond efficiency, it's built to keep cheaper models productive: a robust agent loop repairs malformed tool calls, validates every write through tree-sitter before disk, and escalates to a stronger model on repeated failure. Add a single explainable permission engine, role-based multi-provider routing, self-improving project memory, and a Janet plugin system.
Q Does dirge support local models?
Yes. Ollama is a first-class provider, and you can point at any OpenAI-compatible local server (llama.cpp, vLLM, etc.) via config or by registering one from a plugin. With role-based routing you can even run a small local model for the main loop and escalate to a hosted model only when needed.
Q What languages does code intelligence support?
Tree-sitter grammars for TypeScript/TSX, Python, Clojure, Go, Ruby, Rust, Java, C, C++, and Elixir — 10 languages by default. Each provides list_symbols, get_symbol_body, find_callers, find_callees, and find_definition, with LSP diagnostics surfaced inline.
Q How does the memory system work?
dirge keeps persistent per-project memory and pitfalls files. After a session, a post-session orchestrator extracts learnings, curates memory and skills, and promotes patterns that recur across sessions — so the agent carries preferences and past mistakes forward instead of starting cold every time.
Q What are the permission modes?
Four modes: standard — safe ops auto-approved, writes/bash/MCP prompt; restrictive — every tool prompts; accept-all — auto-approve inside cwd; yolo — auto-approve everything. All authorization flows through one Policy Decision Point, and /why traces exactly which rule decided. Add --sandbox to isolate bash in bubblewrap.