Metadata-Version: 2.4
Name: harness-project
Version: 0.1.0
Summary: Multi-provider coding agent harness with memory, tools, and TUI
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: python-dotenv>=1.2.2
Requires-Dist: tavily-python>=0.5.0
Requires-Dist: rich>=13.0.0
Requires-Dist: httpx>=0.28.0
Requires-Dist: textual>=3.0.0

# Coding Agent Harness

A terminal coding agent with a real TUI — live streaming responses, rendered
markdown, a `/`-command palette, and persistent sessions you can walk away
from and resume later. Point it at Anthropic, OpenAI, Gemini, or anything
routed through OpenRouter, switch between them mid-conversation, and it never
asks you to type the same API key twice.

Think of it as a minimal, hackable "Claude Code style" agent that lives in a
proper terminal UI instead of a bare prompt.

---

## Why the TUI

This is the way the harness is meant to be run. Launch it, and:

- Your provider, model, and API key are remembered — `harness --tui` drops
  you straight into a chat, no setup screen, as long as `.env` has what it
  needs. Missing a key for the provider you want? A popup asks for it right
  there, no restart.
- Responses **stream live**, token by token, and render as actual markdown —
  headers, bold, code blocks, lists — not raw `**`/`` ``` `` syntax.
- Every command lives behind `/` — type it and get a live-filtered dropdown,
  with second-level menus for anything that has a fixed set of choices
  (`/provider`, for instance). No memorizing flags.
- Every conversation is a real, resumable session. Close the TUI, come back
  tomorrow, `/resume` (or `--resume` at launch) and your entire history
  replays back into the chat.
- Long sessions don't just die once they outgrow the model's context window —
  compaction kicks in automatically, quietly, in the background.

```bash
harness --tui
```

That's the whole onboarding.

---

## Features

- **A real TUI, not a form** — Textual-powered, with a persistent status bar
  showing provider/model/status, a live streaming preview, and a scrollable,
  markdown-rendered chat log.
- **`/` command palette** — `/provider`, `/model`, `/apikey`, `/resume`,
  `/sessions`, `/remember`, `/tools`, `/clear`, `/help`, `/exit` — all
  discoverable by typing `/` and reading the dropdown, no docs required.
- **Multi-provider** — `anthropic`, `openai`, `gemini`, `openrouter` behind
  one interface. Switch any of them, live, without leaving the chat.
- **Live streaming** — real token-by-token SSE streaming, all four providers.
- **Markdown rendering** — what the model writes is what you see rendered.
- **Real session persistence** — append-only transcripts on disk, resumable
  by id, never rewritten, so a crash never loses more than one in-flight
  message.
- **Project memory** — `HARNESS.md`, a plain markdown file always loaded into
  context, editable by hand or via `/remember`. No hidden retrieval — you can
  always see exactly what the agent knows.
- **Context compaction** — old tool output gets elided first, and if that's
  not enough, older turns get summarized, automatically, before a session
  ever hits a hard context-window failure.
- **11 built-in tools** — file read/write/edit, bash, grep, glob, web search,
  clarifying questions, todo tracking, skill loading, sub-agent spawning.
- **A plain-text mode too**, for scripts and one-shot piping — see
  [Scripting / one-shot mode](#scripting--one-shot-mode) below if that's what
  you actually need.

---

## Installation

Requires **Python ≥ 3.12**. Uses [`uv`](https://github.com/astral-sh/uv).

### Global install (recommended — gives you the `harness` command anywhere)

```bash
uv tool install --editable .
harness --tui
```

`--editable` runs against this actual source tree — changes to the code take
effect immediately, no reinstalling. It also means `harness --tui` works from
*any* directory, and treats wherever you're standing as the project root.
Uninstall any time with `uv tool uninstall harness-project`.

### Local (run from inside this repo only)

```bash
uv sync
uv run harness --tui
```

### Configuration

Create a `.env` file in the project root:

```env
# pick a default provider
PROVIDER=anthropic

# provider API keys — only the one(s) you use are required
ANTHROPIC_API_KEY=sk-ant-...
OPENAI_API_KEY=sk-...
GEMINI_API_KEY=...
OPENROUTER_API_KEY=sk-or-...

# web search (optional, for the web_search tool)
TAVILY_API_KEY=tvly-...

# optional model overrides
MODEL=                              # force a specific model for any provider
ANTHROPIC_MODEL=claude-sonnet-4-20250514
OPENAI_MODEL=gpt-4o
GEMINI_MODEL=gemini-2.5-flash
OPENROUTER_MODEL=deepseek/deepseek-v4-flash-0731
```

`.env` is `.gitignore`'d — your keys never get committed, and once they're
set, `harness --tui` never asks for them again. Don't have a key for the
provider you want yet? Launch anyway — `/provider` pops up a prompt for it on
the spot.

---

## Using the TUI

```bash
harness --tui                       # fresh session
harness --tui --resume <session-id> # pick up where you left off
```

Type `/` in the input box at any point for the command dropdown:

| Command | Does |
|---|---|
| `/provider <name>` | Switch provider. Pops up an API-key prompt on the spot if `.env` doesn't have one for it. |
| `/model <name>` | Switch the model for the current provider. |
| `/apikey <key>` | Set the API key for the current provider — this session only, never saved to `.env`. |
| `/resume <id>` | Load and replay a previous session's transcript. |
| `/sessions` | List every saved session. |
| `/remember <note>` | Append a note to `HARNESS.md`. |
| `/tools` | List every registered tool. |
| `/clear` | Clear the visible chat log. |
| `/help` | List all commands. |
| `/exit` | Quit. |

**Quitting**: `Ctrl+Q` quits instantly. `Ctrl+C`/`Cmd+C` copies selected text
if you've selected something in the chat log; otherwise the first press warns
you, the second (within 2s) quits.

**Copying text**: click-and-drag inside the chat log to select, then `Ctrl+C`
to copy. Not working in your terminal? Hold `Option (⌥)` on macOS while
dragging to force native terminal selection instead.

---

## Scripting / one-shot mode

For CI, scripts, or anything that isn't an interactive chat, there's a plain
CLI underneath the TUI — same agent loop, same tools, no interface:

```bash
harness "add a --version flag to cli.py"    # one-shot, non-interactive
harness                                     # plain interactive REPL, no TUI
harness -p openrouter -m "anthropic/claude-sonnet-4" "explain the memory system"
harness --resume <session-id>               # resume without the TUI
harness --list-tools
harness --list-sessions
```

| Flag | Description |
|------|-------------|
| `message` (positional) | Initial task. If omitted, starts the plain REPL. |
| `-p, --provider` | `anthropic` \| `openai` \| `gemini` \| `openrouter` |
| `-m, --model` | Model name (defaults to the provider's default) |
| `-k, --api-key` | API key override (else read from `.env`) |
| `-r, --resume <id>` | Resume a previous session by id |
| `--tui` | Start the TUI instead |
| `--list-tools` | Print the tool registry and exit |
| `--list-sessions` | Print every saved session and exit |

The plain REPL also understands `/resume` and `/remember`, plus `exit`,
`quit`, `q` to stop — just without the dropdown, since there's no widget to
draw it in.

---

## Session & memory model

Three independent, deliberately simple mechanisms — no vector DB, no hidden
retrieval, nothing the model decides to remember on your behalf:

1. **Session transcripts** (`agent/session.py`) — every message, append-only,
   one `.jsonl` file per session under `.harness/sessions/`. Resuming replays
   the exact original history; nothing is ever rewritten, so a crash mid-turn
   only ever loses the one in-flight message.
2. **Project memory** (`HARNESS.md`) — a plain markdown file at the project
   root, loaded in full into the system prompt every turn. You write to it
   directly, or via `/remember` — deterministic, no LLM involved in the write.
3. **Context compaction** (`agent/compaction.py`) — triggers once a session
   crosses ~75% of the active model's context window. Elides old, bulky tool
   results first (free); if that's still not enough, summarizes what's left
   with one extra LLM call. The full transcript on disk is untouched either
   way — a resumed session just re-compacts on its next turn if it's still long.

---

## Architecture

```mermaid
flowchart TD
    TUI["tui.py — the main entry point"]
    CLI["cli.py — scripting / one-shot"]

    subgraph Core["agent/"]
        LOOP["loop.py — agent_loop / run_agent_once"]
        SESS["session.py — transcripts"]
        COMPACT["compaction.py"]
        SP["system_prompt.py"]
    end

    CONFIG["config.py"]
    MODELS["models.py"]
    HMD["HARNESS.md"]

    subgraph Providers["providers/"]
        P["anthropic · openai · gemini · openrouter"]
    end

    subgraph Tools["tools/"]
        REG["registry.py"]
    end

    TUI --> LOOP
    CLI --> LOOP
    LOOP --> SP
    LOOP --> CONFIG
    LOOP -- generate_stream --> Providers
    LOOP -- execute --> REG
    LOOP --> SESS
    LOOP --> COMPACT
    SP -. reads .-> HMD
    Providers --> MODELS
    REG --> MODELS
```

**The loop, in one sentence:** the TUI (or the plain CLI, for scripting) hands
a message to `agent_loop`, which builds a system prompt (tool docs +
`HARNESS.md` if present), streams a response from the selected provider, and
— while the provider keeps returning `tool_call` — executes tools via the
registry and feeds results back, until it returns `text`.

---

## Project layout

```
main.py                 # entrypoint → cli.run_cli()
tui.py                  # the TUI — screens, slash commands, streaming preview
cli.py                  # argparse, tool registration, dispatch to loop / TUI
config.py               # .env loading, paths, per-provider model/key resolution
models.py               # Message, ToolDefinition, LLMResponse, StreamChunk

agent/
  loop.py               # agent_loop (interactive) + run_agent_once (sub-agents)
  session.py            # session transcript read/write
  compaction.py         # context-window compaction
  system_prompt.py      # DEFAULT_SYSTEM_PROMPT template

providers/
  base.py               # BaseProvider ABC — generate() + generate_stream()
  anthropic.py openai.py gemini.py openrouter.py

tools/
  registry.py            # ToolRegistry (register / execute)
  read_tool.py write_tool.py edit_tool.py
  bash_tool.py grep_tool.py glob_tool.py
  web_search.py questions_tool.py todos_tool.py
  skill_reader.py sub_agent_tool.py

.skills/                # skill instruction files (discovered via read_skill)
.harness/                # runtime state: sessions, todos
HARNESS.md               # project memory (created on first /remember)
```

---

## Extending the harness

**Add a tool**
1. Create `tools/my_tool.py` exporting `TOOL_DEF` (name / description /
   JSON-schema `parameters`) and a `*_handler` function.
2. Import it and add the `(TOOL_DEF, handler)` pair to the `tool_map` list in
   `cli.py:setup_tools()`.

**Add a provider**
1. Subclass `BaseProvider` in `providers/my_provider.py` and implement
   `generate()` and `generate_stream()`, converting to/from the shared
   `Message` / `LLMResponse` / `StreamChunk` types.
2. Wire it into `_get_provider()` in `agent/loop.py` and add it to the CLI's
   `--provider` choices and the TUI's `PROVIDER_MODELS` dict.

**Add a skill** — drop a text/markdown file into `.skills/`; the agent finds
it via the `read_skill` tool.

---

## Known limitations

Being upfront about what this doesn't do yet:

- **No approval gates** — tool calls (including `bash` and file writes) run
  immediately, no confirmation step. No path sandboxing either.
- **One tool call per model turn** — if a model requests several tool calls
  at once, only the first is used; the rest are silently dropped.
- **Sub-agents aren't truly parallel** — `spawn_sub_agent` blocks synchronously
  while running, despite the "up to 3 in parallel" framing in the system prompt.
- **No retry/backoff** on transient API errors (rate limits, 5xx).

None of these are hidden — they're just not built yet.
