Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
dirge reads an optional JSON config file named `config.json` from its config
folder:

- If `ZS_CONFIG_DIR` is set: `$ZS_CONFIG_DIR/config.json`
- 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`

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

Example:

Expand Down Expand Up @@ -59,7 +59,7 @@ Accepted top-level keys:

| Key | Type | Description |
| ------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider` | string | Provider name. Built-ins are `openrouter`, `openai`, `anthropic`, `gemini`/`google`, and `ollama`; custom provider aliases are also accepted. Default: `openrouter`. |
| `provider` | string | Provider name. Built-ins are `openrouter`, `openai`, `anthropic`, `gemini`/`google`, `deepseek`, `glm`/`zhipu`, and `ollama`; custom provider aliases are also accepted. Default: `openrouter`. |
| `model` | string | Model name. Default: `deepseek/deepseek-v4-flash`. |
| `max_tokens` | integer | Maximum response tokens. Default: `8192`. |
| `max_agent_turns` | integer | Maximum agent turns per response. Default: `100`. |
Expand Down
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Minimal coding agent written in Rust, inspired by [pi](https://pi.dev/docs/lates
## Features

- **Multi-provider**: OpenRouter, OpenAI, Anthropic, Gemini, DeepSeek, GLM, Ollama, plus custom providers
- **Standard tools**: read, write, edit, bash, grep, find_files, list_dir, write_todo_list
- **Standard tools**: read, write, edit, bash, grep, find_files, glob, list_dir, write_todo_list, apply_patch
- **Line-numbered read output**: `read` tool prefixes each line with right-aligned line numbers (`123: content`)
- **Environment-aware**: system prompt includes OS, shell, working directory, and git branch for context
- **Semantic code tools** (tree-sitter): list_symbols, get_symbol_body, find_definition, find_callers, find_callees — supports TypeScript/TSX and Python
Expand Down Expand Up @@ -36,7 +36,7 @@ _dirge_ is one of the smallest and most performant coding agents on the market.

### Tool result caching

Read-only tool calls (`read`, `grep`, `find_files`, `list_dir`) are cached per agent turn. Repeated calls with identical arguments within the same turn return cached results, avoiding redundant filesystem I/O. The cache clears automatically before each new prompt, and after `write`/`edit`/`bash` so a re-read sees fresh content.
Most tool calls (`read`, `write`, `edit`, `bash`, `grep`, `find_files`, `list_dir`) are cached per agent turn. Repeated calls with identical arguments within the same turn return cached results, avoiding redundant filesystem I/O. The cache clears automatically before each new prompt, and after `write`/`edit`/`bash` so a re-read sees fresh content.

### Error recovery

Expand Down Expand Up @@ -107,11 +107,16 @@ dirge --provider glm # defaults to glm-4
| `/mode [mode]` | Set security mode (`standard`, `restrictive`, `accept`, `yolo`) |
| `/reasoning` | Toggle reasoning visibility |
| `/btw <question>` | Ask a quick question (no tools, doesn't affect session) |
| `/session` | List/save/load sessions |
| `/sessions` | List/save/load sessions |
| `/loop [prompt]` | Start iterative coding loop |
| `/worktree <name>` | Create a git worktree on branch |
| `/wt-merge [branch]` | Merge worktree branch |
| `/wt-exit` | Exit worktree |
| `/toggle` | Toggle features on/off (currently todo tools) |
| `/regen-prompts` | Restore built-in prompts |
| `/mcp` | List MCP servers and tools |
| `/quit` | Exit dirge |
| `/retry` | Retry last prompt |
| `/help` | Show all commands |

### Key bindings
Expand Down Expand Up @@ -179,10 +184,11 @@ Built-in prompts that change the agent's behavior and tone:
| **`review-security`** | Security review mode — finds exploitable vulnerabilities |
| **`simplify`** | Code simplification mode — refines for clarity without changing behavior |
| **`write-prompt`** | Prompt writing mode — creates and optimizes agent prompts |
| **`default`** | Default system prompt — the base built-in prompt |

Custom prompts can be placed in `$XDG_CONFIG_HOME/dirge/prompts/` as `.md` files.

The agent automatically loads `AGENTS.md` or `CLAUDE.md` from the project root or ancestor directories. Use `-n` / `--no-context-files` to disable.
The agent automatically loads `AGENTS.md` or `CLAUDE.md` from the project root, ancestor directories, and `~/.config/dirge/agent/AGENTS.md` as a global fallback. Use `-n` / `--no-context-files` to disable.

## Claude-compatible skills

Expand Down
101 changes: 82 additions & 19 deletions src/agent/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,25 +109,11 @@ pub async fn build_agent_inner<M: CompletionModel + 'static>(

// Inject mode-specific reminders
if let Some(prompt_name) = &context.current_prompt_name {
match prompt_name.as_str() {
"plan" => {
preamble.push_str("\n\n---\n\nYou are now in PLAN mode. Create a detailed implementation plan. Save it to PLAN.md in the current directory. Analyze the task, break it into concrete steps, consider edge cases and trade-offs. Do NOT write any code or run any commands until the user reviews and approves the plan.");
}
"review" | "review-security" => {
preamble.push_str("\n\n---\n\nYou are now in REVIEW mode. Review the code or plan carefully. Identify bugs, security issues, performance problems, and design flaws. Be thorough and specific. Provide actionable feedback.");
}
"code" => {
let plan_path = std::env::current_dir()
.unwrap_or_else(|_| ".".into())
.join("PLAN.md");
if plan_path.exists() {
preamble.push_str(
"\n\n---\n\nA plan file exists at PLAN.md. Execute the plan step by step. Write and test code following the plan. Report progress after each step. The plan is your guide — follow it closely."
);
}
}
_ => {}
}
let plan_exists = std::env::current_dir()
.unwrap_or_else(|_| ".".into())
.join("PLAN.md")
.exists();
append_mode_reminder(&mut preamble, prompt_name, plan_exists);
}

let mut builder = AgentBuilder::new(model).preamble(&preamble);
Expand Down Expand Up @@ -314,3 +300,80 @@ pub fn create_client(api_key: Option<&str>) -> anyhow::Result<openrouter::Client
})?;
Ok(openrouter::Client::new(String::from(key))?)
}

/// Append a mode-specific reminder to `preamble` based on the active prompt
/// name. `plan_exists` reports whether `PLAN.md` is present in CWD — only
/// consulted for the `code` mode reminder. Unknown prompt names produce no
/// reminder so custom prompts don't accidentally pick up plan/review semantics.
pub(crate) fn append_mode_reminder(preamble: &mut String, prompt_name: &str, plan_exists: bool) {
match prompt_name {
"plan" => {
preamble.push_str("\n\n---\n\nYou are now in PLAN mode. Create a detailed implementation plan. Save it to PLAN.md in the current directory. Analyze the task, break it into concrete steps, consider edge cases and trade-offs. Do NOT write any code or run any commands until the user reviews and approves the plan.");
}
"review" | "review-security" => {
preamble.push_str("\n\n---\n\nYou are now in REVIEW mode. Review the code or plan carefully. Identify bugs, security issues, performance problems, and design flaws. Be thorough and specific. Provide actionable feedback.");
}
"code" if plan_exists => {
preamble.push_str(
"\n\n---\n\nA plan file exists at PLAN.md. Execute the plan step by step. Write and test code following the plan. Report progress after each step. The plan is your guide — follow it closely.",
);
}
_ => {}
}
}

#[cfg(test)]
mod reminder_tests {
use super::append_mode_reminder;

#[test]
fn plan_mode_injects_plan_reminder() {
let mut p = String::from("base");
append_mode_reminder(&mut p, "plan", false);
assert!(p.contains("PLAN mode"));
assert!(p.contains("PLAN.md"));
assert!(p.contains("Do NOT write any code"));
}

#[test]
fn review_modes_inject_review_reminder() {
for mode in &["review", "review-security"] {
let mut p = String::from("base");
append_mode_reminder(&mut p, mode, false);
assert!(p.contains("REVIEW mode"), "mode={mode}");
assert!(p.contains("Identify bugs"), "mode={mode}");
}
}

// Regression: the `code` reminder must only appear when PLAN.md exists.
// Without that guard every code-mode session would have a stale "execute
// the plan" instruction even with no plan written.
#[test]
fn regression_code_mode_reminder_requires_plan_md() {
let mut p_with = String::from("base");
append_mode_reminder(&mut p_with, "code", true);
assert!(p_with.contains("plan file exists"));

let mut p_without = String::from("base");
append_mode_reminder(&mut p_without, "code", false);
assert_eq!(p_without, "base", "no reminder must be added");
}

// Unknown prompts (custom user prompts) must produce no reminder so the
// plan/review semantics don't bleed into other modes.
#[test]
fn unknown_prompt_name_appends_nothing() {
let mut p = String::from("base");
append_mode_reminder(&mut p, "my-custom-prompt", true);
assert_eq!(p, "base");
}

// Each reminder is prefixed by the section separator so it visually
// detaches from the prior prompt — regression-guards the leading "\n\n---".
#[test]
fn reminders_use_section_separator() {
let mut p = String::new();
append_mode_reminder(&mut p, "plan", false);
assert!(p.starts_with("\n\n---\n\n"), "got: {p:?}");
}
}
Loading
Loading