harness-project 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. harness_project-0.1.0/PKG-INFO +332 -0
  2. harness_project-0.1.0/README.md +320 -0
  3. harness_project-0.1.0/agent/__init__.py +0 -0
  4. harness_project-0.1.0/agent/compaction.py +195 -0
  5. harness_project-0.1.0/agent/loop.py +413 -0
  6. harness_project-0.1.0/agent/session.py +137 -0
  7. harness_project-0.1.0/agent/system_prompt.py +73 -0
  8. harness_project-0.1.0/cli.py +181 -0
  9. harness_project-0.1.0/config.py +56 -0
  10. harness_project-0.1.0/harness_project.egg-info/PKG-INFO +332 -0
  11. harness_project-0.1.0/harness_project.egg-info/SOURCES.txt +37 -0
  12. harness_project-0.1.0/harness_project.egg-info/dependency_links.txt +1 -0
  13. harness_project-0.1.0/harness_project.egg-info/entry_points.txt +2 -0
  14. harness_project-0.1.0/harness_project.egg-info/requires.txt +5 -0
  15. harness_project-0.1.0/harness_project.egg-info/top_level.txt +8 -0
  16. harness_project-0.1.0/main.py +5 -0
  17. harness_project-0.1.0/models.py +71 -0
  18. harness_project-0.1.0/providers/__init__.py +0 -0
  19. harness_project-0.1.0/providers/anthropic.py +187 -0
  20. harness_project-0.1.0/providers/base.py +31 -0
  21. harness_project-0.1.0/providers/gemini.py +191 -0
  22. harness_project-0.1.0/providers/openai.py +173 -0
  23. harness_project-0.1.0/providers/openrouter.py +175 -0
  24. harness_project-0.1.0/pyproject.toml +27 -0
  25. harness_project-0.1.0/setup.cfg +4 -0
  26. harness_project-0.1.0/tools/__init__.py +0 -0
  27. harness_project-0.1.0/tools/bash_tool.py +48 -0
  28. harness_project-0.1.0/tools/edit_tool.py +51 -0
  29. harness_project-0.1.0/tools/glob_tool.py +58 -0
  30. harness_project-0.1.0/tools/grep_tool.py +80 -0
  31. harness_project-0.1.0/tools/questions_tool.py +73 -0
  32. harness_project-0.1.0/tools/read_tool.py +30 -0
  33. harness_project-0.1.0/tools/registry.py +40 -0
  34. harness_project-0.1.0/tools/skill_reader.py +71 -0
  35. harness_project-0.1.0/tools/sub_agent_tool.py +88 -0
  36. harness_project-0.1.0/tools/todos_tool.py +113 -0
  37. harness_project-0.1.0/tools/web_search.py +55 -0
  38. harness_project-0.1.0/tools/write_tool.py +31 -0
  39. harness_project-0.1.0/tui.py +720 -0
@@ -0,0 +1,332 @@
1
+ Metadata-Version: 2.4
2
+ Name: harness-project
3
+ Version: 0.1.0
4
+ Summary: Multi-provider coding agent harness with memory, tools, and TUI
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: python-dotenv>=1.2.2
8
+ Requires-Dist: tavily-python>=0.5.0
9
+ Requires-Dist: rich>=13.0.0
10
+ Requires-Dist: httpx>=0.28.0
11
+ Requires-Dist: textual>=3.0.0
12
+
13
+ # Coding Agent Harness
14
+
15
+ A terminal coding agent with a real TUI — live streaming responses, rendered
16
+ markdown, a `/`-command palette, and persistent sessions you can walk away
17
+ from and resume later. Point it at Anthropic, OpenAI, Gemini, or anything
18
+ routed through OpenRouter, switch between them mid-conversation, and it never
19
+ asks you to type the same API key twice.
20
+
21
+ Think of it as a minimal, hackable "Claude Code style" agent that lives in a
22
+ proper terminal UI instead of a bare prompt.
23
+
24
+ ---
25
+
26
+ ## Why the TUI
27
+
28
+ This is the way the harness is meant to be run. Launch it, and:
29
+
30
+ - Your provider, model, and API key are remembered — `harness --tui` drops
31
+ you straight into a chat, no setup screen, as long as `.env` has what it
32
+ needs. Missing a key for the provider you want? A popup asks for it right
33
+ there, no restart.
34
+ - Responses **stream live**, token by token, and render as actual markdown —
35
+ headers, bold, code blocks, lists — not raw `**`/`` ``` `` syntax.
36
+ - Every command lives behind `/` — type it and get a live-filtered dropdown,
37
+ with second-level menus for anything that has a fixed set of choices
38
+ (`/provider`, for instance). No memorizing flags.
39
+ - Every conversation is a real, resumable session. Close the TUI, come back
40
+ tomorrow, `/resume` (or `--resume` at launch) and your entire history
41
+ replays back into the chat.
42
+ - Long sessions don't just die once they outgrow the model's context window —
43
+ compaction kicks in automatically, quietly, in the background.
44
+
45
+ ```bash
46
+ harness --tui
47
+ ```
48
+
49
+ That's the whole onboarding.
50
+
51
+ ---
52
+
53
+ ## Features
54
+
55
+ - **A real TUI, not a form** — Textual-powered, with a persistent status bar
56
+ showing provider/model/status, a live streaming preview, and a scrollable,
57
+ markdown-rendered chat log.
58
+ - **`/` command palette** — `/provider`, `/model`, `/apikey`, `/resume`,
59
+ `/sessions`, `/remember`, `/tools`, `/clear`, `/help`, `/exit` — all
60
+ discoverable by typing `/` and reading the dropdown, no docs required.
61
+ - **Multi-provider** — `anthropic`, `openai`, `gemini`, `openrouter` behind
62
+ one interface. Switch any of them, live, without leaving the chat.
63
+ - **Live streaming** — real token-by-token SSE streaming, all four providers.
64
+ - **Markdown rendering** — what the model writes is what you see rendered.
65
+ - **Real session persistence** — append-only transcripts on disk, resumable
66
+ by id, never rewritten, so a crash never loses more than one in-flight
67
+ message.
68
+ - **Project memory** — `HARNESS.md`, a plain markdown file always loaded into
69
+ context, editable by hand or via `/remember`. No hidden retrieval — you can
70
+ always see exactly what the agent knows.
71
+ - **Context compaction** — old tool output gets elided first, and if that's
72
+ not enough, older turns get summarized, automatically, before a session
73
+ ever hits a hard context-window failure.
74
+ - **11 built-in tools** — file read/write/edit, bash, grep, glob, web search,
75
+ clarifying questions, todo tracking, skill loading, sub-agent spawning.
76
+ - **A plain-text mode too**, for scripts and one-shot piping — see
77
+ [Scripting / one-shot mode](#scripting--one-shot-mode) below if that's what
78
+ you actually need.
79
+
80
+ ---
81
+
82
+ ## Installation
83
+
84
+ Requires **Python ≥ 3.12**. Uses [`uv`](https://github.com/astral-sh/uv).
85
+
86
+ ### Global install (recommended — gives you the `harness` command anywhere)
87
+
88
+ ```bash
89
+ uv tool install --editable .
90
+ harness --tui
91
+ ```
92
+
93
+ `--editable` runs against this actual source tree — changes to the code take
94
+ effect immediately, no reinstalling. It also means `harness --tui` works from
95
+ *any* directory, and treats wherever you're standing as the project root.
96
+ Uninstall any time with `uv tool uninstall harness-project`.
97
+
98
+ ### Local (run from inside this repo only)
99
+
100
+ ```bash
101
+ uv sync
102
+ uv run harness --tui
103
+ ```
104
+
105
+ ### Configuration
106
+
107
+ Create a `.env` file in the project root:
108
+
109
+ ```env
110
+ # pick a default provider
111
+ PROVIDER=anthropic
112
+
113
+ # provider API keys — only the one(s) you use are required
114
+ ANTHROPIC_API_KEY=sk-ant-...
115
+ OPENAI_API_KEY=sk-...
116
+ GEMINI_API_KEY=...
117
+ OPENROUTER_API_KEY=sk-or-...
118
+
119
+ # web search (optional, for the web_search tool)
120
+ TAVILY_API_KEY=tvly-...
121
+
122
+ # optional model overrides
123
+ MODEL= # force a specific model for any provider
124
+ ANTHROPIC_MODEL=claude-sonnet-4-20250514
125
+ OPENAI_MODEL=gpt-4o
126
+ GEMINI_MODEL=gemini-2.5-flash
127
+ OPENROUTER_MODEL=deepseek/deepseek-v4-flash-0731
128
+ ```
129
+
130
+ `.env` is `.gitignore`'d — your keys never get committed, and once they're
131
+ set, `harness --tui` never asks for them again. Don't have a key for the
132
+ provider you want yet? Launch anyway — `/provider` pops up a prompt for it on
133
+ the spot.
134
+
135
+ ---
136
+
137
+ ## Using the TUI
138
+
139
+ ```bash
140
+ harness --tui # fresh session
141
+ harness --tui --resume <session-id> # pick up where you left off
142
+ ```
143
+
144
+ Type `/` in the input box at any point for the command dropdown:
145
+
146
+ | Command | Does |
147
+ |---|---|
148
+ | `/provider <name>` | Switch provider. Pops up an API-key prompt on the spot if `.env` doesn't have one for it. |
149
+ | `/model <name>` | Switch the model for the current provider. |
150
+ | `/apikey <key>` | Set the API key for the current provider — this session only, never saved to `.env`. |
151
+ | `/resume <id>` | Load and replay a previous session's transcript. |
152
+ | `/sessions` | List every saved session. |
153
+ | `/remember <note>` | Append a note to `HARNESS.md`. |
154
+ | `/tools` | List every registered tool. |
155
+ | `/clear` | Clear the visible chat log. |
156
+ | `/help` | List all commands. |
157
+ | `/exit` | Quit. |
158
+
159
+ **Quitting**: `Ctrl+Q` quits instantly. `Ctrl+C`/`Cmd+C` copies selected text
160
+ if you've selected something in the chat log; otherwise the first press warns
161
+ you, the second (within 2s) quits.
162
+
163
+ **Copying text**: click-and-drag inside the chat log to select, then `Ctrl+C`
164
+ to copy. Not working in your terminal? Hold `Option (⌥)` on macOS while
165
+ dragging to force native terminal selection instead.
166
+
167
+ ---
168
+
169
+ ## Scripting / one-shot mode
170
+
171
+ For CI, scripts, or anything that isn't an interactive chat, there's a plain
172
+ CLI underneath the TUI — same agent loop, same tools, no interface:
173
+
174
+ ```bash
175
+ harness "add a --version flag to cli.py" # one-shot, non-interactive
176
+ harness # plain interactive REPL, no TUI
177
+ harness -p openrouter -m "anthropic/claude-sonnet-4" "explain the memory system"
178
+ harness --resume <session-id> # resume without the TUI
179
+ harness --list-tools
180
+ harness --list-sessions
181
+ ```
182
+
183
+ | Flag | Description |
184
+ |------|-------------|
185
+ | `message` (positional) | Initial task. If omitted, starts the plain REPL. |
186
+ | `-p, --provider` | `anthropic` \| `openai` \| `gemini` \| `openrouter` |
187
+ | `-m, --model` | Model name (defaults to the provider's default) |
188
+ | `-k, --api-key` | API key override (else read from `.env`) |
189
+ | `-r, --resume <id>` | Resume a previous session by id |
190
+ | `--tui` | Start the TUI instead |
191
+ | `--list-tools` | Print the tool registry and exit |
192
+ | `--list-sessions` | Print every saved session and exit |
193
+
194
+ The plain REPL also understands `/resume` and `/remember`, plus `exit`,
195
+ `quit`, `q` to stop — just without the dropdown, since there's no widget to
196
+ draw it in.
197
+
198
+ ---
199
+
200
+ ## Session & memory model
201
+
202
+ Three independent, deliberately simple mechanisms — no vector DB, no hidden
203
+ retrieval, nothing the model decides to remember on your behalf:
204
+
205
+ 1. **Session transcripts** (`agent/session.py`) — every message, append-only,
206
+ one `.jsonl` file per session under `.harness/sessions/`. Resuming replays
207
+ the exact original history; nothing is ever rewritten, so a crash mid-turn
208
+ only ever loses the one in-flight message.
209
+ 2. **Project memory** (`HARNESS.md`) — a plain markdown file at the project
210
+ root, loaded in full into the system prompt every turn. You write to it
211
+ directly, or via `/remember` — deterministic, no LLM involved in the write.
212
+ 3. **Context compaction** (`agent/compaction.py`) — triggers once a session
213
+ crosses ~75% of the active model's context window. Elides old, bulky tool
214
+ results first (free); if that's still not enough, summarizes what's left
215
+ with one extra LLM call. The full transcript on disk is untouched either
216
+ way — a resumed session just re-compacts on its next turn if it's still long.
217
+
218
+ ---
219
+
220
+ ## Architecture
221
+
222
+ ```mermaid
223
+ flowchart TD
224
+ TUI["tui.py — the main entry point"]
225
+ CLI["cli.py — scripting / one-shot"]
226
+
227
+ subgraph Core["agent/"]
228
+ LOOP["loop.py — agent_loop / run_agent_once"]
229
+ SESS["session.py — transcripts"]
230
+ COMPACT["compaction.py"]
231
+ SP["system_prompt.py"]
232
+ end
233
+
234
+ CONFIG["config.py"]
235
+ MODELS["models.py"]
236
+ HMD["HARNESS.md"]
237
+
238
+ subgraph Providers["providers/"]
239
+ P["anthropic · openai · gemini · openrouter"]
240
+ end
241
+
242
+ subgraph Tools["tools/"]
243
+ REG["registry.py"]
244
+ end
245
+
246
+ TUI --> LOOP
247
+ CLI --> LOOP
248
+ LOOP --> SP
249
+ LOOP --> CONFIG
250
+ LOOP -- generate_stream --> Providers
251
+ LOOP -- execute --> REG
252
+ LOOP --> SESS
253
+ LOOP --> COMPACT
254
+ SP -. reads .-> HMD
255
+ Providers --> MODELS
256
+ REG --> MODELS
257
+ ```
258
+
259
+ **The loop, in one sentence:** the TUI (or the plain CLI, for scripting) hands
260
+ a message to `agent_loop`, which builds a system prompt (tool docs +
261
+ `HARNESS.md` if present), streams a response from the selected provider, and
262
+ — while the provider keeps returning `tool_call` — executes tools via the
263
+ registry and feeds results back, until it returns `text`.
264
+
265
+ ---
266
+
267
+ ## Project layout
268
+
269
+ ```
270
+ main.py # entrypoint → cli.run_cli()
271
+ tui.py # the TUI — screens, slash commands, streaming preview
272
+ cli.py # argparse, tool registration, dispatch to loop / TUI
273
+ config.py # .env loading, paths, per-provider model/key resolution
274
+ models.py # Message, ToolDefinition, LLMResponse, StreamChunk
275
+
276
+ agent/
277
+ loop.py # agent_loop (interactive) + run_agent_once (sub-agents)
278
+ session.py # session transcript read/write
279
+ compaction.py # context-window compaction
280
+ system_prompt.py # DEFAULT_SYSTEM_PROMPT template
281
+
282
+ providers/
283
+ base.py # BaseProvider ABC — generate() + generate_stream()
284
+ anthropic.py openai.py gemini.py openrouter.py
285
+
286
+ tools/
287
+ registry.py # ToolRegistry (register / execute)
288
+ read_tool.py write_tool.py edit_tool.py
289
+ bash_tool.py grep_tool.py glob_tool.py
290
+ web_search.py questions_tool.py todos_tool.py
291
+ skill_reader.py sub_agent_tool.py
292
+
293
+ .skills/ # skill instruction files (discovered via read_skill)
294
+ .harness/ # runtime state: sessions, todos
295
+ HARNESS.md # project memory (created on first /remember)
296
+ ```
297
+
298
+ ---
299
+
300
+ ## Extending the harness
301
+
302
+ **Add a tool**
303
+ 1. Create `tools/my_tool.py` exporting `TOOL_DEF` (name / description /
304
+ JSON-schema `parameters`) and a `*_handler` function.
305
+ 2. Import it and add the `(TOOL_DEF, handler)` pair to the `tool_map` list in
306
+ `cli.py:setup_tools()`.
307
+
308
+ **Add a provider**
309
+ 1. Subclass `BaseProvider` in `providers/my_provider.py` and implement
310
+ `generate()` and `generate_stream()`, converting to/from the shared
311
+ `Message` / `LLMResponse` / `StreamChunk` types.
312
+ 2. Wire it into `_get_provider()` in `agent/loop.py` and add it to the CLI's
313
+ `--provider` choices and the TUI's `PROVIDER_MODELS` dict.
314
+
315
+ **Add a skill** — drop a text/markdown file into `.skills/`; the agent finds
316
+ it via the `read_skill` tool.
317
+
318
+ ---
319
+
320
+ ## Known limitations
321
+
322
+ Being upfront about what this doesn't do yet:
323
+
324
+ - **No approval gates** — tool calls (including `bash` and file writes) run
325
+ immediately, no confirmation step. No path sandboxing either.
326
+ - **One tool call per model turn** — if a model requests several tool calls
327
+ at once, only the first is used; the rest are silently dropped.
328
+ - **Sub-agents aren't truly parallel** — `spawn_sub_agent` blocks synchronously
329
+ while running, despite the "up to 3 in parallel" framing in the system prompt.
330
+ - **No retry/backoff** on transient API errors (rate limits, 5xx).
331
+
332
+ None of these are hidden — they're just not built yet.
@@ -0,0 +1,320 @@
1
+ # Coding Agent Harness
2
+
3
+ A terminal coding agent with a real TUI — live streaming responses, rendered
4
+ markdown, a `/`-command palette, and persistent sessions you can walk away
5
+ from and resume later. Point it at Anthropic, OpenAI, Gemini, or anything
6
+ routed through OpenRouter, switch between them mid-conversation, and it never
7
+ asks you to type the same API key twice.
8
+
9
+ Think of it as a minimal, hackable "Claude Code style" agent that lives in a
10
+ proper terminal UI instead of a bare prompt.
11
+
12
+ ---
13
+
14
+ ## Why the TUI
15
+
16
+ This is the way the harness is meant to be run. Launch it, and:
17
+
18
+ - Your provider, model, and API key are remembered — `harness --tui` drops
19
+ you straight into a chat, no setup screen, as long as `.env` has what it
20
+ needs. Missing a key for the provider you want? A popup asks for it right
21
+ there, no restart.
22
+ - Responses **stream live**, token by token, and render as actual markdown —
23
+ headers, bold, code blocks, lists — not raw `**`/`` ``` `` syntax.
24
+ - Every command lives behind `/` — type it and get a live-filtered dropdown,
25
+ with second-level menus for anything that has a fixed set of choices
26
+ (`/provider`, for instance). No memorizing flags.
27
+ - Every conversation is a real, resumable session. Close the TUI, come back
28
+ tomorrow, `/resume` (or `--resume` at launch) and your entire history
29
+ replays back into the chat.
30
+ - Long sessions don't just die once they outgrow the model's context window —
31
+ compaction kicks in automatically, quietly, in the background.
32
+
33
+ ```bash
34
+ harness --tui
35
+ ```
36
+
37
+ That's the whole onboarding.
38
+
39
+ ---
40
+
41
+ ## Features
42
+
43
+ - **A real TUI, not a form** — Textual-powered, with a persistent status bar
44
+ showing provider/model/status, a live streaming preview, and a scrollable,
45
+ markdown-rendered chat log.
46
+ - **`/` command palette** — `/provider`, `/model`, `/apikey`, `/resume`,
47
+ `/sessions`, `/remember`, `/tools`, `/clear`, `/help`, `/exit` — all
48
+ discoverable by typing `/` and reading the dropdown, no docs required.
49
+ - **Multi-provider** — `anthropic`, `openai`, `gemini`, `openrouter` behind
50
+ one interface. Switch any of them, live, without leaving the chat.
51
+ - **Live streaming** — real token-by-token SSE streaming, all four providers.
52
+ - **Markdown rendering** — what the model writes is what you see rendered.
53
+ - **Real session persistence** — append-only transcripts on disk, resumable
54
+ by id, never rewritten, so a crash never loses more than one in-flight
55
+ message.
56
+ - **Project memory** — `HARNESS.md`, a plain markdown file always loaded into
57
+ context, editable by hand or via `/remember`. No hidden retrieval — you can
58
+ always see exactly what the agent knows.
59
+ - **Context compaction** — old tool output gets elided first, and if that's
60
+ not enough, older turns get summarized, automatically, before a session
61
+ ever hits a hard context-window failure.
62
+ - **11 built-in tools** — file read/write/edit, bash, grep, glob, web search,
63
+ clarifying questions, todo tracking, skill loading, sub-agent spawning.
64
+ - **A plain-text mode too**, for scripts and one-shot piping — see
65
+ [Scripting / one-shot mode](#scripting--one-shot-mode) below if that's what
66
+ you actually need.
67
+
68
+ ---
69
+
70
+ ## Installation
71
+
72
+ Requires **Python ≥ 3.12**. Uses [`uv`](https://github.com/astral-sh/uv).
73
+
74
+ ### Global install (recommended — gives you the `harness` command anywhere)
75
+
76
+ ```bash
77
+ uv tool install --editable .
78
+ harness --tui
79
+ ```
80
+
81
+ `--editable` runs against this actual source tree — changes to the code take
82
+ effect immediately, no reinstalling. It also means `harness --tui` works from
83
+ *any* directory, and treats wherever you're standing as the project root.
84
+ Uninstall any time with `uv tool uninstall harness-project`.
85
+
86
+ ### Local (run from inside this repo only)
87
+
88
+ ```bash
89
+ uv sync
90
+ uv run harness --tui
91
+ ```
92
+
93
+ ### Configuration
94
+
95
+ Create a `.env` file in the project root:
96
+
97
+ ```env
98
+ # pick a default provider
99
+ PROVIDER=anthropic
100
+
101
+ # provider API keys — only the one(s) you use are required
102
+ ANTHROPIC_API_KEY=sk-ant-...
103
+ OPENAI_API_KEY=sk-...
104
+ GEMINI_API_KEY=...
105
+ OPENROUTER_API_KEY=sk-or-...
106
+
107
+ # web search (optional, for the web_search tool)
108
+ TAVILY_API_KEY=tvly-...
109
+
110
+ # optional model overrides
111
+ MODEL= # force a specific model for any provider
112
+ ANTHROPIC_MODEL=claude-sonnet-4-20250514
113
+ OPENAI_MODEL=gpt-4o
114
+ GEMINI_MODEL=gemini-2.5-flash
115
+ OPENROUTER_MODEL=deepseek/deepseek-v4-flash-0731
116
+ ```
117
+
118
+ `.env` is `.gitignore`'d — your keys never get committed, and once they're
119
+ set, `harness --tui` never asks for them again. Don't have a key for the
120
+ provider you want yet? Launch anyway — `/provider` pops up a prompt for it on
121
+ the spot.
122
+
123
+ ---
124
+
125
+ ## Using the TUI
126
+
127
+ ```bash
128
+ harness --tui # fresh session
129
+ harness --tui --resume <session-id> # pick up where you left off
130
+ ```
131
+
132
+ Type `/` in the input box at any point for the command dropdown:
133
+
134
+ | Command | Does |
135
+ |---|---|
136
+ | `/provider <name>` | Switch provider. Pops up an API-key prompt on the spot if `.env` doesn't have one for it. |
137
+ | `/model <name>` | Switch the model for the current provider. |
138
+ | `/apikey <key>` | Set the API key for the current provider — this session only, never saved to `.env`. |
139
+ | `/resume <id>` | Load and replay a previous session's transcript. |
140
+ | `/sessions` | List every saved session. |
141
+ | `/remember <note>` | Append a note to `HARNESS.md`. |
142
+ | `/tools` | List every registered tool. |
143
+ | `/clear` | Clear the visible chat log. |
144
+ | `/help` | List all commands. |
145
+ | `/exit` | Quit. |
146
+
147
+ **Quitting**: `Ctrl+Q` quits instantly. `Ctrl+C`/`Cmd+C` copies selected text
148
+ if you've selected something in the chat log; otherwise the first press warns
149
+ you, the second (within 2s) quits.
150
+
151
+ **Copying text**: click-and-drag inside the chat log to select, then `Ctrl+C`
152
+ to copy. Not working in your terminal? Hold `Option (⌥)` on macOS while
153
+ dragging to force native terminal selection instead.
154
+
155
+ ---
156
+
157
+ ## Scripting / one-shot mode
158
+
159
+ For CI, scripts, or anything that isn't an interactive chat, there's a plain
160
+ CLI underneath the TUI — same agent loop, same tools, no interface:
161
+
162
+ ```bash
163
+ harness "add a --version flag to cli.py" # one-shot, non-interactive
164
+ harness # plain interactive REPL, no TUI
165
+ harness -p openrouter -m "anthropic/claude-sonnet-4" "explain the memory system"
166
+ harness --resume <session-id> # resume without the TUI
167
+ harness --list-tools
168
+ harness --list-sessions
169
+ ```
170
+
171
+ | Flag | Description |
172
+ |------|-------------|
173
+ | `message` (positional) | Initial task. If omitted, starts the plain REPL. |
174
+ | `-p, --provider` | `anthropic` \| `openai` \| `gemini` \| `openrouter` |
175
+ | `-m, --model` | Model name (defaults to the provider's default) |
176
+ | `-k, --api-key` | API key override (else read from `.env`) |
177
+ | `-r, --resume <id>` | Resume a previous session by id |
178
+ | `--tui` | Start the TUI instead |
179
+ | `--list-tools` | Print the tool registry and exit |
180
+ | `--list-sessions` | Print every saved session and exit |
181
+
182
+ The plain REPL also understands `/resume` and `/remember`, plus `exit`,
183
+ `quit`, `q` to stop — just without the dropdown, since there's no widget to
184
+ draw it in.
185
+
186
+ ---
187
+
188
+ ## Session & memory model
189
+
190
+ Three independent, deliberately simple mechanisms — no vector DB, no hidden
191
+ retrieval, nothing the model decides to remember on your behalf:
192
+
193
+ 1. **Session transcripts** (`agent/session.py`) — every message, append-only,
194
+ one `.jsonl` file per session under `.harness/sessions/`. Resuming replays
195
+ the exact original history; nothing is ever rewritten, so a crash mid-turn
196
+ only ever loses the one in-flight message.
197
+ 2. **Project memory** (`HARNESS.md`) — a plain markdown file at the project
198
+ root, loaded in full into the system prompt every turn. You write to it
199
+ directly, or via `/remember` — deterministic, no LLM involved in the write.
200
+ 3. **Context compaction** (`agent/compaction.py`) — triggers once a session
201
+ crosses ~75% of the active model's context window. Elides old, bulky tool
202
+ results first (free); if that's still not enough, summarizes what's left
203
+ with one extra LLM call. The full transcript on disk is untouched either
204
+ way — a resumed session just re-compacts on its next turn if it's still long.
205
+
206
+ ---
207
+
208
+ ## Architecture
209
+
210
+ ```mermaid
211
+ flowchart TD
212
+ TUI["tui.py — the main entry point"]
213
+ CLI["cli.py — scripting / one-shot"]
214
+
215
+ subgraph Core["agent/"]
216
+ LOOP["loop.py — agent_loop / run_agent_once"]
217
+ SESS["session.py — transcripts"]
218
+ COMPACT["compaction.py"]
219
+ SP["system_prompt.py"]
220
+ end
221
+
222
+ CONFIG["config.py"]
223
+ MODELS["models.py"]
224
+ HMD["HARNESS.md"]
225
+
226
+ subgraph Providers["providers/"]
227
+ P["anthropic · openai · gemini · openrouter"]
228
+ end
229
+
230
+ subgraph Tools["tools/"]
231
+ REG["registry.py"]
232
+ end
233
+
234
+ TUI --> LOOP
235
+ CLI --> LOOP
236
+ LOOP --> SP
237
+ LOOP --> CONFIG
238
+ LOOP -- generate_stream --> Providers
239
+ LOOP -- execute --> REG
240
+ LOOP --> SESS
241
+ LOOP --> COMPACT
242
+ SP -. reads .-> HMD
243
+ Providers --> MODELS
244
+ REG --> MODELS
245
+ ```
246
+
247
+ **The loop, in one sentence:** the TUI (or the plain CLI, for scripting) hands
248
+ a message to `agent_loop`, which builds a system prompt (tool docs +
249
+ `HARNESS.md` if present), streams a response from the selected provider, and
250
+ — while the provider keeps returning `tool_call` — executes tools via the
251
+ registry and feeds results back, until it returns `text`.
252
+
253
+ ---
254
+
255
+ ## Project layout
256
+
257
+ ```
258
+ main.py # entrypoint → cli.run_cli()
259
+ tui.py # the TUI — screens, slash commands, streaming preview
260
+ cli.py # argparse, tool registration, dispatch to loop / TUI
261
+ config.py # .env loading, paths, per-provider model/key resolution
262
+ models.py # Message, ToolDefinition, LLMResponse, StreamChunk
263
+
264
+ agent/
265
+ loop.py # agent_loop (interactive) + run_agent_once (sub-agents)
266
+ session.py # session transcript read/write
267
+ compaction.py # context-window compaction
268
+ system_prompt.py # DEFAULT_SYSTEM_PROMPT template
269
+
270
+ providers/
271
+ base.py # BaseProvider ABC — generate() + generate_stream()
272
+ anthropic.py openai.py gemini.py openrouter.py
273
+
274
+ tools/
275
+ registry.py # ToolRegistry (register / execute)
276
+ read_tool.py write_tool.py edit_tool.py
277
+ bash_tool.py grep_tool.py glob_tool.py
278
+ web_search.py questions_tool.py todos_tool.py
279
+ skill_reader.py sub_agent_tool.py
280
+
281
+ .skills/ # skill instruction files (discovered via read_skill)
282
+ .harness/ # runtime state: sessions, todos
283
+ HARNESS.md # project memory (created on first /remember)
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Extending the harness
289
+
290
+ **Add a tool**
291
+ 1. Create `tools/my_tool.py` exporting `TOOL_DEF` (name / description /
292
+ JSON-schema `parameters`) and a `*_handler` function.
293
+ 2. Import it and add the `(TOOL_DEF, handler)` pair to the `tool_map` list in
294
+ `cli.py:setup_tools()`.
295
+
296
+ **Add a provider**
297
+ 1. Subclass `BaseProvider` in `providers/my_provider.py` and implement
298
+ `generate()` and `generate_stream()`, converting to/from the shared
299
+ `Message` / `LLMResponse` / `StreamChunk` types.
300
+ 2. Wire it into `_get_provider()` in `agent/loop.py` and add it to the CLI's
301
+ `--provider` choices and the TUI's `PROVIDER_MODELS` dict.
302
+
303
+ **Add a skill** — drop a text/markdown file into `.skills/`; the agent finds
304
+ it via the `read_skill` tool.
305
+
306
+ ---
307
+
308
+ ## Known limitations
309
+
310
+ Being upfront about what this doesn't do yet:
311
+
312
+ - **No approval gates** — tool calls (including `bash` and file writes) run
313
+ immediately, no confirmation step. No path sandboxing either.
314
+ - **One tool call per model turn** — if a model requests several tool calls
315
+ at once, only the first is used; the rest are silently dropped.
316
+ - **Sub-agents aren't truly parallel** — `spawn_sub_agent` blocks synchronously
317
+ while running, despite the "up to 3 in parallel" framing in the system prompt.
318
+ - **No retry/backoff** on transient API errors (rate limits, 5xx).
319
+
320
+ None of these are hidden — they're just not built yet.
File without changes