Skip to content

Latest commit

 

History

History
1250 lines (1049 loc) · 80 KB

File metadata and controls

1250 lines (1049 loc) · 80 KB

Configuration

dirge reads an optional JSON config file named config.json from its config folder:

  • If DIRGE_CONFIG_DIR is set: $DIRGE_CONFIG_DIR/config.json
  • Otherwise: the platform config directory joined with dirge/config.json (for example $XDG_CONFIG_HOME/dirge/config.json on Linux)
  • Fallback: $HOME/.config/dirge/config.json

A project may also ship a partial <project>/.dirge/config.json. It is deep-merged on top of the global file: scalar fields override, while maps (providers, mcp_servers, agents, slash_aliases) union key-by-key, so a project can add or override a single entry without redeclaring the whole map. Arrays are replaced wholesale, not merged — notably keybindings (a list): a project keybindings entry wipes the global list rather than extending it, so redeclare the full set if you also want the global bindings. Absent keys fall through to the global file. An empty object (e.g. "providers": {}) is a no-op, not a wipe — there is no syntax to clear a global map from a project config. CLI flags and env vars still take precedence over both files.

All config keys are optional. CLI flags and their environment-backed values (such as DIRGE_PROVIDER and DIRGE_MODEL) take precedence where both exist.

Example:

{
  "provider": "openrouter",
  "max_tokens": 8192,
  "temperature": 0.7,
  "context_window": 128000,
  "reserve_tokens": 16384,
  "keep_recent_tokens": 20000,
  "compact_enabled": true,
  "default_prompt": "code",
  "default_permission_mode": "standard",
  "show_tool_details": true,
  "show_edit_diff": true,
  "animations_enabled": true,
  "show_reasoning": false,
  "display": "left|main|right",
  "tool_result_max_chars": 500,
  "tool_result_max_lines": 4,
  "providers": {
    "openrouter": {
      "model": "deepseek/deepseek-v4-flash"
    },
    "local-vllm": {
      "provider_type": "openai",
      "base_url": "http://localhost:8000/v1",
      "api_key_env": "VLLM_API_KEY"
    }
  },
  "permission": {
    "*": "ask",
    "rules": [
      { "op": "edit",    "match": "**/*.rs",   "effect": "allow" },
      { "op": "edit",    "match": "**",        "effect": "ask"   },
      { "op": "execute", "match": "cargo test", "effect": "allow" },
      { "op": "execute", "match": "rm **",     "effect": "deny"  }
    ],
    "external_directory": [
      { "match": "/tmp/**", "effect": "allow" },
      { "match": "/**",     "effect": "ask"   }
    ],
    "doom_loop": "ask"
  }
}

Accepted top-level keys:

Key Type Description
provider string Active provider alias. Built-ins are openrouter, openai, anthropic, gemini/google, deepseek, glm/zhipu, cerebras, opencode, kimi/kimi-code/moonshot, and ollama; any alias declared in providers is also accepted. Default: openrouter. See Providers and roles.
auth string Default authentication source for providers that don't set their own providers.<name>.auth: api-key (the implicit default), chatgpt (Codex/OpenAI login tokens), anthropic / claude-code (Anthropic Claude Code OAuth), or kimi (Kimi Code device OAuth). See Providers and roles.
providers object Map of provider alias → entry. The active model lives in providers.<active-provider>.model. Each role key below points at one of these aliases. See Providers and roles.
review_provider string Provider alias for the background session-review pass. Falls back to provider.
escalation_provider string Provider alias for the one-shot retry after repair-exhaustion / pre-write syntax failure. Falls back to provider (no-op when equal).
summarization_provider string Provider alias for context compaction. Falls back to provider; with Anthropic OAuth, configure a non-Anthropic-OAuth summarization provider for LLM compaction side calls. Reactive overflow can still use a local prune-only emergency fallback, but high-fidelity LLM summaries require this route.
subagent_provider string Provider alias for task tool subagents. Falls back to provider.
subagent_dispatch_strategy string Coordinated background dispatch mode: off (default), optional, or full. Coordination requires eligible readonly and readwrite agent profiles and runs only in the interactive TUI. See Coordinated Subagents.
subagent_write_isolation string Where coordinated read-write subagents run: auto (default), worktree, or serialize. Worktree isolation requires a confining Linux sandbox; auto otherwise falls back to one serialized writer in a clean parent checkout. See Coordinated Subagents.
critic_provider string Provider alias for the F6 in-loop critic (tier 3). When set, the verifier escalates to a bounded LLM critique at finalization on substantive runs (one call per run); it also judges the goal gate (--goal) and powers the diff-aware code reviewer (reviews the run's uncommitted diff, blocks on high/critical findings, advises on medium/low — also runnable on demand via /code-review). No fallback — unset means no critic, no reviewer, no goal gate, and no cost.
critic_preamble string Optional system-preamble override for the F6 in-loop critic. Replaces the built-in critic stance for every prompt. A prompt's critic_preamble frontmatter overrides this per-prompt; a critic: false frontmatter suppresses the critic entirely for that prompt. The goal gate is unaffected by either — it always judges under its own fixed preamble. Unset = built-in.
code_review string How the diff-aware code reviewer engages at finalization: off | advisory | blocking. advisory (default) runs the review in the background after the turn finalizes and surfaces all findings (high/critical included) as one non-blocking <system> notice — it never holds up the turn and never re-enters the loop, so a tight debug loop isn't interrupted. blocking runs it synchronously on the finalization path: high/critical findings re-enter the loop (fix or justify), medium/low advise. off disarms it entirely (no diff capture, no judge call, zero cost). Still requires critic_provider (the reviewer reuses its judge); a critic: false prompt also suppresses it. A prompt's code_review frontmatter overrides this per-prompt.
verification_tiers string Splits build/test commands into a fast tier (typecheck, lint, one targeted test) and a slow tier (full suite/build): off | advisory | blocking. off (default) is byte-identical to the untiered gate. advisory adds one mid-run nudge to run a fast check once three code edits pile up unverified, plus one finalization escalation when only the fast tier ever passed. blocking repeats that escalation (bounded). Unknown commands tier as slow, so an unrecognized one errs toward silence. See failure-ladder.md.
progress_stall_threshold integer Turn boundaries with no progress (no todo closed, no new file touched, no green check) before the loop asks the model what's blocking and whether to cut scope. Absent (default) disables the monitor entirely. Clamped to a minimum of 2. Also enables turn-budget notices at 60%/85% of max_agent_turns — those need a cap set to mean anything. Arms only after the first progress event, so an exploration phase is never nudged. See failure-ladder.md.
progress_prologue_cap integer Barren turn boundaries (or four times that many barren tool calls) before a run that has produced nothing at all gets one checkpoint pushing for the smallest possible first write. Default 24. This is an UPPER BOUND, a safety net against runaway reconnaissance — not an eager nudge — so never firing on normal work is the correct outcome. Only meaningful alongside progress_stall_threshold. See verification-discipline.md.
verification_command string The command whose pass is the only honest green — typically what CI enforces, e.g. cargo clippy --all-targets -- -D warnings. When set and that command hasn't passed, a result that would read VerifiedGreen becomes FastGreenOnly instead, so the full-suite escalation carries it. Matched by (program, subcommand) signature, so flag and env-prefix differences don't matter. Absent (default) leaves behaviour unchanged, including in off mode. See verification-discipline.md.
safe_state_abort string Third rung of the failure ladder — when a failure streak reaches twice the recovery-checkpoint threshold with unverified edits on the tree and a green point behind it, abort the approach and re-plan: off | advisory | auto. off (default). advisory injects the re-plan message and writes no files. auto restores the tree itself, but only after proving against git that the snapshot store covers every file changed since the green point — one uncaptured file (a sed -i, a formatter, any bash write) and it declines to advisory rather than leaving a half-reverted tree. See failure-ladder.md.
publish_guard string Once verification goes green, intercept commands that would DISCARD that work — rm of a file in the verified diff, git reset --hard, git checkout -- <path>, git clean -f, git stash, truncating redirects: off | advisory | blocking. off (default). advisory injects a model-visible warning naming the protected paths, at most twice per run. blocking suppresses the call before dispatch and returns an error naming what it would have destroyed. Ordinary editing of a protected file is never blocked — sessions keep working after green, and blocking rewrites would nag on every edit-test cycle. No override token: advisory and off are the escape hatches. See verification-discipline.md.
claim_gate string Fire when the final answer makes a claim the run's evidence does not support — a test count or named-gate result (4954 passed, clippy clean) while the verifier saw no build/test command, or an applied/fixed/changed claim while zero files moved: off | advisory | blocking. advisory (default) injects one model-visible nudge per run asking it to correct the claim or run the check. blocking re-enters up to three times. off disables it (byte-identical to the loop without the gate). Deterministic — no LLM, so it can't be talked out of it or invent an accusation. Quoted output and claims attributed elsewhere (CI reported ...) never fire. See verification-discipline.md.
source_gate string Fire when a comment ADDED to a source file this run asserts having consulted an external source (checked the X page, per the Y docs) while no webfetch/websearch tool ran: off | advisory | blocking. off (default) — opt-in, unlike claim_gate, because it inspects the diff rather than the final answer and has a larger false-positive surface. advisory injects one nudge per run; blocking re-enters up to three times. Deterministic — no LLM. Only lines added by THIS run count (a run-start baseline diff is subtracted), and RFC citations, bug IDs, spec sections, repo file references, and URLs the code fetches at runtime are excluded. Catches a fabricated citation written into the artifact, which a final-answer scanner structurally cannot see. See verification-discipline.md.
completeness_gate string Fire when the final answer states first-person work the model still intended to do (Next I'll wire up the tests, I still need to implement the retry path) while the run is finishing: off | advisory | blocking. advisory (default) injects one model-visible nudge per run asking it to do the work or record what is left and say it is stopping; blocking re-enters up to three times; off is byte-identical to the loop without the gate. Deterministic — no LLM. Armed by default because without a critic_provider nothing else asks whether the task is finished: every other always-on gate wants a specific mechanical fault (unrun edits, a failed last call, an unsupported claim, tracked todos), so a run that edits real files, runs a real check, claims nothing false and stops halfway hits none of them. Narrow by construction — the sentence needs a first-person forward marker AND a work verb AND no second-person address, so handoffs (you'll want to run the migration), self-fulfilled intent (I'll explain below) and quoted plans never fire. Yields to the todo gate when tracked todos are outstanding. See verification-discipline.md.
open_issues_gate string Fire at finalization when this session left tracked issues open: off (default) | advisory | blocking. Opt-in, unlike the other gates, because nagging about a backlog is intrusive. advisory emits one SystemNotice; blocking re-enters the loop (bounded) so the run cannot finish until the session's issues are closed or deferred. Session-scoped — issues belonging to other sessions never surface here.
injection_scan string How the ingestion-time prompt-injection scanner treats untrusted tool results (read, MCP, websearch): off | advisory (default) | block. advisory fences a positive hit with a warning; block additionally withholds the body when two or more high-severity findings are present.
turn_envelope boolean Re-evaluate the volatile session facts (cwd, OS, shell, git branch) every turn and append them as a <turn_envelope> block, instead of freezing them into the system prompt at agent construction. Default: true. Those four facts were captured once and never refreshed, so a cd, a git switch, or a worktree move left the model reading a world that no longer existed, and the only correction was rebuilding the whole cached prefix to update four lines. Appending at the tail costs no cache churn. Measured on two models at n=6: no regression on any metric, and it removed the blow-up runs on the model that was struggling. Set false to restore the frozen preamble.
capability_projection boolean Render the prompt's tool list from the tools actually registered for the turn, minus what the active prompt's deny_tools removes, instead of a hand-written Available tools: literal. Default: true. The static list cannot see deny_tools, so plan and review mode advertised write/edit/apply_patch/bash while refusing all four — a weaker model plans against the prompt, hits a refusal, and burns turns recovering. Set false to restore the literal list.
prompt_leak_detect string Detect a model that stops answering and starts reciting its own system prompt: off | advisory | blocking. off (default) does no detection and emits nothing. advisory records it and warns, leaving the turn untouched. blocking also stops consuming the stream, keeping the answer given before the recitation and discarding the rest. Method: SimHash-64 over sliding 16-content-word windows of the output against the same windows of the prompt, tripping only on 12 consecutive near-matches — roughly 55 words reproduced verbatim. The margin is smaller than it sounds: two ordinary sentences quoted word-for-word reach a run of 6, so do not lower the threshold without re-running the fixtures in agent_loop::prompt_leak. Off by default because the failure has not yet been observed in dirge; advisory exists so it can be measured before it is trusted.
context_target integer Working-context budget in tokens (default: 250000). The compaction decision treats the effective window as min(model_window, context_target), so models with a window below 250k use their own (e.g. 128k stays 128k) while larger models are capped. Set lower (e.g. 100000) for stricter folding on cost-sensitive routes. Floored at 16k.
compaction_fold_threshold float Fraction of the (budgeted) context window (0.3–0.75) at which history folds into a summary — and the durable checkpoint is written. Lower folds/checkpoints earlier, from more coherent context. Unset keeps the 0.75 default. Composes with context_target: the fold point is fraction × min(model_window, context_target).
incremental_checkpoint bool Refresh the durable session checkpoint in the background at 20%-of-window usage thresholds, without folding, so a resumed session recovers fresh state (adapted from MiMo-Code). Default true; set false to disable the background summary calls. Forced off in headless -p/--loop (nothing there persists it).
file_excerpt_cap_tokens integer Per-result token cap for read excerpts (default: 12000). Every tool result is head+tail truncated at the turn-end cap (3000 tokens, or 1000 once context passes 60%); a file excerpt gets this roomier allowance instead, because it is the material the next edit is written against and edit_lines anchors on line hashes that only survive while the rows are intact. Raise it for a codebase of large files; set it to 3000 to hold reads to the same cap as any other tool output. Floored at 3000, and the aggressive tier still overrides it. See Large file reads.
agents object Optional user-defined agent profiles, keyed by name. Each is a { prompt, model, deny_tools/allow_tools, reasoning, temperature, description, subagent } bundle activated at runtime with /agent <name>. Lowest-precedence source — .dirge/agents/*.md and ~/.config/dirge/agents/*.md override same-named entries. Absent = no profiles (opt-in).
max_tokens integer Maximum response tokens. Default: 8192.
max_agent_turns integer Maximum agent turns per response. Default: 100.
temperature number Model sampling temperature in 0.02.0. --temperature CLI flag overrides this. Values outside the range are clamped with a stderr warning.
no_tools boolean Disable all tools. Default: false.
no_context_files boolean Disable loading global/project AGENTS.md and CLAUDE.md context files. Default: false.
no_skills boolean Disable on-disk skill discovery: no skill catalog in the preamble and no skill tool. Discovery normally spans ~/.claude, ~/.opencode, ~/.agents, ~/.dirge /skills plus every project ancestor, and honours neither DIRGE_CONFIG_DIR nor DIRGE_DATA_DIR — so this is the lever for reproducible runs (scripts/loop-ab.sh sets it). Default: false.
skill_anchor_interval integer Restate loaded skills' declared anchor: sections every N turn boundaries. 0 (default) is off. Preserving an anchor through a compaction happens whenever a skill declares one; this is the other half, for skills whose own spec says the anchor must RECUR. Every fire costs tokens, so the rate is yours to set. Note the interval is a FLOOR, not a schedule: the boundary chain returns one nudge and correctness nudges outrank this one, so the effective rate is lower whenever the run is also being steered.
context_window integer Advertised model context window — feeds the status line and the legacy token-reserve path (the compaction fold decision uses context_target, not this). Resolved as: providers.<name>.context_window → this key → built-in per-model table → 128000. Set it per provider when more than one is configured; this top-level key applies to all of them, so correcting one model's window here corrupts the rest. The table usually decides, e.g. Claude Sonnet/Opus 4.x, DeepSeek-V4, GPT-4.1, Gemini-2.5-Flash, and Llama-4 = 1M (Gemini-2.5-Pro = 2M); base Claude and o3 = 200k; GPT-4o, DeepSeek-R1, and older 128k models = 128k. A model neither lookup knows warns once and falls back to 128000.
reserve_tokens integer Tokens to reserve before compaction is triggered. Default: 16384.
thinking_budget_tokens integer Absolute cap on one turn's reasoning trace, in estimated tokens, enforced on dirge's side of the stream — the provider-side budget is a request, and a locally-served model honours it only as far as its template does. Crossing it truncates the trace and disables thinking for the rest of the task. Absent (default) derives a cap per turn instead.
keep_recent_tokens integer Approximate recent-token budget kept verbatim during compaction. Default: 20000.
compact_enabled boolean Enable automatic conversation compaction. Default: true.
compact_tool_schemas string Trim tool schemas to breadcrumbs on a small context window: auto (default) | on | off. auto engages at or below a 48k resolved window. Each tool's description is cut to its first sentence and each parameter's to a clause; names, types, enums and required-ness are untouched, so the model keeps everything it needs to form a well-formed call and loses only the prose about when to prefer one tool over another. No tool is dropped — a model that cannot see a tool cannot ask for it, and that failure is silent and reads as incapability. The full surface is ~16k tokens with the built-ins alone and ~32.6k with MCP servers loaded, the latter larger than a 32k window in its entirety.
dynamic_tool_search boolean Ship only tool_search + a small always-on toolset per request; the model loads more tools on demand via tool_search(query). The always-on set covers the core loop (read, write, edit, bash, grep, glob, list_dir) plus write_todo_list and task_status, so only the long tail (MCP, semantic, spec, debug) needs discovery. ~30% token savings on MCP-heavy sessions. Default: false.
code_mode_rubric boolean Append the "code mode" rubric to the system prompt: for a bulk/fan-out of similar tool calls (roughly 10+ items), write ONE bash script that returns only the distilled result instead of N per-item calls, keeping raw output out of context. Default: false. See code-mode-rubric.md for the measured effect.
context_depth_reminder_threshold integer Consecutive same-file tool calls before a single mid-turn reminder restates the active task + touched files. Opt-in; unset (default) disables. Recommended value: 8.
phased_workflow_enabled boolean Enable the /plan phased workflow (explore → plan → implement → reviewer-runs-code loop). Master kill-switch — /plan is inert unless this is true. Default: false. See agent-loop.md.
phased_workflow_max_review_cycles integer Reviewer-runs-code fix-cycle budget for /plan: how many times a NEEDS_FIX verdict re-runs the implementer before stopping. Default: 2.
phased_workflow_plan_approval boolean Pause /plan after the plan phase and prompt to approve / edit / cancel before any code is written. On edit you type feedback and the plan is regenerated for another review. Default: false (implement immediately). See agent-loop.md.
permission object Permission rules; see the permission config notes below.
restrictive boolean Select restrictive permission mode. Overridden by accept_all/yolo if those are also true.
accept_all boolean Select accept mode, equivalent to --accept-all. Overridden by yolo if true.
yolo boolean Select yolo mode, auto-approving all operations.
sandbox bool / string / object Sandbox bash commands. true/false, a mode string ("off", "bwrap", "microvm"), or an object — see Sandbox configuration. Default: false.
default_permission_mode string Permission mode when no mode boolean/CLI flag is set. Use standard, restrictive, accept, or yolo.
show_tool_details boolean Show tool-result output in the TUI. Default: true.
show_edit_diff boolean Show colorized diff output for edit tool results (- red, + green, @@ cyan). Default: true.
animations_enabled boolean Enable TUI animations (avatar face toggling, spinner repaint timer). Default: true. Set to false to reduce terminal flicker and CPU usage; the avatar freezes to a static face.
show_reasoning boolean Show the model's thinking/reasoning by default, instead of having to press Ctrl+O each turn. Default: false.
keyboard_enhancement boolean Enable the terminal's enhanced keyboard (kitty) protocol so distinct chords like Shift+Enter reach the input editor (Shift+Enter then inserts a newline via insert_newline instead of submitting). Only takes effect on terminals that advertise support (kitty, Ghostty, WezTerm, foot, rio, …); a harmless no-op elsewhere — use Alt+Enter or Ctrl+J there. Default: true. Set false to disable if your terminal misbehaves.
desktop_notifications object Optional OS-level desktop notifications. Off when absent. Set { "enabled": true } to notify on completed runs and prompts waiting for input. Backed by notify-rust on macOS, Linux, and Windows.
max_sessions integer How many of the most-recent prior sessions in the same project (same working dir) to mine for Up-arrow / Ctrl+F command history, seeded ahead of the current session's prompts. Default: 3. Set 0 to keep recall to the current session only. See Command history.
display string Preferred startup pane layout: a |/,/space-separated subset of left, main, right (e.g. "main|right", "main"). The main pane is always shown; this picks which side panels appear. Override at runtime with /display. Default: automatic (side panels shown at ≥152 cols).
tool_result_max_chars integer Hard ceiling on characters per tool result. Default: 500. Combined with tool_result_max_lines (lines applied first; chars trim what's left).
tool_result_max_lines integer Body lines shown inside a tool chamber before collapsing to ↓ N more lines (Ctrl+O to expand). Default: 4. Press Ctrl+O to re-print the most recent collapsed result in full. edit, apply_patch, question, task, and task_status are exempt (their body IS the value).
default_prompt string Prompt name to activate on startup. Default: code.
theme string UI color theme. phosphor (default — 80s CRT green-on-black), plain (pre-theme white/cyan), or any <name>.theme.json file in the config dir. See themes.md.
tools object Per-tool settings. tools.websearch / tools.webfetch (bool, default true) gate the two web tools, but only as one operand of an OR: a tool is registered when its config value is true or its env var (WEBSEARCH_ENABLED / WEBFETCH_ENABLED; truthy = true or 1) is set — so an explicit config false is re-enabled by the env var, not dropped. tools.bash_output_inline_max_bytes, tools.webfetch_output_inline_max_bytes, and tools.task_output_inline_max_bytes (integer, default 8192 each) cap how much of a bash / webfetch / task result is returned inline versus relayed to ~/.dirge/transient/<pid>/ with a head/tail summary and a read-tool hint.
memory object Long-term memory retrieval tuning. See Hybrid memory retrieval below. Absent = the builtin BM25 store.
memory_graduation boolean Recurrence-weighted salience graduation for the memory curator: detect near-duplicate entries and boost the representative's salience. Default true. Non-destructive — it never merges or deletes; the LLM curator pass still owns dedup.
mcp_servers object MCP server map when compiled with the mcp feature. When omitted, defaults to a single Exa Web Search server; see below.
acp_servers object ACP server config map when compiled with the acp feature. See the ACP section below.
editor_open_command string Opt-in editor follow-along: a command template with {path} and {line} placeholders (e.g. "zed {path}:{line}", "code --goto {path}:{line}"). When set, dirge opens files it reads or edits in this external GUI editor, detached and non-blocking — the editor "follows along" like Zed's AI panel. None (unset) disables the feature entirely.

Desktop Notifications

Desktop notifications are off unless desktop_notifications.enabled is set. dirge uses notify-rust on macOS, Linux, and Windows: a system notification banner on macOS (sent through the Terminal sender), a freedesktop D-Bus notification on Linux (needs a running notification daemon), and a WinRT toast on Windows. On macOS the banner is attributed to Terminal and on Windows to PowerShell, since dirge doesn't register its own notification identity.

{
  "desktop_notifications": {
    "enabled": true,
    "on_completion": true,
    "on_input_required": true
  }
}

When enabled, omitted on_completion / on_input_required keys default to true. on_completion fires only when a run actually returns to idle, not when an automatic follow-up, reviewer pass, loop iteration, or queued interjection continues immediately.

Context window & compaction

Two keys control how large the live context grows before history is folded into a summary (compaction):

  • context_target — the working-context budget in tokens (default 250000). The decision treats the effective window as min(model_window, context_target), so a model whose window is under the budget uses its own (128k stays 128k) while models advertising 1M+ are held to the budget rather than folding on a fraction of their full window. Context quality degrades gradually past ~100k and varies by model, so 250k is a middle ground — set it lower (e.g. 100000) for smaller local models or cost-sensitive routes (see the README for the reasoning). Floored at 16k; a value above the model's real window is a no-op.
  • compaction_fold_threshold — the fraction of that budget at which the fold (and durable checkpoint) fires, clamped to 0.30.75 (default 0.75). Lower folds earlier, from more coherent context.

The fold point — the size the context reaches before compaction kicks in — is the product of the two:

fold_point = compaction_fold_threshold × min(model_window, context_target)

Examples below assume a large-window model (≥250k) so the budget is what binds; on a smaller model the fold point is fraction × model_window instead.

Goal context_target compaction_fold_threshold Folds at
Default unset (250k) unset (0.75) ~188k
Smaller, tighter context 60000 unset (0.75) ~45k
Same budget, fold earlier unset (250k) 0.5 ~125k
Both 80000 0.6 ~48k
{
  "context_target": 80000,
  "compaction_fold_threshold": 0.6
}

Set compact_enabled to false to disable automatic compaction entirely. (The separate context_window / reserve_tokens keys feed the status line and the older token-reserve path; the budget above is what the fold decision uses.)

Large file reads

Two separate mechanisms shrink what the model sees of a file, and they have different knobs.

The turn-end result cap. Every tool result is head+tail truncated before each send: 3000 tokens normally, 1000 once estimated context passes 60%. The model that called the tool sees the full result on that turn; later turns see the truncation. A read excerpt is held to file_excerpt_cap_tokens (default 12000) instead, because it is the material the next edit is written against and edit_lines anchors on line hashes that only survive while rows are intact.

{
  "file_excerpt_cap_tokens": 40000
}

Raise it if you work in a codebase of large files and see the agent re-reading the same one; set it to 3000 to hold reads to the same cap as any other tool output. It is floored at 3000, and the aggressive (>60% context) tier still overrides it — a roomier allowance is worth nothing if the request stops fitting. Truncation always cuts on a line boundary, so surviving rows keep their <n> <hash>: prefixes and stay usable as edit_lines anchors.

Prompt compression. The [compression] engine windows verbose tool output (logs, diffs, grep dumps). It does not touch source files, file excerpts, or your own messages. Those exemptions have overrides, but they exist for A/B testing the change that introduced them and there is no reason to turn them on:

Key Default Effect when set
enabled true false disables compression entirely — request bodies go out untouched.
preset "dirge" Named profile. "safe" / "lossless" are also output-neutral; "agent", "aggressive", "auto", "rag", "code" enable lossy stages and directives that alter the model's output.
trim_user_text false Let windowing touch your own messages too.
window_code false Let windowing fold and window code and file excerpts.
header "explicit" "legacy" restores the pre-0.21.10 elision-header wording.
verbatim true false ignores read(verbatim=true)'s compression opt-out.

To turn it off for one run without editing config, pass --no-compression or set DIRGE_COMPRESSION=0 (off, false, no, and disabled also work). DIRGE_COMPRESSION_PRESET overrides preset the same way. Precedence is most-local-wins: the CLI flag beats the env var, which beats the config file.

If you need a guarantee that what you are looking at is byte-for-byte what is on disk, call read with verbatim: true — that result is exempt from compression entirely.

Hybrid memory retrieval

By default the memory tool's search is BM25 (keyword) only — exact on paths, error codes, and identifiers, but blind to paraphrase. Opt into hybrid dense+BM25 retrieval to also recover semantically-related entries, fused with Reciprocal Rank Fusion. It needs an OpenAI-compatible embeddings endpoint.

{
  "memory": {
    "hybrid_retrieval": true,
    "embed_url": "https://api.openai.com/v1/embeddings",
    "embed_model": "text-embedding-3-small",
    "embed_api_key_env": "OPENAI_API_KEY"
  }
}
Key Type Description
hybrid_retrieval boolean Turn on dense+BM25 fusion. Default false.
embed_url string OpenAI-compatible /v1/embeddings endpoint. Required for hybrid; if unset, retrieval stays BM25.
embed_model string Embedding model id. Default text-embedding-3-small — set it when pointing at a non-OpenAI endpoint.
embed_api_key_env string Name of the env var holding the API key (the key itself is never stored in config). Omit for a keyless local endpoint.
verbatim_pre_recall boolean Each turn, auto-search memory on the verbatim user message and inject the hits as a supplemental context note (separate from the frozen system-prompt snapshot — it never changes the cached prefix). Surfaces relevant memory the agent wouldn't think to look up. Works with BM25 or hybrid. Default false.
confirm_writes boolean Require human confirmation before any memory add is stored. See Confirming memory writes. Default false.

Confirming memory writes

dirge decides what is worth remembering on its own. The agent writes mid-session whenever it judges something memorable, and after an idle session the background review and memory curator — forked LLM runners — write more. All of it lands in the system prompt of every later session in the project, and under global scope, of every project.

confirm_writes puts a human in that loop:

{ "memory": { "confirm_writes": true } }

An add is then queued rather than stored. A queued entry is inert — it is not in the prompt snapshot, not in memory view, and not in memory search — and the model is told plainly that it is not yet stored, so it does not treat the fact as durable. Review them with:

/memory review

which opens the queue in $EDITOR. The file is the desired final state: delete a block to reject it, edit the text to reword it, or type a new block to record something the model never noticed. Saving stores exactly what is in the file; quitting without saving (:cq) leaves the queue untouched for next time. Accepted entries go in through the normal write path, so a memory you typed is indistinguishable from one the agent proposed.

Only add is gated. replace, supersede and remove act on entries you already accepted, and supersede usually fires because you just corrected the agent — asking you to confirm your own correction would be noise.

A notice appears when the queue is non-empty, both after the post-session passes and once at startup. Headless -p has no one to ask, so entries simply queue there and wait for your next interactive session; nothing is auto-accepted and nothing is lost.

Safe by default and on failure: with hybrid_retrieval off, or the endpoint unset/unreachable/timed out, search silently falls back to BM25 — it never errors. Embeddings are computed at search time (the first search of a session embeds all active entries; later searches only embed the query) and cached for the session.

Cost note: enabling hybrid_retrieval and verbatim_pre_recall together means roughly one embeddings API call per agent turn (pre-recall searches the verbatim message every turn, and with hybrid each search embeds the query). On a paid endpoint that adds up over a long session; on a local endpoint it's just latency.

Providers and roles

Providers are declared once in the providers map and referenced by alias from the role-assignment keys — so each role can run on a different model:

{
  "provider": "deepseek",
  "review_provider": "glm",
  "escalation_provider": "anthropic",
  "subagent_provider": "glm",

  "providers": {
    "deepseek": {
      "model": "deepseek-v4-pro"
    },
    "glm": {
      "model": "glm-4.6"
    },
    "anthropic": {
      "model": "claude-opus-4-5"
    },
    "ollama": {
      "provider_type": "openai",
      "base_url": "http://127.0.0.1:11434/v1",
      "model": "llama3.1"
    }
  }
}

Each providers entry accepts:

Field Description
provider_type Built-in backend to use: openrouter, openai, openai-responses, anthropic, gemini, deepseek, glm, cerebras, opencode, kimi, ollama, or custom. Optional — defaults to the entry's alias when that alias matches a built-in name. openai speaks the Chat Completions API (/v1/chat/completions); openai-responses speaks the Responses API (/v1/responses) — see below.
base_url Endpoint base URL (for custom / self-hosted endpoints).
model Model name for this provider.
api_key Literal key or ${ENV_VAR} interpolation. Takes precedence over api_key_env.
api_key_env Name of the env var holding the API key.
auth Authentication mode: api-key (default), chatgpt for Codex/OpenAI login tokens, anthropic / claude-code for Anthropic Claude Code OAuth, or kimi for Kimi Code (Moonshot) device OAuth.
allow_insecure Allow http:// URLs (plaintext). Default false; only enable for local-only proxies.
context_window Context window for this provider's model, in tokens. Takes precedence over the top-level context_window and the built-in model table, and is the key to reach for when more than one provider is configured — the top-level one applies to all of them. Every compaction threshold divides by this number, so a wrong one folds a context with room to spare or fails to fold one without. Needed for a self-hosted or locally-served model, whose name is often a file path the model table cannot match.
stream_chunk_timeout_secs Per-provider streaming chunk timeout override.
effort Default reasoning effort for this provider's model: off, minimal, low, medium, high, xhigh, or max. xhigh and max are distinct tiers on OpenAI and Anthropic (xhigh for long-horizon agentic work, max for unconstrained capability); providers that lack an xhigh tier fold xhigh up to max (GLM-5.3 takes low/high/max; DeepSeek has no xhigh but does accept medium) or to high (Cerebras takes low/medium/high). Providers that don't accept a given level collapse it to the nearest one they do — e.g. GLM-5.3 takes only low/high/max, so medium/minimal go out as high/low. A /effort session override takes precedence over this. Unrecognized values are warned and ignored.
multimodal Override for whether this provider/model accepts image input (gates the Ctrl+V image-paste UX). true/false forces it either way; omit to auto-detect from the model name and provider type. Set true to enable pasting into a local vision model (e.g. Ollama llama3.2-vision) behind a generic provider type.
headers Extra HTTP request headers. A value that is exactly ${ENV_VAR} is replaced by that environment variable (an unset variable is a hard error); any other value, including one with ${…} embedded in surrounding text, is sent literally.
options Free-form per-provider model options; currently honors temperature.

The aliases on the left of the map become the values you write in role-assignment keys.

OpenAI Chat Completions vs Responses API

provider_type: "openai" sends requests to the Chat Completions endpoint (/v1/chat/completions). Some OpenAI-compatible endpoints only expose the newer Responses API (/v1/responses) — e.g. an OAuth proxy that mirrors what OpenAI itself serves for GPT-5+. Set provider_type: "openai-responses" to target that endpoint instead:

{
  "providers": {
    "gpt5-proxy": {
      "provider_type": "openai-responses",
      "model": "gpt-5.6",
      "base_url": "http://127.0.0.1:8639/v1",
      "api_key": "${MY_PROXY_KEY}",
      "allow_insecure": true
    }
  }
}

It behaves like openai in every other respect (default model, API-key auth, base_url handling) — only the request shape and endpoint differ. The api key is sent as a bearer token; unlike auth: chatgpt this uses no OAuth/Codex login, so it works against any plain /v1/responses server. (allow_insecure is honored here since no OAuth bearer is involved.)

GitHub Copilot

Copilot has no built-in provider_type; it does not need one. Its endpoint is OpenAI-compatible, so it is reached with the generic openai / openai-responses types and a base_url of https://api.githubcopilot.com.

Get a token from the GitHub CLI:

gh auth login
export GH_API_KEY="$(gh auth token)"

Then point one or more provider entries at Copilot. Different Copilot models sit behind different APIs, so the provider_type is per model — the Responses API for the ones that require it, Chat Completions for the rest:

{
  "provider": "grok45",
  "critic_provider": "luna56",
  "summarization_provider": "luna56",
  "approval_provider": "luna56",
  "providers": {
    "grok45": {
      "provider_type": "openai-responses",
      "base_url": "https://api.githubcopilot.com",
      "api_key": "${GH_API_KEY}",
      "model": "grok-4.5",
      "multimodal": true
    },
    "luna56": {
      "provider_type": "openai",
      "base_url": "https://api.githubcopilot.com",
      "api_key": "${GH_API_KEY}",
      "model": "gpt-5.6-luna",
      "multimodal": true
    }
  }
}

api_key is expanded at use time, so the token stays in the environment rather than in the config file. Which models you can select depends on your Copilot plan, and gh auth token returns a token tied to your gh login — re-export it after re-authenticating.

Recipe contributed by @dubchord, verified against Copilot Enterprise (GH #698).

Cerebras

Cerebras needs no providers entry. Export its API key and select the built-in:

export CEREBRAS_API_KEY="..."
dirge --provider cerebras  # defaults to gemma-4-31b

The built-in sends OpenAI-compatible Chat Completions requests to https://api.cerebras.ai/v1. To pin another Cerebras model, add only the model override; keep the secret in CEREBRAS_API_KEY:

{
  "provider": "cerebras",
  "providers": {
    "cerebras": {
      "model": "gpt-oss-120b"
    }
  }
}

Set providers.cerebras.base_url to an HTTPS proxy or custom Cerebras endpoint. Dirge does not read a separate CEREBRAS_BASE_URL variable:

{
  "provider": "cerebras",
  "providers": {
    "cerebras": {
      "model": "gemma-4-31b",
      "base_url": "https://cerebras-proxy.example.com/v1"
    }
  }
}

Reasoning and image support depend on the model. For gemma-4-31b, Dirge sends top-level reasoning_effort values (low, medium, or high) and accepts image input. It clamps lower and higher Dirge reasoning levels to that three-value set and omits the field when reasoning is off. Dirge treats gpt-oss-120b and zai-glm-4.7 as text-only. This integration does not expose other Cerebras-specific request options.

OpenAI browser / device-code auth

Run dirge auth openai to authorize OpenAI through the browser OAuth flow and persist a local OAuth refresh token. Dirge prints an OpenAI authorization URL and waits for the browser redirect on http://localhost:1455/auth/callback. This is the preferred ChatGPT/Codex subscription login path.

For headless environments, run dirge auth openai --device-code to use the older device-code flow. Before using that mode, enable device-code auth in ChatGPT Codex security settings. Dirge prints the OpenAI verification URL and user code; the user code is part of the interactive login UX, but you should not share it with anyone.

The credential store lives in the Dirge data directory, not the repository or program directory:

  • Linux default: ~/.local/share/dirge/auth.json
  • Override: $DIRGE_DATA_DIR/auth.json

Successful login persists across Dirge sessions. Delete auth.json or revoke the OpenAI authorization if you want to force a new login.

For the canonical openai provider with no configured base_url, a fresh stored OAuth credential is treated as subscription auth and is preferred before API-key billing. Explicit auth: "chatgpt" also uses this fresh Dirge-managed OpenAI OAuth credential before falling back to legacy ~/.codex/auth.json storage, so rerunning dirge auth openai is enough to recover from a stale Codex login file. OpenAI-compatible aliases and providers with a custom base_url keep normal API-key behavior. If no fresh OAuth credential exists, Dirge uses the usual API-key sources: explicit CLI keys, key files/stdin, config api_key, config api_key_env, and provider environment variables. If the OAuth/Codex request reports subscription quota or model-access exhaustion, Dirge asks before switching that request to API-key billing.

Troubleshooting:

  • Browser callback port is busy: stop the process using port 1455 and rerun dirge auth openai, or use dirge auth openai --device-code in a headless environment.
  • OpenAI device-code auth is not enabled or a 404 from the user-code endpoint: enable device-code auth in ChatGPT Codex security settings and rerun dirge auth openai --device-code.
  • Timeout: complete approval in the browser and rerun the command.
  • Corrupt auth store: fix or remove auth.json, then rerun dirge auth openai.

Anthropic Claude Code OAuth

To use a Claude Pro/Max subscription token instead of an Anthropic API key, run:

dirge auth anthropic

Complete the browser login. dirge listens on http://localhost:53692/callback, exchanges the PKCE code, and writes credentials to ~/.claude/.credentials.json in the same shape as Claude Code. Then configure the Anthropic provider to use OAuth:

{
  "provider": "anthropic",
  "providers": {
    "anthropic": {
      "auth": "anthropic",
      "model": "claude-sonnet-4-5"
    }
  }
}

Aliases claude-code, claude_code, and claude are accepted for the same auth mode. ANTHROPIC_OAUTH_TOKEN can also provide a raw access token for smoke tests, but persisted credentials are preferred because dirge can refresh expired tokens before rebuilding the Anthropic client.

Kimi Code (Moonshot) OAuth

To use a Kimi membership's managed coding API instead of a raw API key, run:

dirge auth kimi   # alias: dirge auth kimi-code

This is a device-code flow (there is no browser/localhost variant): Dirge prints a verification URL and user code, you authorize in the browser, and Dirge polls until the login completes. The credential is stored under the kimi key in the same $DIRGE_DATA_DIR/auth.json used by the other OAuth logins.

Then select the provider (a stored login is also auto-detected when no provider/env key is configured):

{
  "provider": "kimi",
  "providers": {
    "kimi": {
      "model": "k3"
    }
  }
}

Models: k3 (default), kimi-for-coding, and kimi-for-coding-highspeed, all OpenAI-compatible chat completions against https://api.kimi.com/coding/v1 with a 262144-token context window. Lower membership tiers without K3 access should use --model kimi-for-coding.

Kimi access tokens live only 15 minutes; Dirge refreshes them automatically (before client construction and mid-session) and persists the rotated token bundle, so a long session does not die on token expiry.

Environment overrides:

  • KIMI_CODE_API_KEY — static bearer used instead of the OAuth login (not refreshable).
  • KIMI_CODE_BASE_URL — API base URL override (https only).
  • KIMI_CODE_OAUTH_HOST / KIMI_OAUTH_HOST — OAuth host override (default https://auth.kimi.com).

Aliases kimi-code and moonshot are accepted for the provider name and the auth mode. The separate Kimi Platform API (api.moonshot.ai) is not this provider — use a custom provider with an API key for that.

Prompt caching

dirge asks each provider to cache the stable part of every request (the system prompt and tool definitions, plus the conversation history where the provider supports it) so it is billed once instead of on every turn. Cached input reads at roughly a tenth of the normal rate, which makes the cache hit ratio the main cost lever on a long session. /cache reports that ratio along with the cumulative read and write totals.

Nothing needs configuring for this to work. Most providers cache the longest matching prefix by themselves; where an explicit breakpoint is required (Anthropic directly, and the Anthropic, Qwen and Gemini routes through OpenRouter) dirge places one.

The one knob is how long an Anthropic cache entry lives:

{
  "prompt_cache": { "ttl": "1h" }
}

"1h" (the default) or "5m". The tradeoff is in the write price: a 1h write costs 2x the base input rate against 1.25x for 5m, while reads are a tenth either way. A read refreshes a 5m entry, so a session that keeps working stays warm on 5m and never pays the premium. What 1h buys is the idle gap: if more than five minutes pass between turns, a lapsed 5m entry means re-writing the whole prefix on the next turn rather than just that turn's delta. Pick "5m" if your sessions run continuously, keep "1h" if you leave dirge sitting while you read or work elsewhere. DIRGE_PROMPT_CACHE_TTL overrides the setting at runtime.

Role assignments

Key Used for Falls back to
provider Default / main loop (none — required)
review_provider Background session-review pass provider
escalation_provider One-shot retry after repair-exhaustion / pre-write syntax failure provider (no-op when equal)
summarization_provider Context compaction side calls (required for LLM compaction when provider uses Anthropic OAuth) provider when safe
subagent_provider task tool subagents provider
critic_provider F6 in-loop critic (tier 3) + diff-aware code reviewer (/code-review) + goal-gate judge (--goal) none (off)

When a role's provider equals provider (either explicitly or by fallback), no duplicate client is constructed and the feature has zero overhead — escalation routes, for example, simply don't fire because they'd be a no-op anyway.

Migration note: dirge no longer reads the legacy top-level model, custom_providers, or review_model keys — starting a session with any of those at the root fails fast with a migration hint. Move model inside the active provider's entry, custom_providers.<name> entries directly into providers, and review_model into the entry referenced by review_provider.

Permissions

Permission actions are lowercase strings: allow, ask, or deny. rules is an ordered list read top-to-bottom; last match wins. Each rule has:

  • op — the operation class it governs (NOT a tool name). One of: read, edit, execute, network, mcp, memory, skill, agent, meta, or * (any). edit covers write/edit/apply_patch — they're one operation, so one rule governs all three.
  • match — a glob. Read/edit use path-style globs (* is one path segment, ** spans directories); execute/network/mcp use shell-style (* matches anything including /, trailing * makes args optional). The * (any) op uses shell-style too, since it can match commands and MCP keys as well as paths. MCP patterns match the full key mcp_tool:{server}:{tool}.
  • effectallow, ask, or deny.
  • tool (optional) — narrow the rule to a single concrete tool name (e.g. "grep") instead of the whole op.

Use "*" for the default action, external_directory (also a rules list, op defaults to *) for absolute-path rules outside the working directory, and doom_loop for the retry-loop hard-deny (set to allow to disable it). dirge always installs its built-in safe bash allow/deny rules and a read-only/memory/skill/in-cwd-write allow set beneath your rules; your rules override them.

MCP tools default to ask for ALL servers — they execute external code (the server's implementation, plus whatever filesystem / network / API effects it has), and silent default-allow let entire query sequences run before any prompt fired. To re-enable silent allow for a trusted server:

{
  "permission": {
    "rules": [
      { "op": "mcp", "match": "mcp_tool:lattice:*", "effect": "allow" }
    ]
  }
}

Or accept once at the alert and pick "allow always" for the same session-allowlist effect.

Mode semantics

  • standard (default): every rule in permission is consulted; tools without matching rules fall back to * (default allow).
  • restrictive: like standard, but any tool whose rule resolves to allow via the * fallback (no explicit allow rule matched) is converted to ask. Explicit allow rules still allow. Explicit deny rules still deny.
  • accept (equivalent to --accept-all): auto-allows tools whose targets resolve inside the working directory; tools touching paths outside still consult external_directory rules.
  • yolo (equivalent to --yolo): bypasses every check. Use with caution.

CLI precedence (high → low): --yolo > --accept-all > --restrictive > default_permission_mode config > standard.

When compiled with MCP support, mcp_servers accepts command-based and URL-based servers:

{
  "mcp_servers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "@modelcontextprotocol/server-filesystem", "."],
      "env": {}
    },
    "semantic-index": {
      "command": "my-indexer",
      "args": ["--repo", "/work/other-project"],
      "allow_external_paths": true
    },
    "remote-search": {
      "url": "https://example.com/mcp",
      "headers": {
        "authorization": "Bearer token"
      }
    }
  }
}

If mcp_servers is omitted (null) and the mcp feature is enabled, dirge adds a default Exa Web Search MCP server at https://mcp.exa.ai/mcp with the x-api-key header set to EXA_API_KEY when that environment variable is set. Set "mcp_servers": {} to disable all MCP servers.

Per-server external-path opt-in (allow_external_paths)

By default an MCP tool call whose JSON arguments name a path resolving outside the working directory is refused with a clear error — matching the trust model of dirge's built-in file tools (read / write / edit anchored to cwd). The check scans top-level args fields named path, file_path, file, directory, dir, cwd, and the paths array.

Some MCP servers legitimately need broader scope: a semantic indexer pointed at a sibling repo, a project-wide search tool, a backup utility. Set "allow_external_paths": true on that one server's config (both Command and Url variants accept it; default false) to skip the cwd guard for tools from THAT server only.

The flag is path-scoped and narrow:

  • It only bypasses the cwd-external-path check.
  • It does NOT bypass mcp_tool deny rules, prompt deny_tools frontmatter, doom-loop detection, the sandbox, or --yolo/--restrictive mode logic — every other gate runs unchanged.
  • It applies per-server: enabling it on semantic-index does not affect filesystem or any other server in the same config.

Pair it with a tight mcp_tool rule for layered control, e.g.:

{
  "mcp_servers": {
    "semantic-index": {
      "command": "indexer",
      "allow_external_paths": true
    }
  },
  "permission": {
    "rules": [
      { "op": "mcp", "match": "mcp_tool:semantic-index:*",          "effect": "allow" },
      { "op": "mcp", "match": "mcp_tool:semantic-index:write_file", "effect": "deny"  }
    ]
  }
}

MCP tools and prompt deny-lists

Per-prompt deny_tools frontmatter (see "Prompt restrictions" below) applies to MCP tools too. The deny gate matches against three names for each MCP tool call:

  • the raw tool name as exported by the MCP server (e.g. edit, write_file),
  • the qualified mcp_tool:<server>:<name>,
  • the umbrella mcp_tool (denies every MCP tool from every server).

So a plan-mode prompt that ships deny_tools: [edit, write, apply_patch, bash] also blocks any MCP server that exports a tool named edit / write / apply_patch / bash. Use mcp_tool as a blanket deny when in doubt about what an MCP server might expose.

Plugin trust boundary

The Janet plugin system runs INSIDE the trust boundary. Plugin hooks (on-tool-start, on-tool-end) can mutate tool inputs, block tool calls, and replace tool outputs with arbitrary text. They cannot, however, bypass the permission checker (check_perm* runs inside the inner tool, after the plugin pre-hook). If you load third-party plugins, treat them with the same care you'd give to executing third-party code in your shell — the plugin's trust level effectively equals the user's. There is no sandboxing.

Per-plugin settings (plugins)

Plugins are discovered from ~/.config/dirge/plugins/ and ./.dirge/plugins/ and load automatically. The optional plugins object toggles them by name — the directory name (multi-file plugins) or the .janet file stem (single-file plugins):

{
  "plugins": {
    "backpressured": { "enabled": true, "auto_start": true },
    "nrepl":         { "enabled": false }
  }
}
Field Default Effect
enabled true Whether to load the plugin. false skips it entirely.
auto_start false Passed to the plugin via harness/plugin-config; a plugin that supports it self-engages at startup (e.g. backpressured runs its loop without the keyword).

A plugin with no entry — or no plugins block at all — is enabled and not auto-started, so existing setups load every plugin exactly as before.

Plugin authors: read your own settings in load-time code with (harness/plugin-config), which returns @{:enabled bool :auto-start bool} (or nil). The host sets it just before your files load and clears it after, so capture it at the top level — not from a shared hook, where it would reflect the last plugin loaded.

Sandbox configuration

The sandbox key accepts three forms:

// 1. boolean — false (off) or true (bubblewrap, Linux)
"sandbox": true

// 2. mode string
"sandbox": "off" | "bwrap" | "microvm"

// 3. object — required for microVM, optional for tuning
"sandbox": {
  "mode": "microvm",        // "off" | "bwrap" | "microvm"
  "image": "alpine:latest", // microVM root image (microvm mode)
  "cpus": 2,                // microVM vCPUs (1–255)
  "memory_mib": 1024        // microVM memory in MiB
}
  • bwrap runs each bash command inside bubblewrap (Linux only; needs the bwrap binary). The working directory is bound read-write; the rest of the filesystem is read-only.
  • microvm runs commands in a full microVM (requires the sandbox-microvm build feature). image, cpus, and memory_mib apply only to this mode.
  • A legacy nested form {"mode": "microvm", "microvm": {"image", "cpus", "memory_mib"}} is still accepted; out-of-range cpus/memory_mib are now a config error rather than silently wrapping.
  • A top-level microvm_image key (e.g. "local://dirge-microvm:alpine") is deprecated but still honoured as a fallback. Prefer sandbox.image above.

Streaming timeouts

dirge applies a per-chunk read deadline to streaming LLM responses so a silently-dropped TCP connection (which reqwest can't always detect) doesn't freeze the agent. The default is 5 minutes (300s) — well above any legitimate reasoning gap from Claude 3.7 extended thinking, GPT-5 thinking, or large-tool-output processing. Bump it if you see false-positive stream chunk timed out errors in the middle of a turn.

Resolution order (first hit wins):

  1. providers.<name>.stream_chunk_timeout_secs — per-provider override
  2. top-level stream_chunk_timeout_secs — applies to every provider
  3. 300s default

Provider name matching is case-insensitive (anthropic matches --provider Anthropic).

{
  "stream_chunk_timeout_secs": 300,
  "providers": {
    "anthropic": { "stream_chunk_timeout_secs": 900 },
    "ollama":    { "stream_chunk_timeout_secs": 60 },
    "my-vllm": {
      "provider_type": "openai",
      "base_url": "http://localhost:8000/v1",
      "api_key_env": "VLLM_API_KEY",
      "stream_chunk_timeout_secs": 1200
    }
  }
}

Operation timeouts

Every other per-operation timeout is named in one place — the timeouts block — and installed process-wide at startup. Each field is in seconds; omitted fields keep their built-in default. (The streaming chunk timeout above is the one exception with richer per-provider precedence; timeouts.stream_chunk_secs acts as its global fallback.)

Field Default What it bounds
stream_chunk_secs 300 Per-chunk read deadline for a streaming LLM response (fallback for the per-provider key above)
request_establish_secs 300 Deadline for establishing a streaming request — the connection/handshake and the wait for the first response event. Distinct from stream_chunk_secs, which only guards gaps between chunks once the stream is live; a connection that stalls during the handshake never produces a first chunk. A timeout here is retried automatically. Generous by default so it only fires on a genuine stall; lower it for faster failure or raise it if a provider legitimately delays the first response.
tool_call_gap_secs 60 Stall window while a tool call is mid-assembly in the stream. A timeout here is retried automatically when no response text has been emitted yet (the partial tool call is discarded and the request restarted); if text was already shown, the partial is kept to avoid a duplicated response. Raise it only if your provider legitimately pauses longer than 60s between tool-call deltas.
mcp_call_secs 120 Total budget for one MCP tool call, including reconnect + retry
mcp_init_secs 10 MCP server initialize handshake
lsp_request_secs 30 Any non-initialize LSP request
lsp_initialize_secs 45 LSP initialize handshake
bash_secs 120 Default bash tool timeout when the call omits one. A DEFAULT, not a bound — the model may pass its own timeout and that wins, up to bash_max_secs
bash_max_secs 3600 Ceiling on a foreground bash timeout, including one the model asked for. Kept separate from bash_secs because raising the timeout for a genuinely long command (a full test suite) is correct and clamping every request to the default would break it; this only has to sit above anything real. Lower it to hold commands shorter. Foreground only — a backgrounded shell with no timeout still runs until it exits or is killed
{
  "timeouts": {
    "mcp_call_secs": 60,
    "lsp_initialize_secs": 90,
    "bash_secs": 300
  }
}

Key bindings

VSCode-style overrides. keybindings is an array of { "key": "<chord>", "command": "<command>" }; each entry layers over the built-in defaults, so you only list what you want to change. One array covers BOTH the global "command" keys (scroll, chat nav, …) and the input-editor keys (cursor motion, kill-ring, history, …) — each entry routes to the right one by its command name.

{
  "keybindings": [
    { "key": "ctrl-t",        "command": "toggle_reasoning" },
    { "key": "ctrl-shift-k",  "command": "kill_subagent" },
    { "key": "ctrl-r",        "command": "none" },
    { "key": "alt-a",         "command": "cursor_line_start" },
    { "key": "ctrl-x ctrl-s", "command": "scroll_to_top" }
  ]
}
  • key — a chord, or a whitespace-separated sequence of chords for an emacs-style binding (e.g. ctrl-x ctrl-s). A chord is case-insensitive, - or + separated, modifiers before the key. Modifiers: ctrl, alt (a.k.a. meta/option), shift. Keys: a single character, f1f12, or a named key (enter, esc, tab, backspace, delete, insert, space, up/down/left/right, home, end, pageup/pgup, pagedown/pgdn). Examples: ctrl-t, pageup, ctrl-shift-x, f5, ctrl-x ctrl-s.
  • command — one of the global or input commands below, or none (also unbind) to disable the default binding on that chord (clears it from both contexts).
  • Binding a command to a new chord adds it (the default chord still works unless you separately unbind it). Binding a chord that already has a default replaces it.

Global commands

Command Default Action
toggle_reasoning ctrl-r Show/hide reasoning tokens
expand ctrl-o Expand buffered thinking / reprint last collapsed tool result
scroll_page_up pageup Scroll chat up one page
scroll_page_down pagedown Scroll chat down one page
scroll_to_top ctrl-home Jump to top of chat
scroll_to_bottom ctrl-end Jump to bottom of chat
next_chat ctrl-n Next subagent chat window
prev_chat ctrl-p Previous subagent chat window
close_chat ctrl-x Close the active chat window
kill_subagent ctrl-k Kill the focused subagent
drop_queue alt-x Drop queued interjections (without cancelling the run)
cycle_prompt shift-tab Cycle the active prompt layer to the next available prompt

Input-editor commands

Command Default Action
cursor_line_start ctrl-a, home Cursor to start of line
cursor_line_end ctrl-e, end Cursor to end of line
cursor_left ctrl-b, left Cursor one character left
cursor_right right Cursor one character right
word_left alt-b, alt-left Cursor one word left
word_right alt-f, alt-right Cursor one word right
delete_char_back ctrl-h Delete character before cursor
delete_char_forward ctrl-d Delete character at cursor (forward)
kill_to_line_end ctrl-k Kill to end of line
kill_to_line_start ctrl-u Kill to start of line
kill_word_back ctrl-w Kill word before cursor
delete_word_back alt-backspace Delete word before cursor
delete_word_forward alt-d Delete word after cursor
yank ctrl-y Paste from the kill-ring
yank_pop alt-y Cycle the kill-ring at the last yank
history_prev ctrl-p Previous history entry
history_next ctrl-n Next history entry
reverse_search ctrl-f Reverse-i-search over history
line_up up Up one line (then history)
line_down down Down one line (then history)
undo ctrl-z Undo the last edit
external_editor ctrl-g Open the current buffer in $EDITOR
insert_newline shift-enter, alt-enter, ctrl-j Add a line instead of submitting

insert_newline is how you write a multi-line prompt. shift-enter only reaches dirge on terminals that report it distinctly (see the keyboard_enhancement option above), while alt-enter and ctrl-j work everywhere. Plain enter always submits and is not rebindable.

Some chords serve both contexts (e.g. ctrl-k is kill_subagent and kill_to_line_end, ctrl-n is next_chat and history_next). The global command only fires in its situation — kill_subagent only when the input box is empty, chat nav only with more than one chat window — so the editor handler gets the key the rest of the time.

Chord sequences (emacs-style)

A key may be a sequence like ctrl-x ctrl-s. After the first chord the footer shows the pending prefix (ctrl-x-) and waits; the next chord completes (or aborts) the sequence. Esc or Ctrl+G cancels a pending prefix. By default a pending prefix waits indefinitely (emacs style); set "chord_timeout_ms": <n> at the top level of the config to auto-cancel it after n milliseconds of inactivity. Sequences fire for global commands only; a sequence bound to an input command is rejected with a startup warning. Binding a sequence whose first chord is also a single-key command disables that single-key binding (the sequence wins) — you'll see a warning.

Notes:

  • Always fixed (never rebindable): the cancel/interrupt gesture Ctrl+C / Esc (the panic button) and intrinsic editing — typing a character, Backspace, Delete, Enter to submit, Ctrl+J (insert newline), and Tab completion. Binding a global command to one of these chords shadows the intrinsic behavior while active.
  • Plugins can also add and override bindings; user config always wins over a plugin. See plugins.md.
  • Unrecognized chords or unknown commands are skipped with a warning on startup; the rest of the config still loads.

Command history (cross-session recall)

Pressing Up in the input box recalls previous prompts, and Ctrl+F opens a reverse-i-search over them. By default the recall pool is the current session's prompts. Set top-level max_sessions to an integer N (default 3) to additionally mine the N most-recent prior sessions in the same project (matching working_dir) for their user prompts. Those older prompts are seeded ahead of the current session's own, so Up starts from your newest command and walks back through earlier conversations in the project.

The scan is scoped to the same project and excludes the current conversation's own compaction-fold rotations, so a fold doesn't double its prompts into history. Set "max_sessions": 0 to keep recall limited to the current session. Synthetic turns (system-reminder wrappers, mid-turn steering, auto-continue markers) never enter history.

Slash-command aliases

Rename a built-in slash command, or give it a short alias, with the top-level slash_aliases map. The key is what you type (with or without a leading /); the value is the built-in command it runs (again with or without a leading /). Arguments you type after the alias are passed through to the target.

{
  "slash_aliases": {
    "exit": "quit",
    "q": "/quit",
    "cls": "/clear"
  }
}

With the above, /exit, /q, and /cls all run /quit or /clear. The alias is resolved once before dispatch, so it inherits its target's behavior (e.g. an alias for /quit works while the agent is running).

  • Aliases don't replace the built-in — both names work unless they collide (an alias key that matches a built-in shadows it).
  • A leading / on either side is optional and normalized.
  • A target that isn't a known built-in produces a startup warning (likely a typo) but is still passed through — it may resolve to a plugin command. Plugin-command targets can't be validated ahead of time.
  • An empty alias key is ignored (with a warning); it would otherwise make a bare / run the target.
  • Configured aliases are listed under slash aliases in /help.

Environment variables

Variable Purpose
EXA_API_KEY API key for the built-in websearch tool and the default Exa MCP server. Without this the websearch tool emits a startup warning and is not registered.
DIRGE_WEBFETCH_ALLOW_PRIVATE Set to 1 (or any non-empty value) to allow webfetch to call private / loopback IPs. By default webfetch enforces SSRF protection — it refuses localhost, 127.x, 10.x, 172.16-31.x, 192.168.x, and link-local addresses. Override only in trusted local-dev contexts; never set this in production environments that touch attacker-influenced URLs.
WEBSEARCH_ENABLED / WEBFETCH_ENABLED Force-enable the corresponding tool when not enabled via tools.* config. Useful in container builds where you set the toggle once via env rather than per-config-file.
DIRGE_PROMPT_CACHE_TTL 5m or 1h, overriding [prompt_cache] ttl. See Prompt caching for the cost tradeoff.

LSP configuration

When compiled with the lsp feature (default-on), dirge spawns language servers on demand to surface compile errors in tool output. The lsp config key accepts three forms:

// Default-on, built-in commands for rust/typescript/pyright/clojure-lsp.
{ "lsp": true }

// Off entirely. Same as the --no-lsp CLI flag.
{ "lsp": false }

// Default-on with per-server overrides.
{
  "lsp": {
    "rust": {
      "command": ["rust-analyzer"],
      "env": { "RA_LOG": "rust_analyzer=debug" },
      "initialization": { "cargo": { "buildScripts": { "enable": true } } }
    },
    "typescript": { "disabled": true }
  }
}

Per-server fields (all optional):

Field Type Description
command string[] argv to launch the server. Replaces the built-in default.
extensions string[] Replaces the server's built-in extension list.
extend_extensions string[] Appends to the built-in list (deduped). e.g. route .janet to clojure-lsp without re-listing clj/cljs/cljc/edn/bb. Accepts extendExtensions too.
env object extra env vars for the child process.
initialization object sent as initializationOptions in the LSP initialize request.
disabled boolean true removes the server entirely.

Example — make clojure-lsp also handle Janet files (keeps the built-in Clojure extensions):

{ "lsp": { "clojure-lsp": { "extend_extensions": ["janet"] } } }

CLI flag: --no-lsp (overrides the config; same effect as lsp: false).

Built-in server commands

Server id Default command
rust rust-analyzer
typescript typescript-language-server --stdio
pyright pyright-langserver --stdio
clojure-lsp clojure-lsp
gopls gopls
jdtls jdtls
clangd clangd
ruby-lsp ruby-lsp
bash-language-server bash-language-server start
dafny dafny server --verify-on change
swift sourcekit-lsp
cmake cmake-language-server
mojo mojo-lsp-server

Servers are spawned lazily on first file touch and cached per (workspace_root, server_id) pair. Concurrent agent tool calls for the same file deduplicate so dirge never races two rust-analyzer processes against one workspace.

Known limitations

  • The extensions override is currently ignored. The claimed-extensions list lives in the static builtin_servers() registry at src/lsp/server.rs. Adding new extensions today requires editing that file. Follow-up.

ACP (Agent Communication Protocol) configuration

When compiled with the acp feature, dirge can act as an ACP agent server. The following config keys are available:

Key Type Description
acp_servers object Named ACP server configurations (see below)

dirge's ACP runs over stdio only; the acp_host / acp_port keys that earlier docs mentioned have been removed from the CLI and config in favor of editors driving the agent via stdio.

ACP server configs (in acp_servers) support two transport types:

{
  "acp_servers": {
    "tcp-server": {
      "host": "127.0.0.1",
      "port": 7243,
      "api_key": "optional-key"
    }
  }
}

When --acp is passed without --acp-host, dirge runs in stdio mode (the editor spawns it as a subprocess). With --acp-host, it listens on TCP.