| @@ -12,7 +12,7 @@ The code is the byproduct. The deliverable is [METRICS.md](METRICS.md). |
| 12 | 12 | export ANTHROPIC_API_KEY=sk-ant-... |
| 13 | 13 | go build -o harness ./cmd/harness |
| 14 | 14 | |
| 15 | | - ./harness -C /path/to/a/throwaway/clone # REPL |
| 15 | + ./harness -C /path/to/a/throwaway/clone # full-screen TUI |
| 16 | 16 | ./harness -C /path/to/repo -p "why does TestFoo fail?" # one prompt |
| 17 | 17 | ``` |
| 18 | 18 | |
| @@ -30,7 +30,89 @@ which is what OAuth tokens and most gateways want; `-api-key` sends |
| 30 | 30 | prompt and every file the agent reads go wherever that URL points. |
| 31 | 31 | |
| 32 | 32 | Useful flags: `-model`, `-effort low|medium|high|xhigh|max`, `-max-turns`, |
| 33 | | - `-resume latest`, `-no-stream`, `-no-cache`, `-v`, `-yolo`. |
| 33 | + `-resume latest`, `-no-stream`, `-no-cache`, `-v`, `-yolo`, `-no-tui`, `-no-skills`. |
| 34 | + |
| 35 | + ## The TUI |
| 36 | + |
| 37 | + On a terminal, the interactive mode is a full-screen [Bubble Tea](https://github.com/charmbracelet/bubbletea) |
| 38 | + app. It opens on a welcome block naming the model, the endpoint and the root, |
| 39 | + and settles into a transcript above a prompt: |
| 40 | + |
| 41 | + ``` |
| 42 | + ⏺ Let me see what the test expects before changing anything. |
| 43 | + |
| 44 | + ⏺ ReadFile(path=internal/agent/loop_test.go) |
| 45 | + ⎿ 1: package agent |
| 46 | + … |
| 47 | + |
| 48 | + ⏺ EditFile(path=internal/agent/compact.go) |
| 49 | + 4 additions, 1 removal |
| 50 | + @@ -50,7 +50,9 @@ |
| 51 | + + if window == 0 { |
| 52 | + |
| 53 | + ✢ Noodling… (14s · turn 3 · ↓ 1.3k tokens · esc to interrupt) ● high · /effort |
| 54 | + ╭──────────────────────────────────────────────────────────────────────────────╮ |
| 55 | + │ › _ │ |
| 56 | + ╰──────────────────────────────────────────────────────────────────────────────╯ |
| 57 | + joni ✳ Opus 5 ◐ 62.2k (6%) ↑ 4.2k ⟳ 58.0k ↓ 1.3k $0.0835 |
| 58 | + ⏵ approvals on (shift+tab to cycle) · /help for commands |
| 59 | + ``` |
| 60 | + |
| 61 | + Every event hangs off a dot in the left gutter, and everything belonging to it |
| 62 | + is indented under that dot, so a screen of tool output still reads as a |
| 63 | + sequence of things that happened. Diffs are shown in full and in colour with a |
| 64 | + count of what changed; other output is elided after six lines. |
| 65 | + |
| 66 | + The status bar reports the context gauge — the last turn's input size against |
| 67 | + the window, which drops when the conversation is compacted — and separates |
| 68 | + fresh input tokens from cache reads, because the gap between the two is what |
| 69 | + [METRICS.md](METRICS.md) is about. The prompt box grows with what you type, up |
| 70 | + to six lines. |
| 71 | + |
| 72 | + | Key | Does | |
| 73 | + | --- | --- | |
| 74 | + | `enter` | send | |
| 75 | + | `ctrl+j` | newline without sending | |
| 76 | + | `shift+tab` | cycle the approval mode | |
| 77 | + | `ctrl+r` | expand elided tool output | |
| 78 | + | `pgup`/`pgdn` | scroll the transcript | |
| 79 | + | `esc` | interrupt the turn | |
| 80 | + | `ctrl+c` | interrupt the turn; again when idle to exit | |
| 81 | + |
| 82 | + Commands: `/allowed`, `/clear`, `/help`, `/exit`, `/skills`. |
| 83 | + |
| 84 | + `shift+tab` cycles between asking before every mutating call and accepting them |
| 85 | + all, which is `-yolo` reached from the keyboard. The mode line says which is in |
| 86 | + force at all times, and the switch is recorded in the transcript, because a |
| 87 | + mode that silently lets the agent write to disk is not one to keep in a corner. |
| 88 | + |
| 89 | + Approvals appear inline as a bordered prompt, answered by number or by the key |
| 90 | + in brackets — `y`, `n`, `a` for this exact command, `t` for the whole tool. The |
| 91 | + agent goroutine blocks until you answer, which is the point: the tool has not |
| 92 | + run yet. Interrupting while that prompt is open denies the call and cancels the |
| 93 | + turn rather than leaving the agent waiting on a channel nobody will read. |
| 94 | + |
| 95 | + The TUI is skipped automatically when stdin or stdout is not a terminal, and by |
| 96 | + `-p` and `-no-tui`, which use the line-based renderer instead. Both front ends |
| 97 | + consume the same `agent.Event` stream, so neither can show something the other |
| 98 | + cannot. |
| 99 | + |
| 100 | + ### Driving it without an API key |
| 101 | + |
| 102 | + `internal/tui/testdata/mockapi` is a fake Messages API that streams a canned |
| 103 | + reply and asks to run a shell command, which is enough to exercise streaming, |
| 104 | + tool rendering, approve, deny and interrupt for nothing: |
| 105 | + |
| 106 | + ```bash |
| 107 | + go run ./internal/tui/testdata/mockapi & |
| 108 | + mkdir -p /tmp/demo && git -C /tmp/demo init -q |
| 109 | + go build -o harness ./cmd/harness |
| 110 | + ./harness -C /tmp/demo -base-url http://127.0.0.1:8899 -api-key mock |
| 111 | + ``` |
| 112 | + |
| 113 | + Type anything, and the approval prompt appears. `-slow 150ms` stretches the |
| 114 | + stream out if you want to watch the spinner or test `ctrl+c` mid-turn. It sits |
| 115 | + under `testdata` so `./...` ignores it. |
| 34 | 116 | |
| 35 | 117 | ## Tools |
| 36 | 118 | |
| @@ -43,6 +125,47 @@ Useful flags: `-model`, `-effort low|medium|high|xhigh|max`, `-max-turns`, |
| 43 | 125 | | `write_file` | prompts | Whole files only; separate from `edit_file` so the metrics stay honest | |
| 44 | 126 | | `bash` | prompts | Own process group, killed whole on timeout; output capped head and tail | |
| 45 | 127 | |
| 128 | + ## Skills |
| 129 | + |
| 130 | + A skill is a directory of instructions the agent pulls in when it is relevant: |
| 131 | + |
| 132 | + ``` |
| 133 | + ~/.agents/skills/commit-helper/SKILL.md every repo |
| 134 | + ./.agents/skills/commit-helper/SKILL.md this one, checked in |
| 135 | + ``` |
| 136 | + |
| 137 | + ```markdown |
| 138 | + --- |
| 139 | + name: commit-helper |
| 140 | + description: Use when writing commit messages for this repo |
| 141 | + --- |
| 142 | + |
| 143 | + Read the last twenty commits for tone before writing anything. |
| 144 | + Conventions are in references/conventions.md. |
| 145 | + ``` |
| 146 | + |
| 147 | + The directory name is the skill's name, and a `name:` that disagrees with it is |
| 148 | + a load error. Only the description reaches the model until the skill is |
| 149 | + invoked, so it is what decides whether the skill fires at all; the body can be |
| 150 | + as long as you like. |
| 151 | + |
| 152 | + Two ways in, one way to load. The agent calls the `skill` tool when a |
| 153 | + description matches what it is doing; you can force one with `/commit-helper |
| 154 | + fix the flaky test`, which the model answers by making the same call. `/skills` |
| 155 | + lists what was found, project shadowing personal on a name collision. |
| 156 | + |
| 157 | + Skill directories are readable by `read_file`, `glob` and `grep` even though |
| 158 | + they sit outside the working root, so a skill can ship reference files and |
| 159 | + point at them. They are not writable by anything: `edit_file`, `write_file` and |
| 160 | + `bash` keep the working root as their only root. A skill is input, not a work |
| 161 | + product. |
| 162 | + |
| 163 | + Discovery happens at startup. The name-and-description listing goes in the |
| 164 | + cached prompt prefix, so re-reading the directory each turn would cost a cache |
| 165 | + miss on every turn for a file that never changes. Restart to pick up a new |
| 166 | + skill. `-no-skills` turns the whole thing off; the eval runner never discovers |
| 167 | + skills at all, so METRICS.md is unaffected by what is in your home directory. |
| 168 | + |
| 46 | 169 | ## Evals |
| 47 | 170 | |
| 48 | 171 | ```bash |
| @@ -107,13 +230,22 @@ cmd/harness flag parsing, wiring, REPL |
| 107 | 230 | internal/llm Messages API: types, HTTP, SSE, pricing |
| 108 | 231 | internal/agent turn loop, conversation state, compaction, sessions |
| 109 | 232 | internal/tools the tool registry and the tools |
| 233 | + internal/skills discovery and parsing of SKILL.md |
| 110 | 234 | internal/approval allow / deny / ask, with shell analysis |
| 111 | 235 | internal/metrics JSONL event log |
| 112 | | - internal/ui terminal rendering |
| 236 | + internal/ui line-based terminal rendering |
| 237 | + internal/tui full-screen Bubble Tea front end |
| 113 | 238 | evals task suite and runner |
| 114 | 239 | ``` |
| 115 | 240 | |
| 116 | | - Two rules hold the shape: `agent` never imports `ui`, so the eval runner can |
| 117 | | - drive the loop headless; and every tool returns `(string, error)` where the |
| 118 | | - error is text the model reads, not an exception the harness swallows. A failed |
| 119 | | - edit is a normal event in the transcript. |
| 241 | + Two rules hold the shape: `agent` never imports `ui` or `tui`, so the eval |
| 242 | + runner can drive the loop headless; and every tool returns `(string, error)` |
| 243 | + where the error is text the model reads, not an exception the harness swallows. |
| 244 | + A failed edit is a normal event in the transcript. |
| 245 | + |
| 246 | + The TUI obeys the first rule through two one-way hatches. Events reach it via |
| 247 | + `agent.Observe`, forwarded into the Bubble Tea loop with `Program.Send`, which |
| 248 | + is safe from the agent's goroutine. Approvals go out the same way and then |
| 249 | + block the agent on a reply channel until a key arrives. That channel is |
| 250 | + buffered and the wait honours the turn's context, so neither side can strand |
| 251 | + the other. |