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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/target

/.tinyharness
/.tinyharness
/todo
31 changes: 31 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Contributing

TinyHarness is a Rust workspace with three crates. See the full guide at [docs/contributing.md](docs/contributing.md).

## Quick Start

```bash
git clone https://github.com/yourusername/TinyHarness.git
cd TinyHarness
cargo build --workspace
cargo test --workspace
```

## Verification Checklist

Before submitting a PR:

```bash
cargo fmt --all
cargo clippy --workspace -- -D warnings
cargo test --workspace
cargo build
```

## Docs

User-facing docs are in `docs/`. Developer docs (this file) are in `docs/contributing.md`. Enhancement tracking is in `todo/` (local only, not committed).

## License

MIT — see [LICENSE](LICENSE).
6 changes: 3 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = [".", "tinyharness-lib", "tinyharness-ui"]

[package]
name = "TinyHarness"
version = "0.1.1"
version = "0.1.2"
license = "MIT"
description = "tinyharness - ai coding harness"
edition = "2024"
Expand All @@ -13,8 +13,8 @@ name = "tinyharness"
path = "src/main.rs"

[dependencies]
tinyharness-lib = { version = "0.1.1", path = "tinyharness-lib" }
tinyharness-ui = { version = "0.1.1", path = "tinyharness-ui" }
tinyharness-lib = { version = "0.1.2", path = "tinyharness-lib" }
tinyharness-ui = { version = "0.1.2", path = "tinyharness-ui" }
clap = { version = "4.6.1", features = ["derive"] }
tokio = { version = "1.52.1", features = ["full"] }
serde = { version = "1.0.228", features = ["derive"] }
Expand Down
406 changes: 344 additions & 62 deletions README.md

Large diffs are not rendered by default.

152 changes: 67 additions & 85 deletions TINYHARNESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,111 +8,93 @@ Lightweight AI assistant framework in Rust with pluggable LLM providers (Ollama,
- Test: `cargo test --workspace`
- Lint: `cargo clippy --workspace -- -D warnings`
- Format check: `cargo fmt --all -- --check`
- Formating: `cargo fmt --all`
- Formatting: `cargo fmt --all`
- Install: `make install` (builds release + copies to `~/.local/bin`)
- Run: `cargo run` (Ollama default) or `cargo run -- --llama-cpp` / `--vllm`

## Workspace Structure

The project uses a Cargo workspace with two crates:

- **`tinyharness-lib`** — Core library crate (frontend-agnostic, no terminal I/O)
- **`tinyharness`** — Binary CLI crate (depends on `tinyharness-lib`)

### Library crate (`tinyharness-lib/`)

```
tinyharness-lib/src/
├── lib.rs Re-exports all public types
├── provider/ Provider trait + implementations (ollama, llama_cpp, vllm, openai_compat)
├── config/mod.rs Settings persistence (provider, model, mode, API key, denied commands)
├── mode.rs AgentMode enum (casual/planning/agent/research) with system prompts
├── context.rs WorkspaceContext — auto-detected project metadata + TINYHARNESS.md loading
├── session.rs JSONL session persistence with UUIDs
├── token.rs Token estimation and context window calculations
└── tools/ Tool implementations (ls, read, write, edit, grep, run, glob, web_search, etc.)
```

### Binary crate (`src/`)

```
src/
├── main.rs Entry point, CLI parsing, provider creation
├── agent.rs Main interaction loop, tool call dispatch, confirmation UI, context load warning
├── style.rs ANSI color constants
├── commands/ Slash command handlers
│ ├── mod.rs CommandDispatcher — parse and dispatch /commands
│ ├── command.rs /command — manage safe/denied commands
│ ├── compact.rs /compact — cascading summarization for long sessions
│ ├── settings.rs /settings — configuration display (supports `all` variant)
│ └── ... Other commands (session, model, apikey, etc.)
└── ui/ Terminal UI helpers (confirmation prompts, input, diffs)
```
Three crates in a Cargo workspace:

- **`tinyharness-lib`** — Core library: providers, tools, sessions, context, skills, tokens. No terminal I/O.
- **`tinyharness-ui`** — UI library: ANSI output, confirmation prompts, diff display, command input.
- **`TinyHarness`** — Binary CLI: agent loop, slash commands, tool dispatch, setup.

### Key `tinyharness-lib` modules

- `provider/` — Provider trait, `OllamaProvider` (raw SSE, Gemini signatures), `LlamaCppProvider`/`VllmProvider` (shared OpenAI-compat internals)
- `tools/` — 15 tools (ls, read, write, edit, grep, glob, run, web_search, web_fetch, switch_mode, question, auto_compact, invoke_skill, screenshot), registration in `register_defaults()`, mode-based filtering
- `session.rs` — JSONL persistence, auto-save every 5 messages
- `context.rs` — Workspace metadata + instruction file discovery (TINYHARNESS.md → .tinyharness.md → AGENTS.md → CLAUDE.md)
- `skill.rs` — Skill discovery from `~/.config/tinyharness/skills/` and `.tinyharness/skills/`
- `mode.rs` — Agent modes with `.md` system prompts
- `config/mod.rs` — SettingsStore, ProviderKind, OllamaThinkType

### Binary crate structure

- `src/agent/` — Agent loop, tool execution, safety checks, display, multi-line input, provider setup
- `src/commands/` — 22+ slash commands (mode, model, sessions, compact, init, context, files, image, skill, settings, help, etc.), `CommandRegistry` and `async_command!` macro

## Code Conventions

- Rust edition 2024
- Core logic lives in `tinyharness-lib` — no terminal I/O, no ANSI codes, no rustyline
- CLI-specific code (terminal output, interactive prompts) stays in the binary crate
- Binary crate imports from `tinyharness_lib` for provider, config, tools, session, etc.
- Tools registered in `tinyharness-lib/src/tools/mod.rs` via `ToolManager::register_defaults()`
- Tool definitions live in `tinyharness-lib/src/tools/<name>.rs` — each exposes a `*_tool_entry()` function returning a `Tool`
- Providers implement the `Provider` trait in `tinyharness-lib/src/provider/mod.rs`
- Settings persisted as JSON in `~/.config/tinyharness/settings.json`
- Sessions stored as JSONL in `~/.local/share/tinyharness/sessions/`
- Core logic (`tinyharness-lib`) must not use terminal I/O, ANSI codes, or rustyline
- Use `serde` + `schemars` for serialization and tool schema generation
- Minimize dependencies — prefer `std` and lightweight crates over heavy ones; avoid adding new deps when the same can be achieved with what's already in the workspace
- Prefer manual `Pin<Box<dyn Future>>` over `async-trait` to keep the dependency tree small
- Error handling: `Result<T, String>` for user-facing errors, `Result<T, Box<dyn Error>>` for internal
- Prefer `Pin<Box<dyn Future>>` over `async-trait` to keep dependency tree small
- Error handling: `Result<T, String>` for user-facing, `Result<T, Box<dyn Error>>` for internal
- Minimize dependencies; avoid adding new crates when existing ones suffice
- `#[macro_export]` macros (`extract_args!`) live at `tinyharness_lib` root, not inside `tools`
- Tool categories: `ReadOnly` (auto-executed), `Destructive` (requires confirmation), `Signal` (handled specially by agent loop)

## Architecture

Key flow: `main.rs` → `create_provider()` → `run_agent_loop()` (in `agent.rs`) → streams responses from provider → dispatches tool calls → confirms with user for sensitive tools (write/edit/run/switch_mode) → appends results.
1. `main.rs` → parse CLI, create provider, health check, auto-select model, collect workspace context, initialize prompts, register tools, load/create session, build command registry, enter `run_agent_loop()`
2. Agent loop: read input (or `--prompt`), dispatch slash commands, send messages to provider, stream response, handle tool calls
3. Signal tools (`switch_mode`, `question`, `auto_compact`, `invoke_skill`) bypass generic tool execution and are handled inline
4. Destructive tools prompt for confirmation (except `run` which cannot be auto-accepted); ReadOnly tools run immediately
5. Tool results are batched into a single `Role::Tool` message, appended to conversation
6. Auto-save session every 5 messages; flush on mode switch, session switch, exit

## Agent Modes

| Mode | Tools | Purpose |
|------|-------|---------|
| casual | None | Pure chat, no filesystem access |
| planning | read-only (ls, read, grep, glob, web_search) + switch_mode, question | Analyze & plan, then escalate to agent |
| agent | All tools | Full development access |
| research | read-only + web_search, web_fetch + switch_mode, question | Web research, then escalate |
| Mode | Tools | Purpose |
|----------|-------|---------|
| casual | web_search, web_fetch | Chat with web access |
| planning | ReadOnly + Signal tools | Analyze, plan, escalate to agent |
| agent | All 15 tools | Full development access |
| research | Same as planning (research-focused prompt) | Web research, then escalate |

## Testing

- Framework: built-in `#[test]` + `cargo test --workspace`
- Use `tempfile` crate in dev-dependencies for test isolation — tool tests must not write to real filesystem
- `cargo test --workspace` runs all tests
- `tinyharness-lib` has good coverage; `tinyharness-ui` and binary crate have limited coverage (see `todo/01-testing-gaps.md`)
- Use `tempfile` for test isolation; tool tests must not touch the real filesystem
- Run specific test: `cargo test <test_name>`
- Library tests: `cargo test -p tinyharness-lib`
- Binary tests: `cargo test -p TinyHarness`

## Important Rules

- Never modify `src/style.rs` ANSI codes without checking terminal compatibility
- `switch_mode` and `question` tools are handled specially in `agent.rs` — they bypass the generic tool execution path
- Confirmation for `run` tool cannot be auto-accepted even with 'a' (auto-accept) — only write/edit can
- System prompt is refreshed after mode switches, file pinning (/add, /drop), and /refresh
- Session auto-saves every 5 messages
- When adding new modules to `tinyharness-lib`, update `lib.rs` re-exports
- Command safety: `is_safe_command()` in `src/agent.rs` checks prefixes and deny list; shell redirections (`2>&1`, `>/dev/null`) are stripped before matching
- Context management: `/compact` uses cascading for sessions >60% of context window; load-time warnings at 70%/90% thresholds

## Known Gotchas

- All providers now run a health check on startup; Ollama is included
- If the saved model is unavailable, auto-select picks the first available model with a warning
- `rustyline` history stored in `~/.local/share/tinyharness/history.txt`
- Web search requires an Ollama API key set via `/apikey`
- `#[macro_export]` macros (`define_tool!`, `extract_args!`, `register_tools!`) are exported at the crate root of `tinyharness_lib`, not in the `tools` module
- Shell commands with redirections (`2>&1`, `>/dev/null`) are auto-accepted if the base command is safe — redirections are stripped before prefix matching
- Cascading compaction may produce less coherent summaries than single-pass (trade-off for handling very long sessions)
- Context load warnings are estimates based on token counting; actual usage may vary by model
- Ctrl+C interrupts the current LLM generation; a second Ctrl+C exits the process immediately
- Run per crate: `cargo test -p tinyharness-lib`, `cargo test -p TinyHarness`, `cargo test -p tinyharness-ui`

## Important Rules & Gotchas

- **Provider startup**: All providers run a health check (Ollama calls `list_local_models`). If saved model is unavailable, auto-select picks the first available with a warning.
- **Ollama specifics**: Own raw SSE parser (not ollama-rs streaming) to handle native and OpenAI-compatible formats; captures Gemini `thought_signature` from tool responses and re-injects them; fixes serialization quirks (lowercases tool type, injects `name` in tool results).
- **System prompts**: Assembled from `header.md` + `<mode>.md` for Agent/Planning/Research; Casual is self-contained. Prompts are refreshed on mode switch, file pinning changes, skill activation, and `/refresh`.
- **Command safety** (`src/agent/safety.rs`): Prefix matching with word boundaries, deny list priority, strips redirections before matching; rejects `;`, `&`, `|`, `$()`, backticks, newlines. Redirections like `2>&1` are auto-accepted if base command is safe.
- **Confirmation**: `run` tool cannot be auto-accepted even with 'a' (auto-accept mode); only `write` and `edit` can.
- **Compaction**: `/compact` uses single-pass for ≤200 intermediate messages, cascading (chunk+merge) for larger sessions.
- **Context warnings**: Load warnings at 70%/90% thresholds based on last known token count (estimation).
- **Session files**: JSONL (metadata line first, then message lines); malformed lines silently skipped on load; stored in `~/.local/share/tinyharness/sessions/`.
- **Web tools**: `web_search` and `web_fetch` use `https://ollama.com/api/web_search` and require an Ollama API key set via `/apikey`.
- **Ctrl+C**: Interrupts current LLM generation; second Ctrl+C exits immediately.
- **Configuration**: Set via `--config` (interactive setup), stored as JSON in `~/.config/tinyharness/settings.json`. Persistent prompts are seeded from embedded defaults into `~/.config/tinyharness/prompts/`.
- **Image attachments**: Base64 data URIs, used by multimodal models; set via `/image`.
- **`async_command!` macro**: Registers commands that need `provider.lock().await`.
- **`CommandResult` variants**: `SwitchSession`, `RenameSession`, `Init`, `SkillUse`, `SkillUnload` carry data back to the agent loop.
- **`CommandContext`** holds shared mutable state: provider, mode, file context, session ID, skill registry, active skills, pending images, thinking toggle, compaction token usage.
- **`extract_args!` macro** exported at `tinyharness_lib` root, not in `tools`.

## Verification Steps

After making changes, run:
1. `cargo fmt --all` — ensure formatting is clean
2. `cargo clippy --workspace -- -D warnings` — no clippy warnings
3. `cargo test --workspace` — all tests pass
4. `cargo build` — clean release build succeeds
After making changes, run in order:
1. `cargo fmt --all`
2. `cargo clippy --workspace -- -D warnings`
3. `cargo test --workspace`
4. `cargo build`
73 changes: 73 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# TinyHarness Documentation

## User Guides

- [Skills Guide](skills.md) — creating and using SKILL.md skill modules
- [Tools Reference](tools-reference.md) — tool categories, parameters, and behavior
- [Configuration Guide](configuration.md) — all settings, XDG paths, prompt customization
- [Safety & Security](safety.md) — command safety model, best practices
- [Agent Modes](modes.md) — mode behavior, prompt architecture, escalation
- [Per-Project Settings](per-project-settings.md) — `.tinyharness/config.json` reference
- [Language Detection](language-detection.md) — how project types are auto-detected
- [Project Instructions](project-instructions.md) — configuring `TINYHARNESS.md` discovery

## Developer Docs

- [Contributing](contributing.md) — project setup, code conventions, PR process

## Quick References

### By Role

| Role | Start Here |
|------|------------|
| New user | [Configuration Guide](configuration.md) → [Agent Modes](modes.md) |
| Setting up a project | [Project Instructions](project-instructions.md) → [Per-Project Settings](per-project-settings.md) |
| Writing skills | [Skills Guide](skills.md) |
| Security-conscious user | [Safety & Security](safety.md) |
| Contributor | [Contributing](contributing.md) |

### By Question

| Question | Doc |
|----------|-----|
| "How do I add project-specific commands?" | [Per-Project Settings](per-project-settings.md) |
| "How do I customize prompts?" | [Configuration Guide](configuration.md#system-prompts) |
| "What's the difference between planning and agent?" | [Agent Modes](modes.md) |
| "Can the AI auto-execute any command?" | [Safety & Security](safety.md#safe-commands) |
| "How do I write a skill?" | [Skills Guide](skills.md#skill-file-format) |
| "What tools are available?" | [Tools Reference](tools-reference.md) |
| "How do I override TINYHARNESS.md discovery?" | [Project Instructions](project-instructions.md#customizing-the-file-list) |
| "What languages are auto-detected?" | [Language Detection](language-detection.md) |
| "How do I contribute?" | [Contributing](contributing.md) |

## Files and Directories

TinyHarness stores data in standard XDG paths:

```
~/.config/tinyharness/
├── settings.json Global settings
├── prompts/ Customizable system prompt .md files
│ ├── header.md Shared header (agent, planning, research modes)
│ ├── casual.md Casual mode prompt
│ ├── planning.md Planning mode prompt
│ ├── agent.md Agent mode prompt
│ └── research.md Research mode prompt
└── skills/ Personal skills
└── <name>/
└── SKILL.md

~/.local/share/tinyharness/
├── sessions/ JSONL session files
│ └── <uuid>.jsonl
├── history.txt Command history (rustyline)
└── backups/ File backups (when /undo is implemented)
└── <session-id>/

<project>/.tinyharness/
├── config.json Per-project settings
└── skills/ Project-local skills
└── <name>/
└── SKILL.md
```
Loading
Loading