Vectrum
Sign In

rename: the harness is Codey now, and it has a gopher

Everything that said harness says codey: the module path and every import
under it, cmd/codey, the binary, the stderr prefix, the wordmark in the
splash and the wizard, and the prose in README and PLAN.

The two on-disk names move differently, because the risks differ. The
config directory is a read-time fallback — with nothing at
~/.config/codey, the old path is read and left exactly where it is, so a
setup already done still counts and an older build still finds what it
wrote. The per-repository .harness/ is instead renamed once, at startup,
before anything opens a file inside it: events.jsonl is append-only, and
a history split across two names is worse than no history at all.

That directory name now lives in one place, internal/statedir, rather
than being spelled independently by agent and metrics — which is the
duplication this rename would otherwise have had to find twice.

The new mascot is a gopher, on the same 8x4 half-block grid as the
robot it replaces, so the splash layout and the wizard's row-at-a-time
assembly needed no changes at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Jjonicommitted 1 parent 93ba06dcommit752dcdc

Showing 67 changed files with 537 additions and 209 deletions.

+6−1.gitignoreView File
@@ -1,3 +1,8 @@
1- /.harness/
1+ /.codey/
22 /evals/runs/
3+ /codey
4+ /.superpowers/
5+
6+ # Left behind by builds from before the rename.
7+ /.harness/
38 /harness
+1−1METRICS.mdView File
@@ -18,7 +18,7 @@ against a live model, not by anything written in advance.
1818 ## Reading the numbers
1919
2020 - **Pass rate** counts a task solved only when *every* repeat passed. A task
21- that passes two runs in three is a task the harness cannot do reliably, and
21+ that passes two runs in three is a task Codey cannot do reliably, and
2222 averaging that away hides the thing worth fixing.
2323 - **Edit success** is `edit_file` calls that returned a diff, over all
2424 `edit_file` calls. `write_file` is deliberately a separate tool so a
+6−6PLAN.mdView File
@@ -1,4 +1,4 @@
1- # Building a Coding Agent Harness in Go — Build Plan
1+ # Building Codey, a Coding Agent Harness in Go — Build Plan
22
33 Target: a harness that can be pointed at a real Go or TypeScript repo, told
44 "make the failing test pass," and actually do it — with numbers proving how
@@ -19,8 +19,8 @@ comes first.** (See METRICS.md.)
1919 ## Repo layout
2020
2121 ```
22- harness/
23- cmd/harness/main.go # flag parsing, wiring, REPL entry
22+ codey/
23+ cmd/codey/main.go # flag parsing, wiring, REPL entry
2424 internal/
2525 llm/
2626 client.go # HTTP + SSE against the Messages API
@@ -70,7 +70,7 @@ rather than hand-writing JSON Schema. Hand-written schemas drift from the
7070 structs and you won't notice until the model sends a field you don't parse.
7171
7272 Also in this phase: `metrics.Log(event)` writing one JSON object per line to
73- `./.harness/events.jsonl`. Wire it before you have anything to measure.
73+ `./.codey/events.jsonl`. Wire it before you have anything to measure.
7474 Retrofitting instrumentation is how projects end up with no numbers.
7575
7676 ## Phase 1 — The minimal loop
@@ -147,7 +147,7 @@ code-search story; you do not need embeddings.
147147 New files only, or full rewrites the model explicitly chose. Keep it separate
148148 from edit so your metrics don't blend them.
149149
150- **Hard rule from this phase onward:** run the harness only against a throwaway
150+ **Hard rule from this phase onward:** run Codey only against a throwaway
151151 git clone, or inside a container. There is no approval layer until Phase 6.
152152 Assume it will run `rm -rf` on something eventually.
153153
@@ -249,7 +249,7 @@ correction without losing the session.
249249 - Show diffs before applying, with color.
250250 - Show running token and cost totals per session. Cheap to add, and it changes
251251 how you design.
252- - Persist sessions to `.harness/sessions/<id>.json` and add `--resume`.
252+ - Persist sessions to `.codey/sessions/<id>.json` and add `--resume`.
253253
254254 Keep `agent` free of terminal dependencies. The eval runner depends on that.
255255
+59−23README.mdView File
@@ -1,7 +1,7 @@
1- # harness
1+ # codey
22
3- A coding agent harness in Go. Point it at a repository, tell it to make the
4- failing test pass, and measure how often it actually does.
3+ Codey is a coding agent harness in Go. Point it at a repository, tell it to
4+ make the failing test pass, and measure how often it actually does.
55
66 The code is the byproduct. The deliverable is [METRICS.md](METRICS.md).
77 [PLAN.md](PLAN.md) is the build plan this follows.
@@ -9,13 +9,13 @@ The code is the byproduct. The deliverable is [METRICS.md](METRICS.md).
99 ## Quick start
1010
1111 ```bash
12- go build -o harness ./cmd/harness
12+ go build -o codey ./cmd/codey
1313
14- ./harness -C /path/to/a/throwaway/clone # full-screen TUI
15- ./harness -C /path/to/repo -p "why does TestFoo fail?" # one prompt
14+ ./codey -C /path/to/a/throwaway/clone # full-screen TUI
15+ ./codey -C /path/to/repo -p "why does TestFoo fail?" # one prompt
1616 ```
1717
18- If you are logged into Claude Code, that is all the setup there is: the harness
18+ If you are logged into Claude Code, that is all the setup there is: Codey
1919 borrows the OAuth credential from `~/.claude/.credentials.json` when nothing
2020 else supplied one. Otherwise, or to use a different account:
2121
@@ -27,7 +27,7 @@ A different endpoint — a gateway, a proxy, a local server speaking the Message
2727 API:
2828
2929 ```bash
30- ./harness -base-url http://127.0.0.1:4000 -api-key ... -C .
30+ ./codey -base-url http://127.0.0.1:4000 -api-key ... -C .
3131 # or ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN
3232 ```
3333
@@ -41,7 +41,7 @@ points.
4141 The Claude Code login is the last resort, tried only when no flag and no
4242 environment variable supplied a credential, and only against Anthropic's own
4343 endpoint — a credential found on disk is not one you chose to send, so it is
44- never forwarded to a `-base-url` you pointed the harness at. `-no-claude-code-auth`
44+ never forwarded to a `-base-url` you pointed Codey at. `-no-claude-code-auth`
4545 skips it entirely. Note that the credential is issued to Claude Code; using it
4646 from another client is outside Anthropic's terms for it, which API keys and
4747 gateway tokens do not carry.
@@ -70,7 +70,7 @@ a handful of enormous tool results is too few messages to cut six off the end of
7070 nothing if the next request has no room to be sent, so the floor folds anyway
7171 and settles for a one-message tail. `/compact` follows the same rule past the
7272 floor, since with auto-compaction off it is the only move left. When even that
73- finds nothing to fold, the harness says so, rather than letting the next
73+ finds nothing to fold, Codey says so, rather than letting the next
7474 request fail as a bare API error.
7575
7676 `-context-window` (default 1,000,000) is what both fractions are measured
@@ -84,8 +84,21 @@ folding: the session stays intact, nothing is summarized behind your back, and
8484 ## The TUI
8585
8686 On a terminal, the interactive mode is a full-screen [Bubble Tea](https://github.com/charmbracelet/bubbletea)
87- app. It opens on a welcome block naming the model, the endpoint and the root,
88- and settles into a transcript above a prompt:
87+ app. It opens on a welcome block — Codey, then the model, the endpoint and the
88+ root:
89+
90+ ```
91+ ▄██████▄ codey v0.6.0
92+ █ ██ █ Opus 5 · anthropic
93+ ███▀▀███ ~/Work/GO/coding-harness
94+ ▀█▀▀█▀
95+ ```
96+
97+ The gopher is drawn in half-blocks, so it needs no patched font, and it is
98+ dropped rather than sliced on a terminal too narrow to hold it beside a
99+ readable amount of text: half a mascot reads as a rendering bug, where the
100+ three lines alone read as a choice. From there it settles into a transcript
101+ above a prompt:
89102
90103 ```
91104 ⏺ Let me see what the test expects before changing anything.
@@ -189,7 +202,7 @@ the top or bottom edge of the transcript scrolls, so a selection can be longer
189202 than the screen; it keeps going while the pointer moves, because a terminal
190203 reporting button events says nothing about a hand held still.
191204
192- The harness runs in the alternate screen, where the terminal has no scrollback
205+ Codey runs in the alternate screen, where the terminal has no scrollback
193206 of its own to roll — so asking for mouse reporting is what makes the wheel work
194207 at all. The cost is the terminal's own text selection, which stops being the
195208 terminal's the moment drags are reported to the program. That is why the
@@ -334,7 +347,7 @@ exists, so a config written by hand, or by a build with fewer questions in it,
334347 still gets asked the ones it is missing.
335348
336349 ```
337- harness setup step 1 of 4
350+ codey setup step 1 of 4
338351 ◆━━━━━━━━━━━━━━━◇───────────────◇───────────────◇
339352
340353 Which model should the agent use?
@@ -368,7 +381,7 @@ The theme step previews by applying the palette for real — reading a swatch
368381 tells you nothing about what a transcript looks like in it — and it recolours
369382 the whole TUI, pickers and slider included. Cancelling puts the old one back.
370383
371- Answers land in `~/.config/harness/config.json` (`XDG_CONFIG_HOME` is
384+ Answers land in `~/.config/codey/config.json` (`XDG_CONFIG_HOME` is
372385 honoured):
373386
374387 ```json
@@ -419,6 +432,29 @@ ignored, and is never overwritten until you finish the wizard.
419432 on"; they do not write to the file. The two screens that do are the wizard and
420433 `/settings`.
421434
435+ ### Where Codey keeps things
436+
437+ Two directories: one global, one per repository. The second sits in your
438+ working tree and is git-ignored, because sessions and an event log are yours
439+ rather than the project's.
440+
441+ | Path | What |
442+ | --- | --- |
443+ | `~/.config/codey/config.json` | the four answers, written by the wizard and `/settings` |
444+ | `<repo>/.codey/sessions/` | saved conversations, which is what `-resume` reads |
445+ | `<repo>/.codey/events.jsonl` | the event log METRICS.md is built from |
446+
447+ Codey was called `harness` until recently, and both of those used to live under
448+ that name. Neither rename costs you anything. The config is read from
449+ `~/.config/harness/config.json` when there is nothing at the new path, so a
450+ setup you have already done still counts and the wizard stays shut; that is a
451+ read, and the old file is left where it is for an older build to keep using.
452+ The per-repository directory is instead moved once, on the first run in a clone
453+ that still has a `.harness/`, and Codey says so when it does — the event log is
454+ append-only, so leaving half a repository's history under the old name would
455+ quietly corrupt the numbers rather than preserve them. A clone that somehow has
456+ both keeps the newer one and leaves `.harness/` alone.
457+
422458 ### /settings
423459
424460 `/settings` is the same four settings without the interview: one row each,
@@ -431,7 +467,7 @@ changed in place, applied as you move.
431467 Approvals Ask me first recommended
432468 Theme ember
433469 claude-opus-5 · enter to pick from anthropic
434- saved to ~/.config/harness/config.json
470+ saved to ~/.config/codey/config.json
435471 ```
436472
437473 `←→` cycles a row and applies it there and then — the theme repaints under the
@@ -542,7 +578,7 @@ session memory. `/allowed` lists what you have agreed to.
542578
543579 Shell commands are parsed with `mvdan.cc/sh/syntax` so the prompt can tell you
544580 what a command will actually run — `git status; rm -rf ~` is shown as running
545- both `git` and `rm`. **This is not a security boundary**, and the harness does
581+ both `git` and `rm`. **This is not a security boundary**, and Codey does
546582 not pretend otherwise: a command containing `$(...)`, backticks, or an
547583 interpreter with `-c` cannot be analysed at all, so it is marked opaque and
548584 "always allow this exact command" is refused for it. String-matching
@@ -550,24 +586,24 @@ allowlists for shell commands do not work; this exists to make the prompt
550586 specific, not to make the decision for you.
551587
552588 **A sandbox.** This is the layer that actually contains a mistake, and the
553- harness cannot provide it for you. The honest recipe on Linux:
589+ Codey cannot provide it for you. The honest recipe on Linux:
554590
555591 ```bash
556592 podman run --rm -it \
557593 -v "$PWD":/work:Z -w /work \
558594 -e ANTHROPIC_API_KEY \
559595 docker.io/library/golang:1.26 \
560- /work/harness -C /work -yolo
596+ /work/codey -C /work -yolo
561597 ```
562598
563599 **Decide about egress explicitly.** The container above has network access,
564- because the harness needs to reach the API — which means the agent's `bash`
600+ because Codey needs to reach the API — which means the agent's `bash`
565601 has it too. That is the line between "broke my repo" and "read my `.env` and
566602 made an HTTP request". If that matters for the tree you are pointing it at:
567603
568604 - Treat every secret reachable from inside the container as exposed.
569605 - Use a token scoped to nothing else, and a clone rather than your working tree.
570- - For real isolation, run the harness on the host and put only `bash` in a
606+ - For real isolation, run Codey on the host and put only `bash` in a
571607 network-less namespace. That is not implemented here.
572608
573609 `-yolo` skips every prompt. It is correct inside a container or against a
@@ -577,7 +613,7 @@ is headless and operates on a temp directory it created.
577613 ## Layout
578614
579615 ```
580- cmd/harness flag parsing, wiring, REPL
616+ cmd/codey flag parsing, wiring, REPL
581617 internal/llm Messages API: types, HTTP, SSE, pricing
582618 internal/agent turn loop, conversation state, compaction, sessions
583619 internal/tools the tool registry and the tools
@@ -592,7 +628,7 @@ evals task suite and runner
592628
593629 Two rules hold the shape: `agent` never imports `ui` or `tui`, so the eval
594630 runner can drive the loop headless; and every tool returns `(string, error)`
595- where the error is text the model reads, not an exception the harness swallows.
631+ where the error is text the model reads, not an exception Codey swallows.
596632 A failed edit is a normal event in the transcript.
597633
598634 The TUI obeys the first rule through two one-way hatches. Events reach it via
+1−1cmd/harness/auth_test.go → cmd/codey/auth_test.gorenamedView File
@@ -6,7 +6,7 @@ import (
66 "path/filepath"
77 "testing"
88
9- "harness/internal/llm"
9+ "codey/internal/llm"
1010 )
1111
1212 // fakeHome writes a credentials file with a token that is not expired.
+36−23cmd/harness/main.go → cmd/codey/main.gorenamedView File
@@ -1,4 +1,4 @@
1- // Command harness is the CLI entry point: flag parsing, wiring, and the REPL.
1+ // Command codey is the CLI entry point: flag parsing, wiring, and the REPL.
22 package main
33
44 import (
@@ -14,26 +14,27 @@ import (
1414 "sync/atomic"
1515 "time"
1616
17- "harness/internal/agent"
18- "harness/internal/approval"
19- "harness/internal/claudecode"
20- "harness/internal/config"
21- "harness/internal/llm"
22- "harness/internal/metrics"
23- "harness/internal/skills"
24- "harness/internal/tools"
25- "harness/internal/tui"
26- "harness/internal/ui"
17+ "codey/internal/agent"
18+ "codey/internal/approval"
19+ "codey/internal/claudecode"
20+ "codey/internal/config"
21+ "codey/internal/llm"
22+ "codey/internal/metrics"
23+ "codey/internal/skills"
24+ "codey/internal/statedir"
25+ "codey/internal/tools"
26+ "codey/internal/tui"
27+ "codey/internal/ui"
2728 )
2829
2930 // version is what the TUI's welcome block reports. It is a constant rather
30- // than build-stamped: the harness is built from this tree, and a stamp that
31+ // than build-stamped: Codey is built from this tree, and a stamp that
3132 // can disagree with the source is worse than no stamp.
3233 const version = "v0.6.0"
3334
3435 func main() {
3536 if err := run(); err != nil {
36- fmt.Fprintln(os.Stderr, "harness:", err)
37+ fmt.Fprintln(os.Stderr, "codey:", err)
3738 os.Exit(1)
3839 }
3940 }
@@ -67,8 +68,14 @@ func run() error {
6768 return err
6869 }
6970
71+ // A clone last used under the old name keeps its sessions and its event
72+ // log. This runs before anything opens a file inside the state directory,
73+ // and is reported rather than fatal: a stale directory that cannot be
74+ // moved is a reason to say so, not a reason to refuse to start.
75+ stateMoved, stateErr := statedir.Migrate(root)
76+
7077 // Saved preferences sit under the flags and over the built-in defaults. A
71- // config that cannot be read is reported and then ignored: the harness
78+ // config that cannot be read is reported and then ignored: Codey
7279 // still starts, and the wizard can write a good one over it.
7380 cfg, _, cfgErr := config.Load()
7481
@@ -152,12 +159,18 @@ func run() error {
152159 banner = append(banner, msg)
153160 return
154161 }
155- fmt.Fprintln(os.Stderr, "harness: "+msg)
162+ fmt.Fprintln(os.Stderr, "codey: "+msg)
156163 }
157164
158165 if cfgErr != nil {
159166 notice("%v — using defaults; /setup will write a fresh one", cfgErr)
160167 }
168+ if stateMoved {
169+ notice("moved %s to %s — sessions and metrics came with it", statedir.LegacyDir, statedir.Dir)
170+ }
171+ if stateErr != nil {
172+ notice("%v — starting a fresh %s; the old one is untouched", stateErr, statedir.Dir)
173+ }
161174
162175 if !client.IsDefaultEndpoint() {
163176 // Worth saying out loud: the prompt and every file the agent reads go
@@ -179,8 +192,8 @@ func run() error {
179192 "error": err.Error(), "file": "~/" + claudecode.Filename,
180193 }})
181194 if !useTUI {
182- fmt.Fprintf(os.Stderr, "harness: your Claude Code login was renewed but ~/%s could not be updated: %v\n", claudecode.Filename, err)
183- fmt.Fprintln(os.Stderr, "harness: run `claude` when convenient — its stored login is now out of date.")
195+ fmt.Fprintf(os.Stderr, "codey: your Claude Code login was renewed but ~/%s could not be updated: %v\n", claudecode.Filename, err)
196+ fmt.Fprintln(os.Stderr, "codey: run `claude` when convenient — its stored login is now out of date.")
184197 }
185198 }
186199 }
@@ -223,7 +236,7 @@ func run() error {
223236 policy.YOLO = acceptAll
224237 if acceptAll && !useTUI {
225238 // The TUI says this itself, in the banner.
226- fmt.Fprintln(os.Stderr, "harness: -yolo — no approval prompts. Use a throwaway clone or a container.")
239+ fmt.Fprintln(os.Stderr, "codey: -yolo — no approval prompts. Use a throwaway clone or a container.")
227240 }
228241 a := agent.New(client, reg, state, log, agent.Options{
229242 MaxTurns: *maxTurns,
@@ -294,7 +307,7 @@ func run() error {
294307 return once(ctx, a, render, sessionDir, session, root, client, *prompt)
295308 }
296309
297- fmt.Printf("harness · %s · %s\ntools: %s\ntype a prompt, ctrl-c to interrupt a turn, ctrl-d to exit\n\n",
310+ fmt.Printf("codey · %s · %s\ntools: %s\ntype a prompt, ctrl-c to interrupt a turn, ctrl-d to exit\n\n",
298311 client.Model(), root, strings.Join(toolNames(reg), ", "))
299312
300313 for {
@@ -337,7 +350,7 @@ func once(ctx context.Context, a *agent.Agent, render *ui.Renderer, sessionDir,
337350 render.Usage(usage, turns, client.Model())
338351
339352 if err := a.Save(sessionDir, id, root, client.Model()); err != nil {
340- fmt.Fprintln(os.Stderr, "harness: saving session:", err)
353+ fmt.Fprintln(os.Stderr, "codey: saving session:", err)
341354 }
342355 if errors.Is(runErr, context.Canceled) {
343356 // An interrupted turn is not an error: the point is to leave the
@@ -365,10 +378,10 @@ func interruptible(parent context.Context) (context.Context, func()) {
365378 cancel := turnCancel.Load()
366379
367380 if cancel == nil || (prev != 0 && now.Sub(time.Unix(0, prev)) < 2*time.Second) {
368- fmt.Fprintln(os.Stderr, "\nharness: exiting")
381+ fmt.Fprintln(os.Stderr, "\ncodey: exiting")
369382 os.Exit(130)
370383 }
371- fmt.Fprintln(os.Stderr, "\nharness: interrupting this turn (ctrl-c again to exit)")
384+ fmt.Fprintln(os.Stderr, "\ncodey: interrupting this turn (ctrl-c again to exit)")
372385 (*cancel)()
373386 }
374387 }()
@@ -421,7 +434,7 @@ func resolveClaudeCode(client *llm.Client, home string, disabled bool) (*claudec
421434 // is the only circumstance in which this process writes to Claude Code's
422435 // file, and not writing then is what would break its login.
423436 //
424- // The refresh client is deliberately not the harness's: that one allows
437+ // The refresh client is deliberately not Codey's: that one allows
425438 // ten minutes for a model to answer, which is the wrong budget for a token
426439 // exchange running under a lock. NewSource supplies a short-timeout client.
427440 return claudecode.NewSource(creds, nil, home), note, nil
+1−1evals/report.goView File
@@ -41,7 +41,7 @@ func report(w io.Writer, results []Result) {
4141 tw.Flush()
4242
4343 // A task counts as solved if every repeat passed; anything less is a task
44- // the harness cannot do reliably, which is the number worth reporting.
44+ // Codey cannot do reliably, which is the number worth reporting.
4545 var solved int
4646 for _, name := range order {
4747 if countPass(byTask[name]) == len(byTask[name]) {
+5−5evals/runner.goView File
@@ -19,11 +19,11 @@ import (
1919 "sync"
2020 "time"
2121
22- "harness/internal/agent"
23- "harness/internal/approval"
24- "harness/internal/llm"
25- "harness/internal/metrics"
26- "harness/internal/tools"
22+ "codey/internal/agent"
23+ "codey/internal/approval"
24+ "codey/internal/llm"
25+ "codey/internal/metrics"
26+ "codey/internal/tools"
2727 )
2828
2929 // Task is one directory under evals/tasks.
+1−1go.modView File
@@ -1,4 +1,4 @@
1- module harness
1+ module codey
22
33 go 1.26.8
44
+4−4internal/agent/compact.goView File
@@ -6,8 +6,8 @@ import (
66 "strconv"
77 "strings"
88
9- "harness/internal/llm"
10- "harness/internal/metrics"
9+ "codey/internal/llm"
10+ "codey/internal/metrics"
1111 )
1212
1313 // metricsCompaction records a compaction so its effect on later turns is
@@ -164,7 +164,7 @@ func (a *Agent) maybeCompact(ctx context.Context, lastInputTokens int) (bool, er
164164 //
165165 // Past the floor it settles for the smaller tail, for the same reason the floor
166166 // does — and because with auto-compaction off this is the only thing between a
167- // full window and /clear. Declining there would be the harness refusing the one
167+ // full window and /clear. Declining there would be Codey refusing the one
168168 // move the user has left.
169169 func (a *Agent) Compact(ctx context.Context) (bool, error) {
170170 did, err := a.compact(ctx, a.lastInputTokens, keepRecentMessages)
@@ -250,7 +250,7 @@ func (a *Agent) summarize(ctx context.Context, history []llm.Message) (string, e
250250 }
251251 // This is the one request in the agent that is built by hand rather than
252252 // through (*Agent).request/systemBlocks, and it is deliberately bare — no
253- // tools, no harness prompt — so it is easy to forget that an OAuth
253+ // tools, no system prompt of ours — so it is easy to forget that an OAuth
254254 // credential still requires the Claude Code identity string as the FIRST
255255 // system block. Skip it here and the API does not 401; it returns a 429
256256 // that (*APIError).Retryable treats as transient, so the request burns
+2−2internal/agent/compact_test.goView File
@@ -10,7 +10,7 @@ import (
1010 "strings"
1111 "testing"
1212
13- "harness/internal/llm"
13+ "codey/internal/llm"
1414 )
1515
1616 func TestCacheBreakpoints(t *testing.T) {
@@ -446,7 +446,7 @@ func TestCompactOnDemandDeclinesAShortConversation(t *testing.T) {
446446 }
447447 }
448448
449- // With auto-compaction off the harness must not fold anything on its own, even
449+ // With auto-compaction off Codey must not fold anything on its own, even
450450 // well past the ordinary threshold.
451451 func TestAutoCompactOffNeverFolds(t *testing.T) {
452452 f := newFakeAPI(t)
+6−6internal/agent/loop.goView File
@@ -8,10 +8,10 @@ import (
88 "strings"
99 "sync"
1010
11- "harness/internal/llm"
12- "harness/internal/metrics"
13- "harness/internal/skills"
14- "harness/internal/tools"
11+ "codey/internal/llm"
12+ "codey/internal/metrics"
13+ "codey/internal/skills"
14+ "codey/internal/tools"
1515 )
1616
1717 // DefaultMaxTurns caps one Run. An agent that loops without converging burns
@@ -87,7 +87,7 @@ type Options struct {
8787 // demand by the skill tool.
8888 Skills []skills.Skill
8989 // SystemPrefix is prepended as a separate system block ahead of the
90- // harness prompt. It carries the identity string an OAuth credential
90+ // Codey's own system prompt. It carries the identity string an OAuth credential
9191 // requires, and is empty for every other credential.
9292 SystemPrefix string
9393 }
@@ -319,7 +319,7 @@ func (a *Agent) dispatch(ctx context.Context, turn int, calls []llm.ContentBlock
319319
320320 // authorizedExecute runs a tool once the approval policy allows it. A denial
321321 // comes back as an ordinary tool error: the model should read it and change
322- // course, not see a harness crash.
322+ // course, not see Codey crash.
323323 func (a *Agent) authorizedExecute(ctx context.Context, call llm.ContentBlock) (string, error) {
324324 if a.Approve != nil {
325325 if err := a.Approve.Approve(ctx, call.Name, call.Input); err != nil {
+8−8internal/agent/loop_test.goView File
@@ -14,10 +14,10 @@ import (
1414 "strings"
1515 "testing"
1616
17- "harness/internal/llm"
18- "harness/internal/metrics"
19- "harness/internal/skills"
20- "harness/internal/tools"
17+ "codey/internal/llm"
18+ "codey/internal/metrics"
19+ "codey/internal/skills"
20+ "codey/internal/tools"
2121 )
2222
2323 // fakeAPI serves a scripted sequence of responses and records every request
@@ -53,7 +53,7 @@ func newFakeAPI(t *testing.T, responses ...string) *fakeAPI {
5353 body, _ := io.ReadAll(r.Body)
5454 var req llm.Request
5555 if err := json.Unmarshal(body, &req); err != nil {
56- t.Errorf("harness sent a body the API types cannot parse: %v\n%s", err, body)
56+ t.Errorf("codey sent a body the API types cannot parse: %v\n%s", err, body)
5757 }
5858 f.requests = append(f.requests, req)
5959 if bytes.Contains(body, []byte("Summarize the conversation above")) {
@@ -62,7 +62,7 @@ func newFakeAPI(t *testing.T, responses ...string) *fakeAPI {
6262 }
6363 i := len(f.requests) - 1 - f.compactions()
6464 if i >= len(f.responses) {
65- t.Errorf("harness made %d requests, script only has %d", i+1, len(f.responses))
65+ t.Errorf("codey made %d requests, script only has %d", i+1, len(f.responses))
6666 w.WriteHeader(http.StatusInternalServerError)
6767 return
6868 }
@@ -468,7 +468,7 @@ func TestDeniedToolBecomesAToolError(t *testing.T) {
468468
469469 got, err := a.Run(context.Background(), "read it")
470470 if err != nil {
471- t.Fatalf("a denial is not a harness failure: %v", err)
471+ t.Fatalf("a denial is not a failure of ours: %v", err)
472472 }
473473 if got != "understood" {
474474 t.Errorf("answer = %q", got)
@@ -538,7 +538,7 @@ func TestSystemPrefixBecomesTheFirstBlock(t *testing.T) {
538538 t.Errorf("first block = %q, want the identity string", blocks[0].Text)
539539 }
540540 if !strings.Contains(blocks[1].Text, "coding agent") {
541- t.Errorf("second block should be the harness prompt, got %q", blocks[1].Text)
541+ t.Errorf("second block should be Codey's own system prompt, got %q", blocks[1].Text)
542542 }
543543 }
544544
+3−2internal/agent/session.goView File
@@ -8,7 +8,8 @@ import (
88 "sort"
99 "time"
1010
11- "harness/internal/llm"
11+ "codey/internal/llm"
12+ "codey/internal/statedir"
1213 )
1314
1415 // Session is a conversation saved to disk so --resume can pick it up.
@@ -24,7 +25,7 @@ type Session struct {
2425 }
2526
2627 // SessionDir is where sessions live, relative to the working root.
27- const SessionDir = ".harness/sessions"
28+ const SessionDir = statedir.Dir + "/sessions"
2829
2930 // Save writes the session for id, overwriting any previous copy.
3031 func (a *Agent) Save(dir, id, root, model string) error {
+1−1internal/agent/session_test.goView File
@@ -9,7 +9,7 @@ import (
99 "testing"
1010 "time"
1111
12- "harness/internal/llm"
12+ "codey/internal/llm"
1313 )
1414
1515 func TestSaveAndResumeSession(t *testing.T) {
+2−2internal/agent/state.goView File
@@ -10,8 +10,8 @@ import (
1010 "sync"
1111 "time"
1212
13- "harness/internal/llm"
14- "harness/internal/tools"
13+ "codey/internal/llm"
14+ "codey/internal/tools"
1515 )
1616
1717 // ReadRecord lives in tools so the edit tool can query the tracker without
+1−1internal/claudecode/claudecode.goView File
@@ -1,5 +1,5 @@
11 // Package claudecode reads the OAuth credential Claude Code stores on disk, so
2- // a harness run can borrow a login that already exists.
2+ // a Codey run can borrow a login that already exists.
33 //
44 // This is Claude Code's private storage, not a documented interface: the
55 // schema can change without notice. Everything that knows its shape lives
+2−2internal/claudecode/refresh.goView File
@@ -24,7 +24,7 @@ const tokenURLEnv = "CLAUDECODE_TOKEN_URL"
2424
2525 const defaultTokenURL = "https://console.anthropic.com/v1/oauth/token"
2626
27- // refreshTimeout bounds a token exchange. The harness's own HTTP client allows
27+ // refreshTimeout bounds a token exchange. Codey's own HTTP client allows
2828 // ten minutes, which is right for a model response and badly wrong here: the
2929 // refresh runs under a lock every other request is queued behind.
3030 const refreshTimeout = 30 * time.Second
@@ -310,7 +310,7 @@ func NewSource(c *Credentials, h *http.Client, home string) *Source {
310310 //
311311 // The lock means concurrent requests — streaming and buffered can be in flight
312312 // together — produce one refresh rather than a thundering herd. Refresh bounds
313- // its own time, so the wait behind that lock is seconds, not the harness
313+ // its own time, so the wait behind that lock is seconds, not Codey's
314314 // client's ten-minute model-response budget.
315315 func (s *Source) Token(ctx context.Context) (string, error) {
316316 s.mu.Lock()
+34−8internal/config/config.goView File
@@ -2,7 +2,7 @@
22 //
33 // It holds defaults, never facts: every field is something a flag or an
44 // environment variable may override for a given run. Nothing here is required
5- // for the harness to start, which is what lets a missing file mean "first
5+ // for Codey to start, which is what lets a missing file mean "first
66 // run" rather than "broken install".
77 package config
88
@@ -14,12 +14,19 @@ import (
1414 "os"
1515 "path/filepath"
1616
17- "harness/internal/llm"
17+ "codey/internal/llm"
1818 )
1919
20- // Filename is the config's name inside the harness config directory.
20+ // Filename is the config's name inside Codey's config directory.
2121 const Filename = "config.json"
2222
23+ // Dir is Codey's directory inside the user's config directory. legacyDir is
24+ // what it was called before the rename, and is still read from.
25+ const (
26+ Dir = "codey"
27+ legacyDir = "harness"
28+ )
29+
2330 // Version is stamped on every write. It exists so a future change of shape
2431 // has something to branch on rather than guessing from which keys are present.
2532 const Version = 1
@@ -86,14 +93,20 @@ func (c Config) AutoCompactEnabled() bool { return c.AutoCompact != AutoCompactO
8693 // that suppresses the wizard on startup.
8794 func (c Config) Done() bool { return c.Setup == SetupDone }
8895
89- // Path is where the config lives: $XDG_CONFIG_HOME/harness/config.json, or
90- // ~/.config/harness/config.json when XDG_CONFIG_HOME is unset.
91- func Path() (string, error) {
96+ // Path is where the config lives: $XDG_CONFIG_HOME/codey/config.json, or
97+ // ~/.config/codey/config.json when XDG_CONFIG_HOME is unset.
98+ func Path() (string, error) { return pathIn(Dir) }
99+
100+ // LegacyPath is where the config lived when Codey was called harness. It is
101+ // read when there is nothing at Path, and never written.
102+ func LegacyPath() (string, error) { return pathIn(legacyDir) }
103+
104+ func pathIn(name string) (string, error) {
92105 dir, err := os.UserConfigDir()
93106 if err != nil {
94107 return "", fmt.Errorf("config: no config directory: %w", err)
95108 }
96- return filepath.Join(dir, "harness", Filename), nil
109+ return filepath.Join(dir, name, Filename), nil
97110 }
98111
99112 // Load reads the config. The boolean reports whether a usable one was found;
@@ -104,12 +117,25 @@ func Path() (string, error) {
104117 // found=false. The caller is expected to carry on with defaults and say so:
105118 // what it must not do is treat the file as absent, because the next Save would
106119 // then overwrite settings the user still has.
120+ //
121+ // With nothing at Path, the pre-rename location is tried before giving up, so
122+ // a setup done under the old name is not thrown away and the wizard does not
123+ // reappear for someone who has already answered it. The fallback is a read:
124+ // the old file stays where it is, and the next Save writes only to Path.
107125 func Load() (Config, bool, error) {
108126 path, err := Path()
109127 if err != nil {
110128 return Config{}, false, err
111129 }
112130 raw, err := os.ReadFile(path)
131+ if errors.Is(err, fs.ErrNotExist) {
132+ legacy, lerr := LegacyPath()
133+ if lerr != nil {
134+ return Config{}, false, lerr
135+ }
136+ path = legacy
137+ raw, err = os.ReadFile(path)
138+ }
113139 if errors.Is(err, fs.ErrNotExist) {
114140 return Config{}, false, nil
115141 }
@@ -125,7 +151,7 @@ func Load() (Config, bool, error) {
125151 }
126152
127153 // sanitize drops values this build does not recognise. A config written by a
128- // newer harness, or edited by hand, should degrade to the default for that
154+ // newer Codey, or edited by hand, should degrade to the default for that
129155 // field rather than be forwarded to the API, which would reject the request
130156 // with an error pointing at nothing the user can see.
131157 func sanitize(c Config) Config {
+90−3internal/config/config_test.goView File
@@ -1,7 +1,7 @@
11 package config
22
33 import (
4- "harness/internal/llm"
4+ "codey/internal/llm"
55 "os"
66 "path/filepath"
77 "strings"
@@ -9,12 +9,99 @@ import (
99 )
1010
1111 // isolate points the config at a temp directory, so a test never reads or
12- // writes the developer's real ~/.config/harness.
12+ // writes the developer's real ~/.config/codey.
1313 func isolate(t *testing.T) string {
1414 t.Helper()
1515 dir := t.TempDir()
1616 t.Setenv("XDG_CONFIG_HOME", dir)
17- return filepath.Join(dir, "harness", Filename)
17+ return filepath.Join(dir, "codey", Filename)
18+ }
19+
20+ // legacyPath is the pre-rename location, alongside the isolated new one.
21+ func legacyPath(t *testing.T) string {
22+ t.Helper()
23+ dir := os.Getenv("XDG_CONFIG_HOME")
24+ if dir == "" {
25+ t.Fatal("legacyPath called outside isolate")
26+ }
27+ return filepath.Join(dir, legacyDir, Filename)
28+ }
29+
30+ func writeConfig(t *testing.T, path, body string) {
31+ t.Helper()
32+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
33+ t.Fatal(err)
34+ }
35+ if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
36+ t.Fatal(err)
37+ }
38+ }
39+
40+ // Someone who set Codey up before the rename should not be shown the
41+ // wizard again: with no config at the new path, the old one still counts.
42+ func TestLoadFallsBackToTheLegacyDirectory(t *testing.T) {
43+ isolate(t)
44+ writeConfig(t, legacyPath(t), `{"model":"claude-sonnet-5","setup":"done"}`)
45+
46+ cfg, found, err := Load()
47+ if err != nil {
48+ t.Fatalf("Load: %v", err)
49+ }
50+ if !found {
51+ t.Fatal("a config under the old name was not found")
52+ }
53+ if cfg.Model != "claude-sonnet-5" || !cfg.Done() {
54+ t.Errorf("the old config did not survive the fallback: %+v", cfg)
55+ }
56+ }
57+
58+ // The fallback is a read, not a migration: the old file is left exactly where
59+ // it was, so an older build still finds the settings it wrote.
60+ func TestLoadLeavesTheLegacyFileAlone(t *testing.T) {
61+ path := isolate(t)
62+ old := legacyPath(t)
63+ writeConfig(t, old, `{"model":"claude-sonnet-5"}`)
64+
65+ if _, _, err := Load(); err != nil {
66+ t.Fatalf("Load: %v", err)
67+ }
68+ if _, err := os.Stat(old); err != nil {
69+ t.Errorf("the old config was moved or removed by a read: %v", err)
70+ }
71+ if _, err := os.Stat(path); err == nil {
72+ t.Error("Load wrote to the new path; only Save may do that")
73+ }
74+ }
75+
76+ // Once there is a config under the new name it wins outright, even if a stale
77+ // one is still sitting in the old directory.
78+ func TestNewConfigBeatsTheLegacyOne(t *testing.T) {
79+ path := isolate(t)
80+ writeConfig(t, path, `{"model":"claude-opus-5"}`)
81+ writeConfig(t, legacyPath(t), `{"model":"claude-sonnet-5"}`)
82+
83+ cfg, found, err := Load()
84+ if err != nil || !found {
85+ t.Fatalf("Load: %v found=%v", err, found)
86+ }
87+ if cfg.Model != "claude-opus-5" {
88+ t.Errorf("the stale config won: %+v", cfg)
89+ }
90+ }
91+
92+ // A corrupt file under the old name is still the user's settings, so it is
93+ // reported rather than skipped over as if nothing were there.
94+ func TestLegacyConfigThatCannotBeParsedIsReported(t *testing.T) {
95+ isolate(t)
96+ writeConfig(t, legacyPath(t), "{not json")
97+
98+ _, found, err := Load()
99+ if err == nil {
100+ t.Fatal("a corrupt config under the old name was read as absent")
101+ }
102+ if found {
103+ t.Error("found=true alongside an error")
104+ }
18105 }
19106
20107 func TestLoadReportsMissingWithoutError(t *testing.T) {
+2−2internal/llm/pricing.goView File
@@ -17,7 +17,7 @@ const (
1717 cacheWriteMultiplier = 1.25
1818 )
1919
20- // prices covers the models this harness is likely to be pointed at. A model
20+ // prices covers the models Codey is likely to be pointed at. A model
2121 // that is not listed reports no cost rather than a wrong one.
2222 var prices = map[string]Price{
2323 "claude-opus-5": {InputPerMTok: 5, OutputPerMTok: 25},
@@ -42,7 +42,7 @@ func (u Usage) Cost(model string) (float64, bool) {
4242 float64(u.CacheCreationInputTokens)*p.InputPerMTok*cacheWriteMultiplier) / 1_000_000, true
4343 }
4444
45- // PriceOf reports the list pricing for a model, if this harness knows it.
45+ // PriceOf reports the list pricing for a model, if Codey knows it.
4646 // The picker shows what it knows and says nothing about what it does not.
4747 func PriceOf(model string) (Price, bool) {
4848 p, ok := prices[basePrice(model)]
+1−1internal/llm/types.goView File
@@ -37,7 +37,7 @@ type Message struct {
3737 Content []ContentBlock `json:"content"`
3838 }
3939
40- // ContentBlock is the flattened union of every block type the harness handles.
40+ // ContentBlock is the flattened union of every block type Codey handles.
4141 // Only the fields belonging to Type are serialized, so blocks survive the
4242 // round-trip back to the API — which matters most for thinking blocks, whose
4343 // signature must be echoed back unchanged.
+4−2internal/metrics/log.goView File
@@ -12,10 +12,12 @@ import (
1212 "path/filepath"
1313 "sync"
1414 "time"
15+
16+ "codey/internal/statedir"
1517 )
1618
17- // DefaultPath is where the harness logs unless told otherwise.
18- const DefaultPath = ".harness/events.jsonl"
19+ // DefaultPath is where Codey logs unless told otherwise.
20+ const DefaultPath = statedir.Dir + "/events.jsonl"
1921
2022 // Event is one line of the log. Extra is flattened into the same JSON object so
2123 // the file stays greppable with jq without nested lookups.
+1−1internal/metrics/log_test.goView File
@@ -108,7 +108,7 @@ func TestNilLoggerIsANoOp(t *testing.T) {
108108 }
109109
110110 func TestOpenCreatesParentDirsAndAppends(t *testing.T) {
111- path := filepath.Join(t.TempDir(), ".harness", "events.jsonl")
111+ path := filepath.Join(t.TempDir(), ".codey", "events.jsonl")
112112 for i := 0; i < 2; i++ {
113113 l, err := Open(path, "s")
114114 if err != nil {
+1−1internal/skills/skills.goView File
@@ -37,7 +37,7 @@ type Skill struct {
3737 Source string // "personal" or "project"
3838 }
3939
40- // frontmatter is the subset of SKILL.md's YAML header this harness reads.
40+ // frontmatter is the subset of SKILL.md's YAML header Codey reads.
4141 // Unknown keys are ignored rather than rejected, so a skill written for
4242 // another harness still loads here.
4343 type frontmatter struct {
+49−0internal/statedir/dir.goaddedView File
@@ -0,0 +1,49 @@
1+ // Package statedir names the directory Codey keeps its per-repository state in,
2+ // and moves the one the old name left behind.
3+ //
4+ // The name lives here rather than in the two packages that write into it,
5+ // because a directory name that two packages spell independently is a name
6+ // that will one day be spelled two ways — which is exactly what the rename
7+ // from harness to Codey would have cost.
8+ package statedir
9+
10+ import (
11+ "fmt"
12+ "os"
13+ "path/filepath"
14+ )
15+
16+ // Dir is where sessions and the event log live, relative to the working root.
17+ // LegacyDir is what it was called before the rename.
18+ const (
19+ Dir = ".codey"
20+ LegacyDir = ".harness"
21+ )
22+
23+ // Migrate renames a pre-rename state directory under root, reporting whether
24+ // it moved one. It is called once at startup, before anything opens a file
25+ // inside Dir.
26+ //
27+ // A rename rather than a read-time fallback, because the event log is
28+ // append-only: leaving the old file where it is would split a repository's
29+ // history across two names and quietly break the numbers METRICS.md reports.
30+ //
31+ // Three cases are deliberately left alone. Nothing to move is the common one,
32+ // and must not create an empty directory. Both present means both builds have
33+ // run here, so the new directory is the live state and the old one is history
34+ // — merging them could only lose whichever copy it picked against. And a
35+ // plain file of that name is not our state directory at all.
36+ func Migrate(root string) (bool, error) {
37+ old := filepath.Join(root, LegacyDir)
38+ info, err := os.Lstat(old)
39+ if err != nil || !info.IsDir() {
40+ return false, nil
41+ }
42+ if _, err := os.Lstat(filepath.Join(root, Dir)); err == nil {
43+ return false, nil
44+ }
45+ if err := os.Rename(old, filepath.Join(root, Dir)); err != nil {
46+ return false, fmt.Errorf("state: moving %s to %s: %w", LegacyDir, Dir, err)
47+ }
48+ return true, nil
49+ }
+100−0internal/statedir/dir_test.goaddedView File
@@ -0,0 +1,100 @@
1+ package statedir
2+
3+ import (
4+ "os"
5+ "path/filepath"
6+ "testing"
7+ )
8+
9+ // mkdirWith makes dir under root with one file in it, so a test can tell a
10+ // moved directory from a freshly created one.
11+ func mkdirWith(t *testing.T, root, dir, name, body string) {
12+ t.Helper()
13+ if err := os.MkdirAll(filepath.Join(root, dir), 0o755); err != nil {
14+ t.Fatal(err)
15+ }
16+ if err := os.WriteFile(filepath.Join(root, dir, name), []byte(body), 0o644); err != nil {
17+ t.Fatal(err)
18+ }
19+ }
20+
21+ func read(t *testing.T, path string) string {
22+ t.Helper()
23+ b, err := os.ReadFile(path)
24+ if err != nil {
25+ t.Fatal(err)
26+ }
27+ return string(b)
28+ }
29+
30+ // The event log is append-only and the sessions are what -resume reads, so a
31+ // clone that predates the rename has its state carried over whole rather than
32+ // started again under the new name.
33+ func TestMigrateMovesTheLegacyDirectory(t *testing.T) {
34+ root := t.TempDir()
35+ mkdirWith(t, root, LegacyDir, "events.jsonl", "{\"event\":\"turn\"}\n")
36+
37+ moved, err := Migrate(root)
38+ if err != nil {
39+ t.Fatalf("Migrate: %v", err)
40+ }
41+ if !moved {
42+ t.Fatal("Migrate did not report the move it made")
43+ }
44+ if got := read(t, filepath.Join(root, Dir, "events.jsonl")); got != "{\"event\":\"turn\"}\n" {
45+ t.Errorf("the event log did not survive the move: %q", got)
46+ }
47+ if _, err := os.Stat(filepath.Join(root, LegacyDir)); !os.IsNotExist(err) {
48+ t.Error("the old directory is still there after a move")
49+ }
50+ }
51+
52+ // Nothing to migrate is the common case and must not leave a directory behind:
53+ // an empty .codey in every repo is litter.
54+ func TestMigrateIsAQuietNoOpWithNothingToMove(t *testing.T) {
55+ root := t.TempDir()
56+
57+ moved, err := Migrate(root)
58+ if err != nil || moved {
59+ t.Fatalf("Migrate on a clean tree: moved=%v err=%v", moved, err)
60+ }
61+ if _, err := os.Stat(filepath.Join(root, Dir)); !os.IsNotExist(err) {
62+ t.Error("Migrate created a state directory with nothing to put in it")
63+ }
64+ }
65+
66+ // Both present means a build of each has run here. The new one is the live
67+ // state, so it is never overwritten by the old one.
68+ func TestMigrateLeavesBothAloneWhenBothExist(t *testing.T) {
69+ root := t.TempDir()
70+ mkdirWith(t, root, LegacyDir, "events.jsonl", "old\n")
71+ mkdirWith(t, root, Dir, "events.jsonl", "new\n")
72+
73+ moved, err := Migrate(root)
74+ if err != nil || moved {
75+ t.Fatalf("Migrate with both present: moved=%v err=%v", moved, err)
76+ }
77+ if got := read(t, filepath.Join(root, Dir, "events.jsonl")); got != "new\n" {
78+ t.Errorf("live state was overwritten by the old directory: %q", got)
79+ }
80+ if got := read(t, filepath.Join(root, LegacyDir, "events.jsonl")); got != "old\n" {
81+ t.Errorf("the old directory was disturbed: %q", got)
82+ }
83+ }
84+
85+ // A repo that happens to have a file of that name is not a state directory,
86+ // and renaming it would be taking something that is not ours.
87+ func TestMigrateIgnoresALegacyFile(t *testing.T) {
88+ root := t.TempDir()
89+ if err := os.WriteFile(filepath.Join(root, LegacyDir), []byte("not a directory"), 0o644); err != nil {
90+ t.Fatal(err)
91+ }
92+
93+ moved, err := Migrate(root)
94+ if err != nil || moved {
95+ t.Fatalf("Migrate over a file: moved=%v err=%v", moved, err)
96+ }
97+ if got := read(t, filepath.Join(root, LegacyDir)); got != "not a directory" {
98+ t.Errorf("the file was touched: %q", got)
99+ }
100+ }
+4−1internal/tools/glob.goView File
@@ -10,6 +10,8 @@ import (
1010 "strings"
1111
1212 "github.com/bmatcuk/doublestar/v4"
13+
14+ "codey/internal/statedir"
1315 )
1416
1517 // maxResults caps how many paths or matches a search tool returns. The count of
@@ -23,7 +25,8 @@ var skipDirs = map[string]bool{
2325 ".git": true, ".hg": true, ".svn": true,
2426 "node_modules": true, "vendor": true, "target": true,
2527 ".venv": true, "venv": true, "__pycache__": true,
26- ".harness": true, ".next": true, ".cache": true,
28+ statedir.Dir: true, statedir.LegacyDir: true,
29+ ".next": true, ".cache": true,
2730 }
2831
2932 // GlobArgs is the glob input.
+2−2internal/tools/read.goView File
@@ -199,9 +199,9 @@ func jailWith(root string, extra []string, path string) (string, error) {
199199 }
200200 }
201201 if len(extra) > 0 {
202- return "", fmt.Errorf("%s is outside the working root %s and every skill directory; this harness only touches files under the root", path, realRoot)
202+ return "", fmt.Errorf("%s is outside the working root %s and every skill directory; codey only touches files under the root", path, realRoot)
203203 }
204- return "", fmt.Errorf("%s is outside the working root %s; this harness only touches files under the root", path, realRoot)
204+ return "", fmt.Errorf("%s is outside the working root %s; codey only touches files under the root", path, realRoot)
205205 }
206206
207207 // realpath resolves symlinks, falling back to a lexical clean for a path that
+2−2internal/tools/registry.goView File
@@ -1,7 +1,7 @@
11 // Package tools defines the tool interface the agent loop dispatches against.
22 //
33 // Tools return (string, error). The error is text for the model to read, not an
4- // exception for the harness to swallow: a failed edit is a normal event in the
4+ // exception for Codey to swallow: a failed edit is a normal event in the
55 // transcript.
66 package tools
77
@@ -13,7 +13,7 @@ import (
1313 "sync"
1414 "time"
1515
16- "harness/internal/metrics"
16+ "codey/internal/metrics"
1717
1818 "github.com/invopop/jsonschema"
1919 )
+1−1internal/tools/registry_test.goView File
@@ -8,7 +8,7 @@ import (
88 "strings"
99 "testing"
1010
11- "harness/internal/metrics"
11+ "codey/internal/metrics"
1212 )
1313
1414 type echoArgs struct {
+7−7internal/tools/search_test.goView File
@@ -15,11 +15,11 @@ import (
1515 func searchRepo(t *testing.T) string {
1616 t.Helper()
1717 dir := t.TempDir()
18- writeFile(t, dir, "go.mod", "module harness\n")
19- writeFile(t, dir, "cmd/harness/main.go", "package main\n\nfunc main() { run() }\n")
18+ writeFile(t, dir, "go.mod", "module codey\n")
19+ writeFile(t, dir, "cmd/codey/main.go", "package main\n\nfunc main() { run() }\n")
2020 writeFile(t, dir, "internal/agent/loop.go", "package agent\n\nfunc Run() {}\n")
2121 writeFile(t, dir, "internal/agent/loop_test.go", "package agent\n\nfunc TestRun(t *testing.T) {}\n")
22- writeFile(t, dir, "README.md", "# harness\n\nRun it.\n")
22+ writeFile(t, dir, "README.md", "# codey\n\nRun it.\n")
2323 writeFile(t, dir, "node_modules/junk/index.js", "function run() {}\n")
2424 writeFile(t, dir, ".git/config", "[core]\n\trun = true\n")
2525 if err := os.WriteFile(filepath.Join(dir, "bin.dat"), []byte{0x00, 0x01, 'r', 'u', 'n'}, 0o644); err != nil {
@@ -54,19 +54,19 @@ func TestGlob(t *testing.T) {
5454 {
5555 name: "bare pattern matches at any depth",
5656 args: GlobArgs{Pattern: "*.go"},
57- want: []string{"cmd/harness/main.go", "internal/agent/loop.go", "internal/agent/loop_test.go"},
57+ want: []string{"cmd/codey/main.go", "internal/agent/loop.go", "internal/agent/loop_test.go"},
5858 unwanted: []string{"go.mod", "README.md"},
5959 },
6060 {
6161 name: "pattern with a slash is anchored to the subtree",
6262 args: GlobArgs{Pattern: "internal/**/*_test.go"},
6363 want: []string{"internal/agent/loop_test.go"},
64- unwanted: []string{"cmd/harness/main.go"},
64+ unwanted: []string{"cmd/codey/main.go"},
6565 },
6666 {
6767 name: "path narrows the search",
6868 args: GlobArgs{Pattern: "*.go", Path: "cmd"},
69- want: []string{"cmd/harness/main.go"},
69+ want: []string{"cmd/codey/main.go"},
7070 unwanted: []string{"internal/agent/loop.go"},
7171 },
7272 {
@@ -171,7 +171,7 @@ func TestGrep(t *testing.T) {
171171 {
172172 name: "path narrows the search",
173173 args: GrepArgs{Pattern: "package", Path: "cmd"},
174- want: []string{"cmd/harness/main.go"},
174+ want: []string{"cmd/codey/main.go"},
175175 },
176176 }
177177 // Both engines must produce identical output, or eval numbers would depend
+1−1internal/tools/skill.goView File
@@ -7,7 +7,7 @@ import (
77 "sort"
88 "strings"
99
10- "harness/internal/skills"
10+ "codey/internal/skills"
1111 )
1212
1313 // SkillArgs is the skill input. The name is constrained to the discovered
+1−1internal/tools/skill_test.goView File
@@ -5,7 +5,7 @@ import (
55 "strings"
66 "testing"
77
8- "harness/internal/skills"
8+ "codey/internal/skills"
99 )
1010
1111 func testSkills() []skills.Skill {
+5−5internal/tui/anim_test.goView File
@@ -181,16 +181,16 @@ func TestIgniteFinishesEveryLetter(t *testing.T) {
181181
182182 // At t=1 every letter must be painted the target colour exactly: the same
183183 // sequence lipgloss emits for a plain render in it.
184- lit := ignite("harness", colFaint, colCoral, 1, false)
184+ lit := ignite("codey", colFaint, colCoral, 1, false)
185185 want := termenv.TrueColor.Color(colCoral.Dark).Sequence(false)
186- if n := strings.Count(lit, want); n != len("harness") {
187- t.Errorf("%d of %d letters reached the target colour at t=1", n, len("harness"))
186+ if n := strings.Count(lit, want); n != len("codey") {
187+ t.Errorf("%d of %d letters reached the target colour at t=1", n, len("codey"))
188188 }
189189
190- if got := stripANSI(lit); got != "harness" {
190+ if got := stripANSI(lit); got != "codey" {
191191 t.Errorf("ignite changed the text to %q", got)
192192 }
193- if got := stripANSI(ignite("harness", colFaint, colCoral, 0, false)); got != "harness" {
193+ if got := stripANSI(ignite("codey", colFaint, colCoral, 0, false)); got != "codey" {
194194 t.Errorf("at t=0 the text is %q, want it present but unlit", got)
195195 }
196196 }
+3−3internal/tui/bridge.goView File
@@ -5,9 +5,9 @@ import (
55
66 tea "github.com/charmbracelet/bubbletea"
77
8- "harness/internal/agent"
9- "harness/internal/approval"
10- "harness/internal/llm"
8+ "codey/internal/agent"
9+ "codey/internal/approval"
10+ "codey/internal/llm"
1111 )
1212
1313 // The agent loop runs on its own goroutine and must not know a UI exists. It
+2−2internal/tui/bridge_test.goView File
@@ -9,8 +9,8 @@ import (
99
1010 tea "github.com/charmbracelet/bubbletea"
1111
12- "harness/internal/agent"
13- "harness/internal/approval"
12+ "codey/internal/agent"
13+ "codey/internal/approval"
1414 )
1515
1616 // recorder stands in for tea.Program: it captures what the bridge sends
+1−1internal/tui/colour_test.goView File
@@ -8,7 +8,7 @@ import (
88 "github.com/charmbracelet/lipgloss"
99 "github.com/muesli/termenv"
1010
11- "harness/internal/llm"
11+ "codey/internal/llm"
1212 )
1313
1414 // forceColour makes styles emit real escape sequences. Without it the test
+1−1internal/tui/commands.goView File
@@ -31,7 +31,7 @@ var builtins = []command{
3131 {name: "clear", desc: "empty the transcript"},
3232 {name: "mouse", desc: "hand the mouse back to the terminal, or take it again"},
3333 {name: "allowed", desc: "what is permanently allowed this session"},
34- {name: "exit", desc: "leave the harness"},
34+ {name: "exit", desc: "leave Codey"},
3535 }
3636
3737 // commands is the catalogue: the built-ins, then every discovered skill.
+1−1internal/tui/compact_test.goView File
@@ -5,7 +5,7 @@ import (
55 "strings"
66 "testing"
77
8- "harness/internal/agent"
8+ "codey/internal/agent"
99 )
1010
1111 // /compact rewrites the history the in-flight request is built from, so it has
+1−1internal/tui/complete_test.goView File
@@ -6,7 +6,7 @@ import (
66
77 tea "github.com/charmbracelet/bubbletea"
88
9- "harness/internal/skills"
9+ "codey/internal/skills"
1010 )
1111
1212 // The menu belongs to a slash command being typed, and to nothing else: a
+1−1internal/tui/effort.goView File
@@ -7,7 +7,7 @@ import (
77 tea "github.com/charmbracelet/bubbletea"
88 "github.com/charmbracelet/lipgloss"
99
10- "harness/internal/llm"
10+ "codey/internal/llm"
1111 )
1212
1313 // defaultEffort is where the marker sits when nothing has been chosen. The
+1−1internal/tui/effort_test.goView File
@@ -6,7 +6,7 @@ import (
66
77 tea "github.com/charmbracelet/bubbletea"
88
9- "harness/internal/llm"
9+ "codey/internal/llm"
1010 )
1111
1212 // levelAt is where a level sits on the track, so tests can talk about
+1−1internal/tui/format.goView File
@@ -7,7 +7,7 @@ import (
77 "strings"
88 "unicode"
99
10- "harness/internal/llm"
10+ "codey/internal/llm"
1111 )
1212
1313 // human renders a token count the way a status bar wants to read it.
+2−2internal/tui/local.goView File
@@ -10,8 +10,8 @@ import (
1010 tea "github.com/charmbracelet/bubbletea"
1111 "github.com/charmbracelet/lipgloss"
1212
13- "harness/internal/llm"
14- "harness/internal/tools"
13+ "codey/internal/llm"
14+ "codey/internal/tools"
1515 )
1616
1717 // localMode is which interpreter the prompt box is pointed at. Off is the
+4−4internal/tui/local_test.goView File
@@ -9,10 +9,10 @@ import (
99 "github.com/charmbracelet/bubbles/spinner"
1010 tea "github.com/charmbracelet/bubbletea"
1111
12- "harness/internal/agent"
13- "harness/internal/approval"
14- "harness/internal/llm"
15- "harness/internal/tools"
12+ "codey/internal/agent"
13+ "codey/internal/approval"
14+ "codey/internal/llm"
15+ "codey/internal/tools"
1616 )
1717
1818 // newLocalModel builds a model with a real agent behind it, so a local
+6−6internal/tui/look_test.goView File
@@ -8,8 +8,8 @@ import (
88 tea "github.com/charmbracelet/bubbletea"
99 "github.com/charmbracelet/lipgloss"
1010
11- "harness/internal/approval"
12- "harness/internal/llm"
11+ "codey/internal/approval"
12+ "codey/internal/llm"
1313 )
1414
1515 func TestPrettyModel(t *testing.T) {
@@ -60,7 +60,7 @@ func TestSplashDropsMascotWhenNarrow(t *testing.T) {
6060 if strings.Contains(narrow, mascot[2]) {
6161 t.Error("mascot survived into a terminal too narrow for it")
6262 }
63- if !strings.Contains(narrow, "harness") {
63+ if !strings.Contains(narrow, "codey") {
6464 t.Error("narrow welcome block lost its name")
6565 }
6666 }
@@ -262,10 +262,10 @@ func TestNewModelOpensWithTheWelcomeBlock(t *testing.T) {
262262 Banner: []string{"resumed session abc"},
263263 })
264264 out := m.tr.render()
265- if !strings.Contains(out, "harness") {
265+ if !strings.Contains(out, "codey") {
266266 t.Error("welcome block missing")
267267 }
268- if i, j := strings.Index(out, "harness"), strings.Index(out, "resumed session abc"); i < 0 || j < 0 || i > j {
268+ if i, j := strings.Index(out, "codey"), strings.Index(out, "resumed session abc"); i < 0 || j < 0 || i > j {
269269 t.Error("the banner should follow the welcome block, not precede it")
270270 }
271271 }
@@ -288,7 +288,7 @@ func TestDiffStatCountsEditsNotHeaders(t *testing.T) {
288288 }
289289 }
290290
291- // Cache reads are the number this harness exists to move, so they belong on
291+ // Cache reads are the number Codey exists to move, so they belong on
292292 // the status bar rather than only in the metrics file.
293293 func TestStatusReportsCacheReadsSeparately(t *testing.T) {
294294 m := newTestModel(t)
+1−1internal/tui/markdown.goView File
@@ -229,7 +229,7 @@ func chromaTheme(t theme, p termenv.Profile) string {
229229 if p == termenv.Ascii {
230230 return ""
231231 }
232- name := "harness-" + t.name
232+ name := "codey-" + t.name
233233 if chromaThemes[name] {
234234 return name
235235 }
+9−9internal/tui/model.goView File
@@ -14,11 +14,11 @@ import (
1414 tea "github.com/charmbracelet/bubbletea"
1515 "github.com/charmbracelet/lipgloss"
1616
17- "harness/internal/agent"
18- "harness/internal/approval"
19- "harness/internal/config"
20- "harness/internal/llm"
21- "harness/internal/skills"
17+ "codey/internal/agent"
18+ "codey/internal/approval"
19+ "codey/internal/config"
20+ "codey/internal/llm"
21+ "codey/internal/skills"
2222 )
2323
2424 // Options wires the TUI to an already-built agent. The TUI owns no policy of
@@ -163,7 +163,7 @@ type model struct {
163163 // say so while the agent unwinds.
164164 interrupting bool
165165
166- // The mouse. mouseOn is whether the harness is asking the terminal for
166+ // The mouse. mouseOn is whether Codey is asking the terminal for
167167 // mouse reporting at all: it costs the terminal's own text selection, so
168168 // /mouse gives it back. sel is the selection that replaces it, copied is
169169 // what the chrome says about the last copy, and the three click fields
@@ -174,7 +174,7 @@ type model struct {
174174 clicks int
175175 clickAt cell
176176 clickTime time.Time
177- // copyFn is how text leaves the harness. It is a field so a test can
177+ // copyFn is how text leaves Codey. It is a field so a test can
178178 // watch what was copied without a clipboard, or a terminal, being there.
179179 copyFn func(string) error
180180
@@ -800,7 +800,7 @@ func (m *model) setEffort(level string) tea.Cmd {
800800
801801 // isSkill reports whether a slash command names a discovered skill. Matching
802802 // here rather than sending every unrecognised slash to the model means a typo
803- // is caught by the harness instead of puzzled over by the agent.
803+ // is caught by Codey instead of puzzled over by the agent.
804804 func (m *model) isSkill(line string) bool {
805805 name := strings.TrimPrefix(line, "/")
806806 if i := strings.IndexAny(name, " \t"); i >= 0 {
@@ -1036,7 +1036,7 @@ func (m *model) status() string {
10361036 }
10371037
10381038 // Cache reads are reported separately from fresh input: the gap between
1039- // the two is the whole point of the caching this harness measures.
1039+ // the two is the whole point of the caching Codey measures.
10401040 u := m.usage
10411041 if u.InputTokens+u.OutputTokens+u.CacheReadInputTokens > 0 {
10421042 io := fmt.Sprintf("↑ %s", human(u.InputTokens))
+1−1internal/tui/model_picker.goView File
@@ -10,7 +10,7 @@ import (
1010 tea "github.com/charmbracelet/bubbletea"
1111 "github.com/charmbracelet/lipgloss"
1212
13- "harness/internal/llm"
13+ "codey/internal/llm"
1414 )
1515
1616 // modelFetchTimeout bounds the wait. Someone is sitting in front of a picker
+2−2internal/tui/model_picker_test.goView File
@@ -10,7 +10,7 @@ import (
1010
1111 tea "github.com/charmbracelet/bubbletea"
1212
13- "harness/internal/llm"
13+ "codey/internal/llm"
1414 )
1515
1616 func someModels() []llm.Model {
@@ -22,7 +22,7 @@ func someModels() []llm.Model {
2222 }
2323
2424 // /model opens on a wait, because the list comes from the endpoint rather
25- // than from a table compiled into the harness.
25+ // than from a table compiled into Codey.
2626 func TestModelPickerAsksTheEndpoint(t *testing.T) {
2727 m := newTestModel(t)
2828 m.submit("/model")
+5−5internal/tui/model_test.goView File
@@ -7,10 +7,10 @@ import (
77
88 tea "github.com/charmbracelet/bubbletea"
99
10- "harness/internal/approval"
11- "harness/internal/config"
12- "harness/internal/llm"
13- "harness/internal/skills"
10+ "codey/internal/approval"
11+ "codey/internal/config"
12+ "codey/internal/llm"
13+ "codey/internal/skills"
1414 )
1515
1616 // newTestModel builds a model far enough to drive Update/View without a
@@ -457,7 +457,7 @@ func TestFirstRunOpensTheWizard(t *testing.T) {
457457 if m.wiz == nil {
458458 t.Fatal("a first run did not open the wizard")
459459 }
460- if !strings.Contains(m.View(), "harness") {
460+ if !strings.Contains(m.View(), "codey") {
461461 t.Error("the wizard is open but its screen is not what View returns")
462462 }
463463 }
+2−2internal/tui/mouse.goView File
@@ -7,7 +7,7 @@ import (
77 tea "github.com/charmbracelet/bubbletea"
88 "github.com/charmbracelet/lipgloss"
99
10- "harness/internal/llm"
10+ "codey/internal/llm"
1111 )
1212
1313 // The mouse is handled here the way keys are handled in onKey: one entry
@@ -345,7 +345,7 @@ func (m *model) onChromeClick(kind sectionKind, row, x int) (tea.Model, tea.Cmd)
345345 // toggleMouse hands the mouse to the terminal or takes it back. With
346346 // reporting on, the terminal stops sending drags to its own selection and
347347 // sends them here instead — which is the whole feature, and also the cost of
348- // it, because the terminal's selection is what works when the harness's own
348+ // it, because the terminal's selection is what works when Codey's own
349349 // is not what you want.
350350 func (m *model) toggleMouse() tea.Cmd {
351351 m.mouseOn = !m.mouseOn
+3−3internal/tui/mouse_test.goView File
@@ -15,8 +15,8 @@ import (
1515
1616 "github.com/muesli/termenv"
1717
18- "harness/internal/approval"
19- "harness/internal/llm"
18+ "codey/internal/approval"
19+ "codey/internal/llm"
2020 )
2121
2222 func press(x, y int) tea.MouseMsg {
@@ -381,7 +381,7 @@ func TestClickingTheModeLineCyclesApprovals(t *testing.T) {
381381 }
382382
383383 // /mouse hands the mouse back to the terminal, which is the only way to get
384- // the terminal's own selection back for a copy the harness cannot do.
384+ // the terminal's own selection back for a copy Codey cannot do.
385385 func TestMouseCanBeHandedBackToTheTerminal(t *testing.T) {
386386 m, clipboard := mouseModel(t, "the quick brown fox")
387387
+1−1internal/tui/selection.goView File
@@ -7,7 +7,7 @@ import (
77 "github.com/charmbracelet/x/ansi"
88 )
99
10- // A selection is the harness's own answer to something the terminal normally
10+ // A selection is Codey's own answer to something the terminal normally
1111 // does for us. Asking for mouse reporting takes the terminal's text selection
1212 // away — it stops sending drags to the clipboard and starts sending them here
1313 // — so the alternative to drawing and copying a selection ourselves is not
+2−2internal/tui/settings.goView File
@@ -7,7 +7,7 @@ import (
77 tea "github.com/charmbracelet/bubbletea"
88 "github.com/charmbracelet/lipgloss"
99
10- "harness/internal/config"
10+ "codey/internal/config"
1111 )
1212
1313 // The settings panel: /settings. One row per thing the config file holds,
@@ -284,7 +284,7 @@ func (m *model) backFromPicker() phase {
284284 // Approvals Ask me first recommended
285285 // Theme ember
286286 // the model every turn is sent to · enter to pick from api.anthropic.com
287- // saved to ~/.config/harness/config.json
287+ // saved to ~/.config/codey/config.json
288288 func (m *model) settingsView() string {
289289 width := maxInt(1, m.width)
290290
+3−3internal/tui/settings_test.goView File
@@ -9,9 +9,9 @@ import (
99 tea "github.com/charmbracelet/bubbletea"
1010 "github.com/charmbracelet/lipgloss"
1111
12- "harness/internal/approval"
13- "harness/internal/config"
14- "harness/internal/llm"
12+ "codey/internal/approval"
13+ "codey/internal/config"
14+ "codey/internal/llm"
1515 )
1616
1717 // newSettingsModel builds a chat model with a config already on disk and a
+13−7internal/tui/splash.goView File
@@ -6,13 +6,19 @@ import (
66 "github.com/charmbracelet/lipgloss"
77 )
88
9- // mascot is drawn with half-blocks from the box-drawing range, so it needs no
10- // patched font and degrades to something recognisable in a plain xterm.
9+ // mascot is Codey: a gopher, because this is Go, wearing the two big eyes the
10+ // half-block grid can just about afford.
11+ //
12+ // It is drawn with half-blocks from the box-drawing range, so it needs no
13+ // patched font and degrades to something recognisable in a plain xterm. Every
14+ // row is eight cells wide — the wizard assembles it a row at a time and the
15+ // splash lays it out in a column, so a ragged row would show up as a wobble in
16+ // both.
1117 var mascot = []string{
12- " ▄▄▄▄▄▄ ",
13- "▐▀█▀█▀█▌",
14- "▐██████▌",
15- " ▀▀ ▀▀ ",
18+ "▄██████▄",
19+ "█ ██ █",
20+ "███▀▀███",
21+ " ▀█▀▀█▀ ",
1622 }
1723
1824 const mascotGap = " "
@@ -24,7 +30,7 @@ const mascotGap = " "
2430 // On a narrow terminal the mascot is dropped rather than sliced: half a mascot
2531 // reads as a rendering bug, where the three text lines alone read as a choice.
2632 func splash(o Options, width int) string {
27- name := styHeading.Render("harness")
33+ name := styHeading.Render("codey")
2834 if o.Version != "" {
2935 name += " " + styDim.Render(o.Version)
3036 }
+1−1internal/tui/styles.goView File
@@ -78,7 +78,7 @@ var (
7878 styModal lipgloss.Style
7979
8080 // A mouse selection. Asking for mouse reporting costs the terminal's own
81- // selection, so this is the harness's replacement for it, and it has to
81+ // selection, so this is Codey's replacement for it, and it has to
8282 // read as a selection at a glance in all four themes.
8383 stySelect lipgloss.Style
8484 )
+4−4internal/tui/testdata/mockapi/main.goView File
@@ -5,7 +5,7 @@
55 // fixture, not part of the build.
66 //
77 // go run ./internal/tui/testdata/mockapi &
8- // ./harness -C /tmp/demo -base-url http://127.0.0.1:8899 -api-key mock
8+ // ./codey -C /tmp/demo -base-url http://127.0.0.1:8899 -api-key mock
99 //
1010 // The script is fixed: the first turn streams a sentence and asks to run a
1111 // shell command, which makes the TUI raise an approval prompt; once a
@@ -17,7 +17,7 @@
1717 // size of the request, so the gauge climbs as you talk; pair it with a small
1818 // -context-window to reach a threshold in a handful of turns:
1919 //
20- // ./harness -base-url http://127.0.0.1:8899 -api-key mock -context-window 30000
20+ // ./codey -base-url http://127.0.0.1:8899 -api-key mock -context-window 30000
2121 //
2222 // Flags: -addr to move it, -fail to make the command exit non-zero, -slow to
2323 // stretch the stream out for watching the spinner and ctrl-c.
@@ -50,7 +50,7 @@ func main() {
5050 func handle(w http.ResponseWriter, r *http.Request) {
5151 body, _ := io.ReadAll(r.Body)
5252
53- // Compaction is the one request the harness does not stream, so it cannot
53+ // Compaction is the one request Codey does not stream, so it cannot
5454 // be answered with the SSE script below.
5555 if strings.Contains(string(body), "Summarize the conversation above") {
5656 w.Header().Set("Content-Type", "application/json")
@@ -66,7 +66,7 @@ func handle(w http.ResponseWriter, r *http.Request) {
6666 return
6767 }
6868
69- // A tool_result in the history means the harness ran (or refused) our
69+ // A tool_result in the history means Codey ran (or refused) our
7070 // command and fed the outcome back, so the next turn is the last one.
7171 final := strings.Contains(string(body), "tool_result")
7272
+2−2internal/tui/theme.goView File
@@ -3,7 +3,7 @@ package tui
33 import (
44 "github.com/charmbracelet/lipgloss"
55
6- "harness/internal/config"
6+ "codey/internal/config"
77 )
88
99 // A theme is a palette, not a set of styles: the styles in styles.go are
@@ -103,7 +103,7 @@ var themes = []theme{{
103103 selBg: lipgloss.AdaptiveColor{Light: "#d0d0d0", Dark: "#484848"},
104104 }}
105105
106- // defaultTheme is what an unconfigured harness looks like, and the fallback
106+ // defaultTheme is what an unconfigured Codey looks like, and the fallback
107107 // for a theme name this build does not have.
108108 const defaultTheme = "ember"
109109
+2−2internal/tui/wizard.goView File
@@ -8,8 +8,8 @@ import (
88 "github.com/charmbracelet/bubbles/textinput"
99 tea "github.com/charmbracelet/bubbletea"
1010
11- "harness/internal/config"
12- "harness/internal/llm"
11+ "codey/internal/config"
12+ "codey/internal/llm"
1313 )
1414
1515 // The setup wizard: a full-screen, one-question-at-a-time interface that owns
+2−2internal/tui/wizard_test.goView File
@@ -7,8 +7,8 @@ import (
77 tea "github.com/charmbracelet/bubbletea"
88 "github.com/charmbracelet/lipgloss"
99
10- "harness/internal/config"
11- "harness/internal/llm"
10+ "codey/internal/config"
11+ "codey/internal/llm"
1212 )
1313
1414 // newTestWizard builds a wizard on a comfortable screen with the catalogue
+5−5internal/tui/wizard_view.goView File
@@ -10,7 +10,7 @@ import (
1010
1111 "github.com/charmbracelet/lipgloss"
1212
13- "harness/internal/config"
13+ "codey/internal/config"
1414 )
1515
1616 // ---- view ---------------------------------------------------------------
@@ -76,7 +76,7 @@ func (w *wizard) card() string {
7676 // header is the mark and the word "setup", present on every step so the screen
7777 // always says what it is.
7878 func (w *wizard) header(width int) string {
79- left := styHeading.Render("harness") + styDim.Render(" setup")
79+ left := styHeading.Render("codey") + styDim.Render(" setup")
8080 right := styFaint.Render(fmt.Sprintf("step %d of %d", int(w.step), railSteps))
8181 return justify(left, right, width)
8282 }
@@ -287,9 +287,9 @@ func (w *wizard) introCard() string {
287287 }
288288 b.WriteString("\n")
289289
290- // "harness" ignites left to right once the mark is up.
290+ // "codey" ignites left to right once the mark is up.
291291 titleT := clamp01((t*float64(introFrames) - 6) / 18)
292- b.WriteString(ignite("harness", colFaint, colCoral, titleT, true) + "\n")
292+ b.WriteString(ignite("codey", colFaint, colCoral, titleT, true) + "\n")
293293
294294 // The tagline types itself underneath. The short form is not a truncation
295295 // of the long one: a cut sentence reads as a bug, where a shorter sentence
@@ -337,7 +337,7 @@ func (w *wizard) doneCard() string {
337337 }
338338
339339 var b strings.Builder
340- b.WriteString(styHeading.Render("harness") + styDim.Render(" setup") + "\n\n")
340+ b.WriteString(styHeading.Render("codey") + styDim.Render(" setup") + "\n\n")
341341
342342 for i, r := range rows {
343343 // Staggered: each row starts three frames after the one above, so
+1−1internal/ui/approve.goView File
@@ -7,7 +7,7 @@ import (
77 "io"
88 "strings"
99
10- "harness/internal/approval"
10+ "codey/internal/approval"
1111 )
1212
1313 // Approver reads approval decisions from a terminal.
+2−2internal/ui/render.goView File
@@ -11,8 +11,8 @@ import (
1111 "sort"
1212 "strings"
1313
14- "harness/internal/agent"
15- "harness/internal/llm"
14+ "codey/internal/agent"
15+ "codey/internal/llm"
1616 )
1717
1818 // ANSI codes, blanked when the output is not a terminal or NO_COLOR is set.
+2−2internal/ui/render_test.goView File
@@ -6,8 +6,8 @@ import (
66 "strings"
77 "testing"
88
9- "harness/internal/agent"
10- "harness/internal/llm"
9+ "codey/internal/agent"
10+ "codey/internal/llm"
1111 )
1212
1313 func TestSummarizeInputIsSortedAndBounded(t *testing.T) {