docs: update README with full config reference, remove completed roadmap

Expands README with complete config.yaml reference, CLI options table,
skills documentation, and updated command list. Removes the old roadmap
(all phases complete). Updates tweaks.md with current design notes.
Adds .sneakycode/ to .gitignore and includes superpowers design specs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 20:05:16 -05:00
parent 133bcbda57
commit 4496fce354
6 changed files with 2116 additions and 167 deletions

3
.gitignore vendored
View File

@@ -34,3 +34,6 @@ htmlcov/
# Worktrees # Worktrees
.worktrees/ .worktrees/
# SneakyCode local data
.sneakycode/

122
README.md
View File

@@ -26,31 +26,79 @@ pip install -e ".[dev]"
## Configuration ## Configuration
Edit `config/config.yaml` to configure the agent. Key settings: Edit `config/config.yaml` to configure the agent. The full configuration reference:
```yaml ```yaml
llm: llm:
model: "qwen3.5:latest" # Ollama model name model: "qwen3.5:latest" # Ollama model name
endpoint: "http://localhost:11434" # Ollama endpoint endpoint: "http://localhost:11434" # Ollama endpoint
max_retries: 3 # Retry attempts on transient errors api_path: "/v1/chat/completions" # API endpoint path
retry_backoff_base: 1.0 # Exponential backoff base (seconds) temperature: 0.1 # Sampling temperature
max_tokens: 4096 # Maximum tokens in LLM response
timeout: 120 # Request timeout in seconds
max_retries: 3 # Retry attempts on transient errors
retry_backoff_base: 1.0 # Exponential backoff base (seconds)
retry_backoff_max: 30.0 # Maximum backoff seconds
agent: agent:
max_iterations: 25 # Max tool-call iterations per turn max_iterations: 25 # Max tool-call iterations per turn
max_conversation_tokens: 32000 # Token budget for conversation max_conversation_tokens: 32000 # Token budget for conversation
workspace_root: "." # Project directory for file operations workspace_root: "." # Project directory for file operations
truncation_keep_recent: 10 # Messages preserved during truncation truncation_keep_recent: 10 # Messages preserved during truncation
truncation_threshold: 0.85 # Budget fraction that triggers truncation truncation_threshold: 0.85 # Budget fraction that triggers truncation
session: session:
auto_save: true # Save session after each turn session_dir: ".sneakycode/sessions" # Directory for session files
max_session_age_hours: 72 # Auto-cleanup old sessions auto_save: true # Save session after each turn
offer_resume: true # Offer to resume on startup max_session_age_hours: 72 # Auto-cleanup old sessions
offer_resume: true # Offer to resume on startup
permissions: permissions:
auto_approve: [read_file, list_dir, grep_files, find_files, finish] auto_approve: [read_file, list_dir, grep_files, find_files, finish]
prompt_user: [write_file, delete_file, run_command, str_replace, patch_apply, make_dir] prompt_user: [write_file, delete_file, run_command, str_replace, patch_apply, make_dir]
deny: [] deny: []
tools:
shell:
allowed_commands: # Commands the LLM may run
- git
- python
- pip
- pytest
- ruff
- ls
- cat
- head
- tail
- wc
- diff
- grep
- find
- echo
denied_commands: # Blocked commands
- rm -rf /
- sudo
- curl
- wget
max_output_bytes: 65536 # Max captured output size (bytes)
filesystem:
max_file_size_bytes: 1048576 # 1 MB — max file size for read/write
binary_detection: true # Detect and reject binary files
display:
show_tool_calls: true # Show tool call details in output
show_token_usage: true # Show token usage stats
stream_output: true # Stream LLM output to terminal
skills:
enabled: true # Enable the skills system
directories: # Directories to scan for skill files
- ".sneakycode/skills"
debug:
enabled: false # Enable debug logging
log_dir: ".sneakycode/logs" # Debug log directory
max_files: 10 # Max debug log files to retain
``` ```
Environment variable `SNEAKYCODE_CONFIG` can override the config file path. Environment variable `SNEAKYCODE_CONFIG` can override the config file path.
@@ -58,9 +106,12 @@ Environment variable `SNEAKYCODE_CONFIG` can override the config file path.
## Usage ## Usage
```bash ```bash
# Start the interactive REPL # Start the interactive TUI
sneakycode sneakycode
# Open a specific project directory
sneakycode /path/to/project
# Or run directly # Or run directly
python -m app.main python -m app.main
@@ -68,25 +119,38 @@ python -m app.main
sneakycode --config path/to/config.yaml --verbose --log-file sneakycode.log sneakycode --config path/to/config.yaml --verbose --log-file sneakycode.log
``` ```
### CLI Options
| Option | Description |
|--------------------------|--------------------------------------------------|
| `DIRECTORY` | Project directory to use as workspace root |
| `--config PATH` | Path to config YAML file (default: `config/config.yaml`) |
| `-v`, `--verbose` | Enable verbose (DEBUG) logging |
| `--log-file PATH` | Path to log file for persistent logging |
### REPL Commands ### REPL Commands
| Command | Description | | Command | Description |
|------------|--------------------------------------| |-------------------|----------------------------------------------------|
| `/quit` | Save session and exit | | `/help` | Show available commands |
| `/history` | Show conversation history | | `/quit` | Save session and exit (also `/exit`, `/bye`) |
| `/clear` | Clear conversation history | | `/history` | Show conversation history |
| `/save` | Manually save session | | `/clear` | Clear conversation history |
| `/session` | Show session info (messages, tokens) | | `/save` | Manually save session |
| `/session` | Show session info (messages, tokens, start time) |
| `/models` | List available Ollama models |
| `/models <name>` | Switch to a different model |
| `/skills` | List available skills |
### Session Persistence ### Session Persistence
Sessions are automatically saved after each agent turn and on exit. On startup, SneakyCode offers to resume the most recent session for the current workspace. Sessions are automatically saved after each agent turn and on exit. On startup, SneakyCode offers to resume the most recent session for the current workspace.
Session files are stored in `.sneakycode/sessions/` within the workspace root. Session files are stored in `.sneakycode/sessions/` within the workspace root (configurable via `session.session_dir`).
## Available Tools ## Available Tools
SneakyCode provides 11 tools across 5 categories. See [docs/tools.md](docs/tools.md) for the full reference. SneakyCode provides tools across 6 categories. See [docs/tools.md](docs/tools.md) for the full reference.
| Category | Tools | Permission | | Category | Tools | Permission |
|------------|-------------------------------------------------|---------------| |------------|-------------------------------------------------|---------------|
@@ -96,6 +160,17 @@ SneakyCode provides 11 tools across 5 categories. See [docs/tools.md](docs/tools
| Edit | `str_replace`, `patch_apply` | User confirm | | Edit | `str_replace`, `patch_apply` | User confirm |
| Shell | `run_command` | User confirm | | Shell | `run_command` | User confirm |
| Control | `finish` | Auto-approved | | Control | `finish` | Auto-approved |
| Skills | `load_skill` | Auto-approved |
The `load_skill` tool is available when `skills.enabled` is `true` in the config. It allows the LLM to load skill instructions from the configured skill directories.
## Skills
SneakyCode includes a skills system that lets you provide reusable instruction sets to the LLM. Skills are markdown files placed in `.sneakycode/skills/` (or any directory listed in `skills.directories`).
Skills are auto-discovered on startup. The LLM can load them via the `load_skill` tool, and you can list available skills with the `/skills` command.
To create a skill, add a `.md` file to your skills directory with a descriptive filename (e.g., `refactoring.md`). The file content is injected into the conversation when the skill is loaded.
## Development ## Development
@@ -121,6 +196,7 @@ app/
├── models/ # Pydantic config and message schemas ├── models/ # Pydantic config and message schemas
├── services/ # LLM client, streaming, permissions, session persistence ├── services/ # LLM client, streaming, permissions, session persistence
├── tools/ # Tool implementations (one file per group) ├── tools/ # Tool implementations (one file per group)
├── ui/ # Textual TUI application and widgets
└── utils/ # Logging, display, file helpers, token counter └── utils/ # Logging, display, file helpers, token counter
config/ config/
└── config.yaml # Application configuration └── config.yaml # Application configuration

View File

@@ -1,144 +0,0 @@
# SneakyCode Implementation Roadmap
A phased plan progressing from bare-bones foundation to full autonomous coding agent.
---
## Phase 1 — Foundation: Models, Config, and Utilities
Establish the data layer and shared infrastructure everything else builds on.
| File | Description |
|------|-------------|
| `app/models/config.py` | Pydantic v2 config model — load and validate `config/config.yaml` |
| `app/models/message.py` | Message schema (role, content, tool_calls) |
| `app/models/tool_call.py` | ToolCall and ToolResult schemas |
| `app/utils/logging.py` | Centralized logger with Rich handler |
| `app/utils/display.py` | Rich console output helpers (stub — expanded in Phase 2) |
| `app/utils/file_helpers.py` | Safe path resolution, binary detection, size guards |
| `app/utils/token_counter.py` | Approximate token usage tracking (character-based heuristic for v1) |
| `app/main.py` | Entrypoint stub — arg parsing, config load, Rich console setup |
**Exit criteria:** `python -m app.main --help` runs, config loads and validates, models can be instantiated and serialized.
---
## Phase 2 — TUI and Interactive Shell
Get a working interactive terminal before wiring up the LLM.
| File | Description |
|------|-------------|
| `app/main.py` | Rich-based interactive REPL loop — prompt for user input, display responses |
| `app/utils/display.py` | Formatted output for agent messages, tool calls, errors, token usage |
| `app/agent/context.py` | Session state and conversation history management |
**Exit criteria:** User can type messages into a styled REPL, see them echoed back with formatting, and conversation history is tracked in memory.
---
## Phase 3 — LLM Integration (Ollama)
Connect to the local LLM and stream responses into the TUI.
| File | Description |
|------|-------------|
| `app/services/llm.py` | Async httpx client wrapping Ollama's OpenAI-compatible `/v1/chat/completions` endpoint |
| `app/services/streaming.py` | SSE parsing, Rich live display, tool call extraction from accumulated stream |
**Integration:** Wire LLM into the REPL — user message goes to LLM, streamed response displays in real time.
**Exit criteria:** User can chat with the local model through the TUI with streamed output. Tool call JSON is parsed from the stream but not yet executed.
---
## Phase 4 — Tool Framework and Core Tools
Build the tool abstraction and implement safe, read-only tools first.
| File | Description |
|------|-------------|
| `app/tools/base.py` | `BaseTool` ABC and `ToolResult` dataclass |
| `app/tools/registry.py` | Tool registration, discovery, and JSON schema export for LLM system prompt |
| `app/services/permissions.py` | Two-tier approval gating (auto-approve reads; prompt for writes/deletes/shell) |
| `app/tools/filesystem.py` | `read_file`, `list_dir` |
| `app/tools/search.py` | `grep_files`, `find_files` |
**Exit criteria:** Tools register themselves, schemas export correctly for inclusion in the system prompt, read-only tools execute and return `ToolResult` objects. Permissions service gates execution.
---
## Phase 5 — Agent Loop (ReAct)
The core autonomy layer — reason, act, observe, repeat.
| File | Description |
|------|-------------|
| `app/agent/loop.py` | ReAct cycle: send conversation to LLM, parse tool calls, execute, feed results back, repeat |
**Key behaviors:**
- System prompt constructed with tool schemas from registry
- Permissions checks before each tool execution
- Loop termination on: plain-text response (no tool calls), explicit `finish` tool call, or `max_iterations` exceeded
**Exit criteria:** Agent can autonomously answer questions about the codebase by chaining `read_file`, `list_dir`, `grep_files`, and `find_files` tool calls in a multi-turn loop.
---
## Phase 6 — Write Tools and Shell
Unlock the agent's ability to modify code and run commands.
| File | Description |
|------|-------------|
| `app/tools/filesystem.py` | `write_file`, `make_dir`, `delete_file` (additions to existing module) |
| `app/tools/edit.py` | `str_replace` (unique-match required), `patch_apply` |
| `app/tools/shell.py` | `run_command` with command allow/deny lists and output truncation |
**All write/shell operations gated through permissions service.**
**Exit criteria:** Agent can autonomously create files, edit code via string replacement, and run shell commands — all with user approval for destructive operations.
---
## Phase 7 — Polish and Hardening
Production-readiness: error handling, resource limits, and documentation.
| Area | Description |
|------|-------------|
| Error handling | Recovery from malformed tool calls, LLM errors, network timeouts in agent loop |
| Token budget | Conversation truncation or summarization when approaching context limit |
| Graceful shutdown | Clean Ctrl+C handling, session state preservation |
| Testing | End-to-end integration tests (`tests/integration/`), unit tests (`tests/unit/`) |
| Documentation | `README.md` with setup and usage instructions, `docs/tools.md` tool reference |
**Exit criteria:** Agent handles edge cases gracefully, tests pass, and a new user can set up and use the project from the README alone.
---
## File Coverage
Every file from the project structure in CLAUDE.md is accounted for:
| File | Phase |
|------|-------|
| `app/main.py` | 1, 2 |
| `app/models/config.py` | 1 |
| `app/models/message.py` | 1 |
| `app/models/tool_call.py` | 1 |
| `app/utils/logging.py` | 1 |
| `app/utils/display.py` | 1, 2 |
| `app/utils/file_helpers.py` | 1 |
| `app/utils/token_counter.py` | 1 |
| `app/agent/context.py` | 2 |
| `app/services/llm.py` | 3 |
| `app/services/streaming.py` | 3 |
| `app/tools/base.py` | 4 |
| `app/tools/registry.py` | 4 |
| `app/services/permissions.py` | 4 |
| `app/tools/filesystem.py` | 4, 6 |
| `app/tools/search.py` | 4 |
| `app/agent/loop.py` | 5 |
| `app/tools/edit.py` | 6 |
| `app/tools/shell.py` | 6 |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,192 @@
# Textual TUI Redesign — Design Spec
## Overview
Replace the current sequential print-and-scroll terminal UI with a full persistent split-screen TUI using Textual. Input is pinned at the bottom, scrollable message history above, with a header showing app/model info and a footer showing token usage and iteration count.
## Layout
```
+------------------- Header --------------------+
| SneakyCode qwen2.5-coder:32b |
+-----------------------------------------------+
| |
| +--- You ---+ |
| | prompt | <- RichLog widget |
| +-----------+ (handles own scrolling) |
| |
| Thinking... |
| |
| +-- Assistant --+ |
| | response... | |
| +---------------+ |
| |
| > read_file README.md -- 148 lines, 5128 ch |
| > grep_files "pattern" -- 3 matches |
| |
+-----------------------------------------------+
| Tokens: ~1,511 / 32,000 | Iteration 5/25 | <- StatusBar
+-----------------------------------------------+
| > [input cursor] | <- Input widget
+-----------------------------------------------+
```
**Widget hierarchy (no VerticalScroll wrapper — RichLog handles its own scrolling):**
- `Header` — Textual built-in, title="SneakyCode", subtitle=model name
- `RichLog` (id="chat-log") — main scroll area, accepts Rich renderables via `.write()`
- `StreamingStatic` — persistent hidden `Static` widget, shown/hidden during streaming (avoids mount/unmount overhead)
- `StatusBar` — custom `Static` widget, 1 row, docked above Input
- `Input` — Textual built-in, pinned at bottom
## New Files
### `app/ui/app.py` — Textual App
SneakyCodeApp subclasses `textual.app.App`. Responsibilities:
- `compose()` yields: Header, RichLog(id="chat-log"), StreamingStatic(id="streaming"), StatusBar(id="status"), Input
- `on_input_submitted()` handler: reads input value, clears input, writes user panel to chat log, dispatches agent turn as a worker
- Agent turn runs via `run_worker()` (async worker, NOT threaded) so the UI stays responsive. Since the worker is async and on the event loop, widget methods can be called directly — no `call_from_thread()` needed.
- Slash commands (/quit, /history, /clear, /save, /session) parsed from input before dispatching to agent
- Holds references to config, SessionContext, AgentLoop (created in `on_mount`)
- Header subtitle set to model name from config
- `on_worker_state_changed()` handler: catches worker errors and writes error panels to RichLog
- Ctrl+C binding: cancels the running agent worker (does NOT quit the app). A second Ctrl+C or `/quit` exits.
### `app/ui/widgets.py` — Custom Widgets
**StatusBar** — A simple `Static` widget styled as a footer bar. Displays token usage and iteration count. Updated by the agent loop after each LLM step via `status_bar.update(renderable)`.
**StreamingStatic** — A `Static` widget that stays mounted but hidden. During streaming, it becomes visible and receives `update()` calls with partial content. When streaming ends, it is hidden and its content is cleared. This avoids the overhead of mounting/unmounting on every LLM response.
### `app/ui/styles.tcss` — Textual CSS
Layout rules:
- RichLog fills available height (fraction-based sizing, e.g. `height: 1fr`)
- StreamingStatic: `display: none` by default, shown during streaming
- StatusBar is 1 row, docked bottom above Input
- Input is 1 row, docked at very bottom
- Color scheme matches existing SNEAKYCODE_THEME (cyan for user, green for assistant, magenta for tools, dim for metadata)
## Modified Files
### `app/main.py`
- Remove `_run_repl()` async function entirely
- Remove `console.input()` usage
- `main()` creates config, runs preflight via `asyncio.run(_preflight(config))` (before Textual starts — this is fine, separate event loop), then instantiates and runs `SneakyCodeApp(config).run()`
- CLI arg parsing stays (--config, -v, --log-file)
- Session resume: `_offer_session_resume()` moves into `SneakyCodeApp.on_mount()` — instead of `console.input()`, push a modal screen asking "Resume previous session? [y/n]" with button/key handlers
- Auto-save: triggers after each agent turn completes (in the worker completion handler)
- SIGTERM handler: removed — Textual manages its own signal handling and shutdown lifecycle
### `app/services/streaming.py`
- Remove `from rich.live import Live` and `from rich.spinner import Spinner`
- `process_stream()` no longer creates a `Rich.Live` context
- Instead, accepts callback parameters:
- `on_content: Callable[[str], None]` — called with accumulated content on each content chunk
- `on_thinking: Callable[[], None]` — called once when first reasoning token arrives
- `on_done: Callable[[], None]` — called when streaming completes
- **Throttling:** Content callback fires at most every 100ms (track last update time, skip intermediate chunks). Final content always fires on stream end.
- Since the agent runs as an async worker (on the event loop), callbacks can directly call widget methods — no `call_from_thread()` needed.
- All accumulation and tool-call parsing logic stays identical
### `app/utils/display.py`
- All `print_*` functions become `render_*` functions that return Rich renderables:
- `render_user_message(content) -> Panel`
- `render_assistant_message(content) -> Panel`
- `render_tool_call(name, args) -> Text`
- `render_tool_result(name, output, is_error) -> Text`
- `render_iteration_header(iteration, max_iter) -> Text`
- `render_warning(message) -> Text`
- `render_error(message) -> Text`
- `print_banner()` removed — Header widget replaces it
- `print_token_usage()` becomes `render_token_usage() -> Text` for the StatusBar
- `print_history()` becomes `render_history() -> Table` — written to RichLog, may need width constraints for narrow terminals
- A `DisplayAdapter` class wraps a `RichLog` reference and provides `write_user_message()`, `write_tool_call()`, etc. methods that call `render_*` then `rich_log.write()`
### `app/agent/loop.py`
- `AgentLoop.__init__()` accepts a `DisplayAdapter` instead of calling `display.py` print functions directly
- All display calls route through the adapter: `self._display.write_tool_call(name, args)`, `self._display.write_iteration_header(i, max)`, etc.
- `_execute_tool_calls()` becomes `async def _execute_tool_calls()` to support async permission checks
- The loop logic (ReAct pattern, retry, truncation) is unchanged
### `app/services/permissions.py`
- `PermissionsService.check()` becomes `async def check()`
- Instead of `rich.prompt.Confirm.ask()` (blocking stdin read), it:
1. Creates an `asyncio.Event`
2. Posts a custom message to the app requesting a permission modal
3. The app pushes a modal screen with the permission question and approve/deny buttons
4. When the user responds, the modal sets the event and stores the result
5. `check()` awaits the event and reads the result
- Edge cases: dismiss without choosing = deny. Ctrl+C during modal = deny. Focus returns to Input after modal dismisses.
### `app/utils/logging.py`
- **Critical change:** The shared `console = Console()` instance will corrupt the Textual display since Textual takes exclusive terminal control
- When running under Textual: disable `RichHandler` (console handler), keep only the file handler
- Add a `setup_logging_for_tui()` function that reconfigures logging to file-only mode
- Called from `SneakyCodeApp.on_mount()` before any agent work begins
- The `console` object still exists but should not be used for output during TUI mode — all output goes through the DisplayAdapter
- Consider: `--log-file` becomes required (or auto-set to a default) when running in TUI mode, so logs are not lost
## Unchanged Files
- `app/services/llm.py` — HTTP client, SSE parsing untouched
- `app/agent/context.py` — session state untouched
- `app/models/*` — all data models untouched
- `app/tools/*` — all tool implementations untouched
- `app/utils/file_helpers.py` — path safety untouched
- `app/utils/token_counter.py` — token counting untouched
## Key Patterns
### Streaming in Textual
The agent loop runs as an async worker (on the event loop, NOT threaded). During streaming:
1. App shows `StreamingStatic` widget, writes "Thinking..." initially
2. Worker calls `StreamHandler.process_stream(chunks, on_content=..., on_thinking=..., on_done=...)`
3. `on_content` callback: updates `StreamingStatic` with `Panel(Markdown(partial_content), title="Assistant", border_style="green")` — throttled to ~100ms intervals
4. `on_done` callback: hides `StreamingStatic`, writes final content to `RichLog` via `DisplayAdapter`
Since the worker is async (not threaded), callbacks run on the event loop and can call widget methods directly.
### Permission Prompts
1. Agent loop (in async worker) calls `await permissions.check(operation, details)`
2. `check()` creates an `asyncio.Event` and posts `PermissionRequest` message to the app
3. App handles `PermissionRequest`: pushes a modal screen with the question, approve/deny buttons
4. Modal screen: on button press, stores result and sets the event
5. `check()` awaits the event, reads result, returns approved/denied
6. Focus management: Input loses focus when modal appears, regains focus when modal dismisses
7. Default on dismiss/Ctrl+C: deny
### Cancellation
- Ctrl+C (first press): cancels the running agent worker via `worker.cancel()`. The agent loop should check for cancellation between iterations.
- Ctrl+C (second press) or `/quit`: exits the app via `app.exit()`
## Dependencies
- Add `textual>=4.0.0` to pyproject.toml dependencies
## Verification
1. Run the app — header shows app name + model, no console corruption
2. Type a prompt — user panel appears in scroll area, input clears
3. During LLM streaming — assistant response types out live (throttled) in the scroll area
4. Thinking indicator shows during reasoning-only phases
5. Tool calls appear as compact lines in the scroll area
6. Footer shows token usage and iteration count, updating each step
7. Scroll area auto-scrolls to bottom on new content
8. /quit, /clear, /history commands work from the input
9. Permission prompts show as modal, approve/deny work, focus returns to input
10. Ctrl+C cancels running agent turn without quitting
11. Worker errors display as error panels in the scroll area
12. Logging goes to file only — no console corruption
13. Session resume works on startup via modal dialog

View File

@@ -1 +1,21 @@
Pressing up should cycle history like claude code. Pressing up should cycle history like claude code.
~~Remove the user's input from output "you" - it's not needed.~~ Brought back as a condensed one-liner (first line + line count for multi-line).
Smart shell auto-approve: auto-approve `run_command` when the base executable is in the `allowed_commands` list and the full command doesn't match any `denied_commands` prefix. Only prompt the user for commands whose base executable is unlisted. Currently all shell commands prompt regardless, which is tedious for safe read-only commands like `git branch` or `ls`. The allow/deny lists in `ShellToolConfig` already define what's safe — the permissions service just needs to be shell-aware.
Show a token count or some other display for when the model is "thinking" for a long period of time. I want a way for the user to know the model is working on it.
/models command to show models available and temporarly change models in the session
pass a directory to the tool so that it uses that directory as it's root for commands.
add a skills directory so we can prompt our own skills for the tool to use similar to Claude Code
need not only a session log, but also a log of what the llm is thinking and how it's working somehow. I need a way to see behind the curtain.
# Left of from Phase 7 of old roadmap - finish these first
- Permission modal auto-approves (TODO: proper modal dialog)
- Session resume auto-resumes (TODO: modal y/n)
- LLM client cleanup on unmount not yet wired
- No automated TUI tests (Textual's AppTest can be added later)