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, breaks repeat-loops, and — when a model keeps failing — injects a recovery checkpoint, suggests the tool/path it likely meant, then escalates to a stronger model.

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

Two-tier per-project memory — hot facts inline, the rest a searchable breadcrumb index — plus a global cross-project tier for durable preferences, injected as a cache-stable snapshot. Reusable skills share the same salience engine: /learn distills one from any source or the current session, and the curator prunes on proven usefulness — decay plus a success/failure record — not just age.

Built-In Issue Board

A persistent, agent-facing kanban in the session DB — a stateful extension of the memory model. The harness surfaces the top open issues at the start of every turn, so the agent works its backlog without polling a tracker. The issue tool manages it (create / start / close, priorities, cross-session); /issues views it. SQLite rows, no external tracker.

Long-Horizon Sessions

Durable, incrementally-refreshed checkpoints anchored to a stable identity — so resuming a long, compacted session recovers its live state instead of a stale snapshot. Hold autonomous runs to a natural-language stop condition with --goal. Adapted from MiMo-Code.

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 and grown from any source with /learn.

MCP & ACP

Connect MCP servers for extra tools — or run dirge mcp to be one, so another agent (e.g. Claude Code) delegates implementation tasks to dirge and reviews them. ACP for editor integration with Zed.

Sandbox Mode

Run every bash command in isolation with --sandbox: bubblewrap for a namespaced jail, or a hardware-isolated libkrun microVM — 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.

Diff-Aware Code Review

As agents write code, a reviewer reads the actual diff — not just the transcript — in two passes (review, then verify to drop false positives). High/critical findings block the loop until fixed or justified; medium/low surface as advisories. Run it any time with /code-review. Reuses the critic's judge, so it's one opt-in with no cost when off.

Spec-Driven Workflow

Align on what before how. The spec tool tracks changes — proposal, requirement deltas, and a task checklist with real status — as SQLite rows, not markdown; living specs are the current truth, and archiving folds a change's deltas into them transactionally. The active change is injected into context; archiving forms a memory. Inspired by OpenSpec.

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

Vision & Image Paste

Paste a clipboard image into the prompt with Ctrl+V and send it to a vision model — turns are multipart, interleaving text and pictures. Images live out-of-line in the session's assets/ dir and are re-read at the provider boundary each turn, so transcripts stay small. Gated by provider/model vision support, with a multimodal override for local models like Ollama llama3.2-vision.

[*] 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 · task_status · write_todo_list · memory · skill · spec · question

Token-efficient I/O

read_minified · edit_minified — collapse a file to its tree-sitter skeleton

Debugging

debug — DAP step-through: breakpoints, step, inspect stacks & variables

Dynamic discovery

tool_search — load tools on demand to keep the prompt lean

[*] 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

/agent

Switch the active agent profile

/prompt

List or activate system prompts

/plan

Run the phased plan workflow

/code-review

Review the working-tree diff for issues

/mode

Set security permission mode

/why

Trace the last permission decision

/allow

Manage the session allowlist

/sandbox

Inspect & control bash sandboxing

/display

Pick visible info panes

/panel

Toggle the side gutters

/compress

Compact history for context

/sessions

List, save, load sessions

/issues

View the native issue board

/tree

Show session branch tree

/fork

Branch the conversation

/loop

Start an iterative coding loop

/tasks

List background shells & agents

/spec

Inspect spec changes & living specs

/mcp

List MCP servers and tools

/worktree

Create a git worktree

/plugins

List loaded Janet plugins

/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?
The code-navigation tools — list_symbols, get_symbol_body, find_callers, find_callees, find_definition — cover TypeScript/TSX, Python, Clojure, Go, Ruby, Rust, Java, C, and C++. Tree-sitter pre-write validation and the minified read/edit tools additionally cover Elixir and Bash, and LSP diagnostics are surfaced inline. Each grammar is gated on its own semantic-<lang> feature, all on by default.
Q How does the memory system work?
dirge keeps persistent per-project memory (facts and pitfalls) in the session DB, injected into the system prompt as a cache-stable snapshot. It's two-tiered: hot entries inline verbatim, and once the budget fills the rest demote to a searchable breadcrumb index the agent expands on demand. A separate global, cross-project tier holds durable user preferences that follow you across every repo. After an idle session a post-session orchestrator extracts learnings and curates memory and skills (stale-detection, lifecycle, LLM consolidation) — so the agent carries preferences and past mistakes forward instead of starting cold every time.
Q What happens to long sessions when I resume them?
Every conversation carries a stable identity and a durable checkpoint that's refreshed in the background as context grows (a 20%-of-window cadence), without discarding history. A long session compacts and rotates internally over time, but resuming by the id you started with resolves to its live tip and recovers fresh state — it picks up where it left off instead of loading a stale pre-compaction snapshot, and the verbatim original request is re-anchored so the goal doesn't drift. Autonomous runs can be held to a natural-language stop condition with --goal "…", checked by an independent judge each time the agent tries to stop. This long-horizon design is adapted from MiMo-Code.
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.