Expand CLAUDE.md to full project reference (~15K chars)
Covers: complete type signatures, data flow, coding standards, testing requirements, documentation standards, anti-patterns, CLI reference, and prioritized remaining work.
This commit is contained in:
parent
b8bf9029fe
commit
386ac8a391
1 changed files with 316 additions and 96 deletions
410
CLAUDE.md
410
CLAUDE.md
|
|
@ -1,110 +1,330 @@
|
|||
# Slug Code
|
||||
# Slug Code — Agent & Developer Reference
|
||||
|
||||
FOSS Rust rewrite of Claude Code — an AI coding assistant with a pluggable LLM backend targeting OpenAI-compatible APIs (vLLM, Ollama, llama.cpp, OpenAI, etc.).
|
||||
FOSS Rust rewrite of Claude Code. Pluggable LLM backend targeting OpenAI-compatible APIs (vLLM primary). Binary: `slug`. Package: `slug-code`. Rust 2024 edition.
|
||||
|
||||
## Project
|
||||
Repo layout: `master` = original TypeScript Claude Code source (reference only). `rust-rewrite` = this Rust implementation (all work happens here).
|
||||
|
||||
- **Binary:** `slug`
|
||||
- **Package:** `slug-code`
|
||||
- **Branch:** `rust-rewrite` (orphan branch — `master` holds original TypeScript source as reference)
|
||||
- **Working directory:** `/home/bryan/Downloads/claude/src`
|
||||
---
|
||||
|
||||
## Architecture
|
||||
## Module Map
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| `src/main.rs` | CLI entry (clap), wires everything together |
|
||||
| `src/provider/` | Provider trait + OpenAI-compatible streaming SSE impl |
|
||||
| `src/agent/` | Core agent loop: stream → tool exec → repeat, SLUG.md injected per turn |
|
||||
| `src/tools/` | Tool trait + bash, read, write, edit, glob, grep |
|
||||
| `src/tui/` | Interactive REPL (basic stdin/stdout — ratatui TUI not yet built) |
|
||||
| `src/permissions/` | ask/yolo/sandbox/allowEdits + glob allow/deny from settings.json |
|
||||
| `src/slugmd/` | SLUG.md hierarchy loaded every turn (40K char budget) |
|
||||
| `src/session/` | JSONL session persistence at ~/.slug/sessions/ |
|
||||
| `src/hooks/` | 5 lifecycle events, command + prompt hook types |
|
||||
| `src/compact/` | 3 compaction strategies, /compact command |
|
||||
| `src/config/` | TOML config + CLI overrides + env vars |
|
||||
|
||||
## Conventions
|
||||
|
||||
- Rust 2024 edition
|
||||
- No `unwrap()` in non-test code — use `?` or `anyhow`
|
||||
- No analytics, telemetry, or Anthropic-internal features
|
||||
- Provider trait is the abstraction boundary — new LLM backends implement it
|
||||
- Permissions are checked in `agent::execute_with_permission` before any tool runs
|
||||
- SLUG.md is never stored in `self.messages` — rebuilt from disk every turn
|
||||
|
||||
## Completed Features
|
||||
|
||||
- OpenAI-compatible provider with streaming SSE
|
||||
- 6 core tools: bash, read, write, edit, glob, grep
|
||||
- Agent loop with tool use
|
||||
- Permission system: ask / yolo / sandbox / allowEdits + glob patterns in `~/.slug/settings.json`
|
||||
- SLUG.md hierarchy (global/project/rules/local)
|
||||
- Session persistence: `--continue`, `--resume`, `--fork-session`
|
||||
- Hook system: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd
|
||||
- Compaction: ToolResultTrim → Truncate, `/compact` command
|
||||
|
||||
## Remaining Work (Prioritized)
|
||||
|
||||
### High
|
||||
- Proper ratatui TUI — markdown rendering, scrollback, syntax highlighting
|
||||
- Retry system — exponential backoff + model fallback on repeated failures
|
||||
- Clean interrupt — Escape aborts active stream without losing context
|
||||
- Wire PreToolUse/PostToolUse hooks into agent tool execution
|
||||
- Concurrent reads / serial writes (tool batching)
|
||||
|
||||
### Medium
|
||||
- Anthropic API provider (different format from OpenAI)
|
||||
- Google Gemini provider
|
||||
- MCP client support
|
||||
- Subagent parallelism (fork/teammate/worktree models)
|
||||
|
||||
### Low
|
||||
- Computer use integration
|
||||
- Session search
|
||||
- Plugin system beyond hooks
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
cargo build
|
||||
./target/debug/slug --help
|
||||
|
||||
# Connect to local vLLM
|
||||
slug -e http://localhost:8000/v1 -m qwen2.5-coder
|
||||
|
||||
# Connect to OpenAI
|
||||
slug -e https://api.openai.com/v1 -k $OPENAI_API_KEY -m gpt-4o
|
||||
|
||||
# Resume last session
|
||||
slug --continue
|
||||
|
||||
# Sandbox mode (auto-approve everything in current dir)
|
||||
slug --sandbox .
|
||||
|
||||
# Skip all permissions
|
||||
slug --yolo
|
||||
```
|
||||
src/
|
||||
├── main.rs CLI entry (clap). Wires all modules. Handles session flags.
|
||||
├── config/mod.rs Config struct + PermissionMode enum. TOML + CLI + env.
|
||||
├── provider/
|
||||
│ ├── mod.rs Provider trait + OpenAIProvider (SSE streaming impl)
|
||||
│ └── types.rs ChatMessage, Role, ToolCall, StreamEvent, StreamChunk
|
||||
├── agent/mod.rs Core loop: user input → LLM stream → tool exec → repeat
|
||||
├── tools/
|
||||
│ ├── mod.rs Tool trait + ToolRegistry (HashMap<name, Box<dyn Tool>>)
|
||||
│ ├── bash.rs BashTool — runs via `bash -c`, captures stdout+stderr
|
||||
│ ├── read.rs ReadTool — reads file with line numbers, offset+limit
|
||||
│ ├── write.rs WriteTool — writes file, creates parent dirs
|
||||
│ ├── edit.rs EditTool — exact string replacement, enforces uniqueness
|
||||
│ ├── glob.rs GlobTool — globwalk, max_depth 20, sorted output
|
||||
│ └── grep.rs GrepTool — prefers rg, falls back to grep, 250 line cap
|
||||
├── permissions/mod.rs PermissionHandler. Glob allow/deny. ask/yolo/sandbox/allowEdits.
|
||||
├── slugmd/mod.rs Loads SLUG.md hierarchy every turn, 40K char budget.
|
||||
├── session/mod.rs JSONL persistence at ~/.slug/sessions/. SessionManager.
|
||||
├── hooks/mod.rs HookManager. 5 events, command+prompt types. .slug/hooks.json.
|
||||
├── compact/mod.rs Compactor. ToolResultTrim→Truncate. /compact command.
|
||||
└── tui/mod.rs Interactive REPL. Wires agent+session+hooks. Slash commands.
|
||||
```
|
||||
|
||||
## Settings
|
||||
---
|
||||
|
||||
Config file: `~/.slug/config.toml`
|
||||
## Key Types
|
||||
|
||||
```toml
|
||||
endpoint = "http://localhost:8000/v1"
|
||||
model = "qwen2.5-coder-32b"
|
||||
max_tokens = 4096
|
||||
permission_mode = { type = "ask" }
|
||||
### provider/types.rs
|
||||
```rust
|
||||
ChatMessage { role: Role, content: Option<String>, tool_calls: Option<Vec<ToolCall>>, tool_call_id: Option<String> }
|
||||
// Constructors: ChatMessage::system(&str), ::user(&str), ::assistant(Option<String>, Option<Vec<ToolCall>>), ::tool_result(&str, &str)
|
||||
|
||||
Role: System | User | Assistant | Tool // serde: lowercase
|
||||
|
||||
ToolCall { id: String, call_type: String, function: FunctionCall }
|
||||
FunctionCall { name: String, arguments: String } // arguments is JSON string
|
||||
|
||||
StreamEvent: Text(String) | ToolCallDelta(ToolCallDelta) | Finish | Done
|
||||
ToolCallDelta { index: usize, id: Option<String>, name: Option<String>, arguments_delta: Option<String> }
|
||||
```
|
||||
|
||||
Permission rules: `~/.slug/settings.json` or `.slug/settings.json`
|
||||
### provider/mod.rs
|
||||
```rust
|
||||
trait Provider: Send + Sync {
|
||||
fn stream_chat(&self, messages: &[ChatMessage], tools: &[ToolDefinition])
|
||||
-> Pin<Box<dyn Stream<Item = Result<StreamEvent>> + Send + '_>>;
|
||||
}
|
||||
// OpenAIProvider implements Provider. POST /chat/completions with stream:true.
|
||||
// SSE parsing: buffer bytes → split on \n → strip "data: " prefix → parse StreamChunk.
|
||||
// Tool calls assembled via ToolCallAccumulator keyed by delta.index.
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": {
|
||||
"allow": ["Bash(cargo *)", "Bash(git *)", "Edit(src/**)", "Write(src/**)"],
|
||||
"deny": ["Bash(rm -rf *)", "Bash(sudo *)"]
|
||||
### tools/mod.rs
|
||||
```rust
|
||||
trait Tool: Send + Sync {
|
||||
fn definition(&self) -> ToolDefinition;
|
||||
fn execute(&self, args: &Value) -> Result<String>;
|
||||
}
|
||||
ToolDefinition { name: String, description: String, parameters: Value } // parameters is JSON Schema
|
||||
// ToolRegistry: HashMap<String, Box<dyn Tool>>. execute() returns Result<String> — errors become tool result strings in agent.
|
||||
```
|
||||
|
||||
### config/mod.rs
|
||||
```rust
|
||||
Config { endpoint, api_key, model, system_prompt, max_tokens: u32, temperature: Option<f32>, max_tool_rounds: usize, permission_mode: PermissionMode }
|
||||
// Defaults: endpoint="http://localhost:8000/v1", model="default", max_tokens=4096, max_tool_rounds=50
|
||||
// Load order: TOML file → with_cli_overrides()
|
||||
|
||||
PermissionMode: Ask | Yolo | AllowEdits | Sandbox(String)
|
||||
// serde: { "type": "ask" } / { "type": "yolo" } / { "type": "allowedits" } / { "type": "sandbox", ... }
|
||||
```
|
||||
|
||||
### permissions/mod.rs
|
||||
```rust
|
||||
PermissionRequest: Bash { command: &str } | FileWrite { path: &str } | FileEdit { path: &str }
|
||||
// check() order: deny rules → allow rules → mode fallback
|
||||
// Glob patterns: "Bash(npm *)", "Edit(src/**)", "Write(tests/**)"
|
||||
// Settings loaded from ~/.slug/settings.json then .slug/settings.json (merged, project extends global)
|
||||
// glob_match(pattern, text): ** matches across /, * does not
|
||||
```
|
||||
|
||||
### session/mod.rs
|
||||
```rust
|
||||
SessionManager { sessions_dir: PathBuf }
|
||||
Session { session_id: String, messages: Vec<ChatMessage> }
|
||||
SessionMeta { session_id, created_at, last_used_at, working_directory, model, summary }
|
||||
// Storage: ~/.slug/sessions/{id}.jsonl (one ChatMessage per line) + {id}.meta.json
|
||||
// Methods: create_session(), load_session(&str), save_message(&str, &ChatMessage), list_sessions(), get_latest_session(), fork_session(&str)
|
||||
```
|
||||
|
||||
### hooks/mod.rs
|
||||
```rust
|
||||
HookEvent: PreToolUse { tool_name, args } | PostToolUse { tool_name, args, result } | UserPromptSubmit { prompt } | SessionStart | SessionEnd
|
||||
HookAction: Command { command: String } | Prompt { content: String }
|
||||
HookResult { additional_context: Option<String>, blocked: bool, block_reason: Option<String> }
|
||||
// HookManager::new() loads ~/.slug/settings.json + .slug/hooks.json
|
||||
// fire(&HookEvent) → HookResult. Command hooks: sh -c, 30s timeout, non-zero = blocked.
|
||||
// Config: { "hooks": [{ "event": "PreToolUse", "tool_filter": "bash", "action": { "type": "command", "command": "..." } }] }
|
||||
```
|
||||
|
||||
### compact/mod.rs
|
||||
```rust
|
||||
CompactionStrategy: Full | Truncate | ToolResultTrim
|
||||
Compactor { max_context_tokens: usize } // default 200_000
|
||||
// estimate_tokens(text) = chars / 4
|
||||
// needs_compaction() = estimated > 80% of max
|
||||
// ToolResultTrim: replace Tool role messages older than last 5 with "[result truncated — N chars]"
|
||||
// Truncate: keep system prompt + last 10 user-turn groups
|
||||
// extract_session_memory(): structured summary of task, files, errors, decisions
|
||||
```
|
||||
|
||||
### slugmd/mod.rs
|
||||
```rust
|
||||
load_slug_context() -> String
|
||||
// Load order (all optional, concatenated with headers):
|
||||
// 1. ~/.slug/SLUG.md "# Global Rules"
|
||||
// 2. ./SLUG.md "# Project Rules"
|
||||
// 3. .slug/rules/*.md "# Rule: {filename}" (sorted alphabetically)
|
||||
// 4. ./SLUG.local.md "# Local Notes (private)"
|
||||
// 40,000 char hard limit — truncates at line boundary with warning appended
|
||||
// Returns "" if no files exist (no-op path, no overhead)
|
||||
// NEVER stored in Agent.messages — reloaded from disk on every LLM call
|
||||
```
|
||||
|
||||
### agent/mod.rs
|
||||
```rust
|
||||
Agent { provider, tools, permissions, messages: Vec<ChatMessage>, max_rounds: usize }
|
||||
// new() → seeds messages with [ChatMessage::system(config.system_prompt)]
|
||||
// new_with_history() → prepends system prompt then extends with prior_messages
|
||||
// messages() → &[ChatMessage] (used for session saving)
|
||||
// stream_turn(input, on_text) → streams LLM response, executes tools, loops
|
||||
// run_once(prompt) → non-interactive, returns final text
|
||||
// compact() → ToolResultTrim first, then Truncate if still over threshold
|
||||
// messages_with_slug_context() → [slug_context_system_msg, ...self.messages] (rebuilt each call)
|
||||
// execute_with_permission(name, args) → checks PermissionHandler before calling ToolRegistry
|
||||
// Tool execution: read/glob/grep = always allowed. bash/write/edit = permission checked.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flow
|
||||
|
||||
```
|
||||
User input
|
||||
→ tui::run()
|
||||
→ hook_mgr.fire(UserPromptSubmit) // can block or inject context
|
||||
→ agent.stream_turn(input, on_text)
|
||||
→ messages.push(ChatMessage::user(input))
|
||||
→ messages_with_slug_context() // fresh SLUG.md prepended every call
|
||||
→ provider.stream_chat(messages, tool_defs)
|
||||
→ POST /v1/chat/completions (stream: true)
|
||||
→ SSE parse → yield StreamEvent::{Text, ToolCallDelta, Finish, Done}
|
||||
→ on_text(chunk) called for each Text event // prints to stdout in TUI
|
||||
→ ToolCallAccumulators assembled from deltas
|
||||
→ if tool_calls not empty:
|
||||
→ messages.push(assistant msg with tool_calls)
|
||||
→ for each tool_call:
|
||||
→ execute_with_permission(name, args)
|
||||
→ permissions.check(request) // deny→allow→mode
|
||||
→ tools.execute(name, args) // ToolRegistry dispatch
|
||||
→ messages.push(tool_result)
|
||||
→ loop (up to max_tool_rounds)
|
||||
→ if no tool_calls: messages.push(assistant msg), return
|
||||
→ session_mgr.save_message() for each new message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Coding Standards
|
||||
|
||||
### Error Handling
|
||||
- Use `?` for propagation everywhere. No `unwrap()` outside tests.
|
||||
- Return `anyhow::Result<T>` from all fallible functions.
|
||||
- Tool execution errors are caught and returned as the tool result string — never propagate to agent loop.
|
||||
- Use `anyhow::bail!("message")` for early returns with context.
|
||||
- Use `anyhow::anyhow!("message")` to construct errors.
|
||||
|
||||
### Async
|
||||
- All async code uses `tokio`. Runtime is `#[tokio::main]` in main.rs.
|
||||
- Streams use `async_stream::stream!` macro with explicit `yield Ok(...)` / `yield Err(...)`.
|
||||
- Never `yield Err` inside `async_stream` using `?` — yield the error explicitly and return.
|
||||
- Provider `stream_chat` returns `Pin<Box<dyn Stream<Item = Result<StreamEvent>> + Send + '_>>`.
|
||||
|
||||
### Serialization
|
||||
- All wire types (ChatMessage, ToolCall, SessionMeta) derive `Serialize + Deserialize`.
|
||||
- `skip_serializing_if = "Option::is_none"` on optional fields to keep JSON clean.
|
||||
- Tool arguments arrive as `serde_json::Value` — extract with `.as_str()`, `.as_u64()` etc, never index directly.
|
||||
- Config uses `#[serde(tag = "type", rename_all = "lowercase")]` for PermissionMode enum.
|
||||
|
||||
### Adding a New Tool
|
||||
1. Create `src/tools/{name}.rs` implementing `Tool` trait.
|
||||
2. Add `mod {name};` to `src/tools/mod.rs`.
|
||||
3. Register: `registry.register(Box::new({name}::{Name}Tool));` in `ToolRegistry::new()`.
|
||||
4. Permission: if mutating (writes files, runs code), add a `PermissionRequest` variant and check in `agent::execute_with_permission`.
|
||||
5. Write unit tests in the same file under `#[cfg(test)]`.
|
||||
|
||||
### Adding a New Provider
|
||||
1. Create `src/provider/{name}.rs` implementing `Provider` trait.
|
||||
2. `pub use` from `src/provider/mod.rs`.
|
||||
3. Wire into `main.rs` behind a config flag or CLI arg.
|
||||
4. Must return `Pin<Box<dyn Stream<Item = Result<StreamEvent>> + Send + '_>>`.
|
||||
5. Normalize provider-specific formats to `StreamEvent` — the agent loop is provider-agnostic.
|
||||
|
||||
---
|
||||
|
||||
## Testing Standards
|
||||
|
||||
Testing is **non-negotiable**. Every module must have tests. Agents: do not consider a task complete without writing tests.
|
||||
|
||||
### Required Tests Per Module
|
||||
- **Tools**: Test each tool with valid args, missing args, and error cases (file not found, bad pattern, etc.)
|
||||
- **Provider**: Test `build_request_body` with and without tools, with and without temperature.
|
||||
- **Permissions**: Test each PermissionMode, glob pattern matching, deny overrides allow, sandbox path checking.
|
||||
- **Session**: Test create/save/load round-trip, fork copies messages, list_sessions returns sorted.
|
||||
- **Hooks**: Test command hook execution, blocked result on non-zero exit, prompt hook injection.
|
||||
- **Compact**: Test ToolResultTrim preserves last 5, Truncate preserves system prompt + last 10 groups, estimate_tokens.
|
||||
- **Slugmd**: Test 40K truncation, missing files are skipped, section headers are present.
|
||||
|
||||
### Test Patterns
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir; // use tempfile crate for file-based tests
|
||||
|
||||
#[test]
|
||||
fn test_edit_tool_not_found() {
|
||||
let tool = EditTool;
|
||||
let args = serde_json::json!({ "file_path": "/nonexistent", "old_string": "x", "new_string": "y" });
|
||||
assert!(tool.execute(&args).is_err());
|
||||
}
|
||||
}
|
||||
```
|
||||
- Use `tempfile::TempDir` for any test touching the filesystem.
|
||||
- Test tool errors produce `Err`, not panics.
|
||||
- Test permission glob matching exhaustively — it's a custom implementation.
|
||||
- Integration tests go in `tests/` at crate root.
|
||||
|
||||
---
|
||||
|
||||
## Documentation Standards
|
||||
|
||||
Every public item must have a doc comment. Agents: do not submit code without doc comments on public API.
|
||||
|
||||
```rust
|
||||
/// One-line summary.
|
||||
///
|
||||
/// Longer explanation if the behavior is non-obvious.
|
||||
/// Include what errors are returned and under what conditions.
|
||||
pub fn load_slug_context() -> String { ... }
|
||||
```
|
||||
|
||||
- `///` for public items (rendered by rustdoc).
|
||||
- `//` for implementation comments explaining *why*, not *what*.
|
||||
- Mark unfinished areas with `// TODO(feature): description` not just `// TODO`.
|
||||
- Module-level `//! ...` doc comments required for every `mod.rs`.
|
||||
|
||||
---
|
||||
|
||||
## CLI Reference
|
||||
|
||||
```
|
||||
slug [OPTIONS]
|
||||
|
||||
-e, --endpoint <URL> LLM API base URL [env: SLUG_ENDPOINT]
|
||||
-k, --api-key <KEY> API key [env: SLUG_API_KEY]
|
||||
-m, --model <NAME> Model name [env: SLUG_MODEL]
|
||||
-p, --prompt <TEXT> Non-interactive single prompt
|
||||
-c, --config <PATH> Config file path
|
||||
--system-prompt <TEXT> Override system prompt
|
||||
--yolo Skip all permission prompts
|
||||
--sandbox <DIR> Auto-approve ops within directory
|
||||
--allow-edits Auto-approve file edits in cwd
|
||||
--continue Resume most recent session
|
||||
--resume <ID> Resume specific session
|
||||
--fork-session <ID> Fork session into new branch
|
||||
```
|
||||
|
||||
Config file: `~/.slug/config.toml`
|
||||
Permission rules: `~/.slug/settings.json` or `.slug/settings.json`
|
||||
Hooks: `.slug/hooks.json`
|
||||
SLUG.md files: `~/.slug/SLUG.md`, `./SLUG.md`, `.slug/rules/*.md`, `./SLUG.local.md`
|
||||
Sessions: `~/.slug/sessions/{id}.jsonl`
|
||||
|
||||
---
|
||||
|
||||
## What's Not Done Yet (Remaining Work)
|
||||
|
||||
**High priority:**
|
||||
- Subagent parallelism — fork (shared cache), teammate (file mailbox), worktree (isolated branch)
|
||||
- Ratatui TUI — proper terminal UI with markdown rendering, scrollback, syntax highlighting
|
||||
- Retry with exponential backoff — 10 retries, jitter, model fallback on repeated 5xx
|
||||
- Stream interruption — Escape key aborts active stream cleanly, context preserved
|
||||
- Wire PreToolUse/PostToolUse hooks into agent::execute_with_permission
|
||||
- Concurrent tool execution — read/glob/grep run in parallel, bash/write/edit serial
|
||||
|
||||
**Medium priority:**
|
||||
- MCP client — connect to MCP servers, deferred tool loading
|
||||
- Computer use (screenshot, click, type)
|
||||
- Auto-compact trigger when context approaches limit
|
||||
|
||||
**Low priority:**
|
||||
- Session search / conversation history browser
|
||||
- Anthropic provider — `/v1/messages` format, different streaming protocol
|
||||
- Google Gemini provider
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns — Never Do These
|
||||
|
||||
- No `unwrap()` or `expect()` in non-test code.
|
||||
- Never store SLUG.md content in `Agent.messages` — it must be reloaded every turn.
|
||||
- Never add analytics, telemetry, or tracking of any kind.
|
||||
- Never add Anthropic-internal features (feature flags, bridge mode, CCR, Kairos, etc.).
|
||||
- Do not add backwards-compat shims — just change the code.
|
||||
- Do not add error handling for impossible cases — trust internal invariants.
|
||||
- Do not add abstractions for one-time use — three similar lines beat a premature helper.
|
||||
- Tool errors should return `Err(...)` — the agent catches them and turns them into tool result strings. Don't swallow errors silently.
|
||||
- Permission checks must happen in `agent::execute_with_permission` — tools themselves are unaware of permissions.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue