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>
Showing 67 changed files with 537 additions and 209 deletions.
| @@ -1,3 +1,8 @@ | ||
| 1 | - /.harness/ | |
| 1 | + /.codey/ | |
| 2 | 2 | /evals/runs/ |
| 3 | + /codey | |
| 4 | + /.superpowers/ | |
| 5 | + | |
| 6 | + # Left behind by builds from before the rename. | |
| 7 | + /.harness/ | |
| 3 | 8 | /harness |
| @@ -18,7 +18,7 @@ against a live model, not by anything written in advance. | ||
| 18 | 18 | ## Reading the numbers |
| 19 | 19 | |
| 20 | 20 | - **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 | |
| 22 | 22 | averaging that away hides the thing worth fixing. |
| 23 | 23 | - **Edit success** is `edit_file` calls that returned a diff, over all |
| 24 | 24 | `edit_file` calls. `write_file` is deliberately a separate tool so a |
| @@ -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 | |
| 2 | 2 | |
| 3 | 3 | Target: a harness that can be pointed at a real Go or TypeScript repo, told |
| 4 | 4 | "make the failing test pass," and actually do it — with numbers proving how |
| @@ -19,8 +19,8 @@ comes first.** (See METRICS.md.) | ||
| 19 | 19 | ## Repo layout |
| 20 | 20 | |
| 21 | 21 | ``` |
| 22 | - harness/ | |
| 23 | - cmd/harness/main.go # flag parsing, wiring, REPL entry | |
| 22 | + codey/ | |
| 23 | + cmd/codey/main.go # flag parsing, wiring, REPL entry | |
| 24 | 24 | internal/ |
| 25 | 25 | llm/ |
| 26 | 26 | client.go # HTTP + SSE against the Messages API |
| @@ -70,7 +70,7 @@ rather than hand-writing JSON Schema. Hand-written schemas drift from the | ||
| 70 | 70 | structs and you won't notice until the model sends a field you don't parse. |
| 71 | 71 | |
| 72 | 72 | 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. | |
| 74 | 74 | Retrofitting instrumentation is how projects end up with no numbers. |
| 75 | 75 | |
| 76 | 76 | ## Phase 1 — The minimal loop |
| @@ -147,7 +147,7 @@ code-search story; you do not need embeddings. | ||
| 147 | 147 | New files only, or full rewrites the model explicitly chose. Keep it separate |
| 148 | 148 | from edit so your metrics don't blend them. |
| 149 | 149 | |
| 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 | |
| 151 | 151 | git clone, or inside a container. There is no approval layer until Phase 6. |
| 152 | 152 | Assume it will run `rm -rf` on something eventually. |
| 153 | 153 | |
| @@ -249,7 +249,7 @@ correction without losing the session. | ||
| 249 | 249 | - Show diffs before applying, with color. |
| 250 | 250 | - Show running token and cost totals per session. Cheap to add, and it changes |
| 251 | 251 | 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`. | |
| 253 | 253 | |
| 254 | 254 | Keep `agent` free of terminal dependencies. The eval runner depends on that. |
| 255 | 255 | |
| @@ -1,7 +1,7 @@ | ||
| 1 | - # harness | |
| 1 | + # codey | |
| 2 | 2 | |
| 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. | |
| 5 | 5 | |
| 6 | 6 | The code is the byproduct. The deliverable is [METRICS.md](METRICS.md). |
| 7 | 7 | [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). | ||
| 9 | 9 | ## Quick start |
| 10 | 10 | |
| 11 | 11 | ```bash |
| 12 | - go build -o harness ./cmd/harness | |
| 12 | + go build -o codey ./cmd/codey | |
| 13 | 13 | |
| 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 | |
| 16 | 16 | ``` |
| 17 | 17 | |
| 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 | |
| 19 | 19 | borrows the OAuth credential from `~/.claude/.credentials.json` when nothing |
| 20 | 20 | else supplied one. Otherwise, or to use a different account: |
| 21 | 21 | |
| @@ -27,7 +27,7 @@ A different endpoint — a gateway, a proxy, a local server speaking the Message | ||
| 27 | 27 | API: |
| 28 | 28 | |
| 29 | 29 | ```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 . | |
| 31 | 31 | # or ANTHROPIC_BASE_URL / ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN |
| 32 | 32 | ``` |
| 33 | 33 | |
| @@ -41,7 +41,7 @@ points. | ||
| 41 | 41 | The Claude Code login is the last resort, tried only when no flag and no |
| 42 | 42 | environment variable supplied a credential, and only against Anthropic's own |
| 43 | 43 | 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` | |
| 45 | 45 | skips it entirely. Note that the credential is issued to Claude Code; using it |
| 46 | 46 | from another client is outside Anthropic's terms for it, which API keys and |
| 47 | 47 | 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 | ||
| 70 | 70 | nothing if the next request has no room to be sent, so the floor folds anyway |
| 71 | 71 | and settles for a one-message tail. `/compact` follows the same rule past the |
| 72 | 72 | 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 | |
| 74 | 74 | request fail as a bare API error. |
| 75 | 75 | |
| 76 | 76 | `-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 | ||
| 84 | 84 | ## The TUI |
| 85 | 85 | |
| 86 | 86 | 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: | |
| 89 | 102 | |
| 90 | 103 | ``` |
| 91 | 104 | ⏺ 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 | ||
| 189 | 202 | than the screen; it keeps going while the pointer moves, because a terminal |
| 190 | 203 | reporting button events says nothing about a hand held still. |
| 191 | 204 | |
| 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 | |
| 193 | 206 | of its own to roll — so asking for mouse reporting is what makes the wheel work |
| 194 | 207 | at all. The cost is the terminal's own text selection, which stops being the |
| 195 | 208 | 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, | ||
| 334 | 347 | still gets asked the ones it is missing. |
| 335 | 348 | |
| 336 | 349 | ``` |
| 337 | - harness setup step 1 of 4 | |
| 350 | + codey setup step 1 of 4 | |
| 338 | 351 | ◆━━━━━━━━━━━━━━━◇───────────────◇───────────────◇ |
| 339 | 352 | |
| 340 | 353 | Which model should the agent use? |
| @@ -368,7 +381,7 @@ The theme step previews by applying the palette for real — reading a swatch | ||
| 368 | 381 | tells you nothing about what a transcript looks like in it — and it recolours |
| 369 | 382 | the whole TUI, pickers and slider included. Cancelling puts the old one back. |
| 370 | 383 | |
| 371 | - Answers land in `~/.config/harness/config.json` (`XDG_CONFIG_HOME` is | |
| 384 | + Answers land in `~/.config/codey/config.json` (`XDG_CONFIG_HOME` is | |
| 372 | 385 | honoured): |
| 373 | 386 | |
| 374 | 387 | ```json |
| @@ -419,6 +432,29 @@ ignored, and is never overwritten until you finish the wizard. | ||
| 419 | 432 | on"; they do not write to the file. The two screens that do are the wizard and |
| 420 | 433 | `/settings`. |
| 421 | 434 | |
| 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 | + | |
| 422 | 458 | ### /settings |
| 423 | 459 | |
| 424 | 460 | `/settings` is the same four settings without the interview: one row each, |
| @@ -431,7 +467,7 @@ changed in place, applied as you move. | ||
| 431 | 467 | Approvals Ask me first recommended |
| 432 | 468 | Theme ember |
| 433 | 469 | claude-opus-5 · enter to pick from anthropic |
| 434 | - saved to ~/.config/harness/config.json | |
| 470 | + saved to ~/.config/codey/config.json | |
| 435 | 471 | ``` |
| 436 | 472 | |
| 437 | 473 | `←→` 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. | ||
| 542 | 578 | |
| 543 | 579 | Shell commands are parsed with `mvdan.cc/sh/syntax` so the prompt can tell you |
| 544 | 580 | 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 | |
| 546 | 582 | not pretend otherwise: a command containing `$(...)`, backticks, or an |
| 547 | 583 | interpreter with `-c` cannot be analysed at all, so it is marked opaque and |
| 548 | 584 | "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 | ||
| 550 | 586 | specific, not to make the decision for you. |
| 551 | 587 | |
| 552 | 588 | **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: | |
| 554 | 590 | |
| 555 | 591 | ```bash |
| 556 | 592 | podman run --rm -it \ |
| 557 | 593 | -v "$PWD":/work:Z -w /work \ |
| 558 | 594 | -e ANTHROPIC_API_KEY \ |
| 559 | 595 | docker.io/library/golang:1.26 \ |
| 560 | - /work/harness -C /work -yolo | |
| 596 | + /work/codey -C /work -yolo | |
| 561 | 597 | ``` |
| 562 | 598 | |
| 563 | 599 | **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` | |
| 565 | 601 | has it too. That is the line between "broke my repo" and "read my `.env` and |
| 566 | 602 | made an HTTP request". If that matters for the tree you are pointing it at: |
| 567 | 603 | |
| 568 | 604 | - Treat every secret reachable from inside the container as exposed. |
| 569 | 605 | - 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 | |
| 571 | 607 | network-less namespace. That is not implemented here. |
| 572 | 608 | |
| 573 | 609 | `-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. | ||
| 577 | 613 | ## Layout |
| 578 | 614 | |
| 579 | 615 | ``` |
| 580 | - cmd/harness flag parsing, wiring, REPL | |
| 616 | + cmd/codey flag parsing, wiring, REPL | |
| 581 | 617 | internal/llm Messages API: types, HTTP, SSE, pricing |
| 582 | 618 | internal/agent turn loop, conversation state, compaction, sessions |
| 583 | 619 | internal/tools the tool registry and the tools |
| @@ -592,7 +628,7 @@ evals task suite and runner | ||
| 592 | 628 | |
| 593 | 629 | Two rules hold the shape: `agent` never imports `ui` or `tui`, so the eval |
| 594 | 630 | 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. | |
| 596 | 632 | A failed edit is a normal event in the transcript. |
| 597 | 633 | |
| 598 | 634 | The TUI obeys the first rule through two one-way hatches. Events reach it via |
| @@ -6,7 +6,7 @@ import ( | ||
| 6 | 6 | "path/filepath" |
| 7 | 7 | "testing" |
| 8 | 8 | |
| 9 | - "harness/internal/llm" | |
| 9 | + "codey/internal/llm" | |
| 10 | 10 | ) |
| 11 | 11 | |
| 12 | 12 | // fakeHome writes a credentials file with a token that is not expired. |
| @@ -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. | |
| 2 | 2 | package main |
| 3 | 3 | |
| 4 | 4 | import ( |
| @@ -14,26 +14,27 @@ import ( | ||
| 14 | 14 | "sync/atomic" |
| 15 | 15 | "time" |
| 16 | 16 | |
| 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" | |
| 27 | 28 | ) |
| 28 | 29 | |
| 29 | 30 | // 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 | |
| 31 | 32 | // can disagree with the source is worse than no stamp. |
| 32 | 33 | const version = "v0.6.0" |
| 33 | 34 | |
| 34 | 35 | func main() { |
| 35 | 36 | if err := run(); err != nil { |
| 36 | - fmt.Fprintln(os.Stderr, "harness:", err) | |
| 37 | + fmt.Fprintln(os.Stderr, "codey:", err) | |
| 37 | 38 | os.Exit(1) |
| 38 | 39 | } |
| 39 | 40 | } |
| @@ -67,8 +68,14 @@ func run() error { | ||
| 67 | 68 | return err |
| 68 | 69 | } |
| 69 | 70 | |
| 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 | + | |
| 70 | 77 | // 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 | |
| 72 | 79 | // still starts, and the wizard can write a good one over it. |
| 73 | 80 | cfg, _, cfgErr := config.Load() |
| 74 | 81 | |
| @@ -152,12 +159,18 @@ func run() error { | ||
| 152 | 159 | banner = append(banner, msg) |
| 153 | 160 | return |
| 154 | 161 | } |
| 155 | - fmt.Fprintln(os.Stderr, "harness: "+msg) | |
| 162 | + fmt.Fprintln(os.Stderr, "codey: "+msg) | |
| 156 | 163 | } |
| 157 | 164 | |
| 158 | 165 | if cfgErr != nil { |
| 159 | 166 | notice("%v — using defaults; /setup will write a fresh one", cfgErr) |
| 160 | 167 | } |
| 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 | + } | |
| 161 | 174 | |
| 162 | 175 | if !client.IsDefaultEndpoint() { |
| 163 | 176 | // Worth saying out loud: the prompt and every file the agent reads go |
| @@ -179,8 +192,8 @@ func run() error { | ||
| 179 | 192 | "error": err.Error(), "file": "~/" + claudecode.Filename, |
| 180 | 193 | }}) |
| 181 | 194 | 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.") | |
| 184 | 197 | } |
| 185 | 198 | } |
| 186 | 199 | } |
| @@ -223,7 +236,7 @@ func run() error { | ||
| 223 | 236 | policy.YOLO = acceptAll |
| 224 | 237 | if acceptAll && !useTUI { |
| 225 | 238 | // 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.") | |
| 227 | 240 | } |
| 228 | 241 | a := agent.New(client, reg, state, log, agent.Options{ |
| 229 | 242 | MaxTurns: *maxTurns, |
| @@ -294,7 +307,7 @@ func run() error { | ||
| 294 | 307 | return once(ctx, a, render, sessionDir, session, root, client, *prompt) |
| 295 | 308 | } |
| 296 | 309 | |
| 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", | |
| 298 | 311 | client.Model(), root, strings.Join(toolNames(reg), ", ")) |
| 299 | 312 | |
| 300 | 313 | for { |
| @@ -337,7 +350,7 @@ func once(ctx context.Context, a *agent.Agent, render *ui.Renderer, sessionDir, | ||
| 337 | 350 | render.Usage(usage, turns, client.Model()) |
| 338 | 351 | |
| 339 | 352 | 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) | |
| 341 | 354 | } |
| 342 | 355 | if errors.Is(runErr, context.Canceled) { |
| 343 | 356 | // 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()) { | ||
| 365 | 378 | cancel := turnCancel.Load() |
| 366 | 379 | |
| 367 | 380 | 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") | |
| 369 | 382 | os.Exit(130) |
| 370 | 383 | } |
| 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)") | |
| 372 | 385 | (*cancel)() |
| 373 | 386 | } |
| 374 | 387 | }() |
| @@ -421,7 +434,7 @@ func resolveClaudeCode(client *llm.Client, home string, disabled bool) (*claudec | ||
| 421 | 434 | // is the only circumstance in which this process writes to Claude Code's |
| 422 | 435 | // file, and not writing then is what would break its login. |
| 423 | 436 | // |
| 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 | |
| 425 | 438 | // ten minutes for a model to answer, which is the wrong budget for a token |
| 426 | 439 | // exchange running under a lock. NewSource supplies a short-timeout client. |
| 427 | 440 | return claudecode.NewSource(creds, nil, home), note, nil |
| @@ -41,7 +41,7 @@ func report(w io.Writer, results []Result) { | ||
| 41 | 41 | tw.Flush() |
| 42 | 42 | |
| 43 | 43 | // 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. | |
| 45 | 45 | var solved int |
| 46 | 46 | for _, name := range order { |
| 47 | 47 | if countPass(byTask[name]) == len(byTask[name]) { |
| @@ -19,11 +19,11 @@ import ( | ||
| 19 | 19 | "sync" |
| 20 | 20 | "time" |
| 21 | 21 | |
| 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" | |
| 27 | 27 | ) |
| 28 | 28 | |
| 29 | 29 | // Task is one directory under evals/tasks. |
| @@ -1,4 +1,4 @@ | ||
| 1 | - module harness | |
| 1 | + module codey | |
| 2 | 2 | |
| 3 | 3 | go 1.26.8 |
| 4 | 4 | |
| @@ -6,8 +6,8 @@ import ( | ||
| 6 | 6 | "strconv" |
| 7 | 7 | "strings" |
| 8 | 8 | |
| 9 | - "harness/internal/llm" | |
| 10 | - "harness/internal/metrics" | |
| 9 | + "codey/internal/llm" | |
| 10 | + "codey/internal/metrics" | |
| 11 | 11 | ) |
| 12 | 12 | |
| 13 | 13 | // 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 | ||
| 164 | 164 | // |
| 165 | 165 | // Past the floor it settles for the smaller tail, for the same reason the floor |
| 166 | 166 | // 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 | |
| 168 | 168 | // move the user has left. |
| 169 | 169 | func (a *Agent) Compact(ctx context.Context) (bool, error) { |
| 170 | 170 | did, err := a.compact(ctx, a.lastInputTokens, keepRecentMessages) |
| @@ -250,7 +250,7 @@ func (a *Agent) summarize(ctx context.Context, history []llm.Message) (string, e | ||
| 250 | 250 | } |
| 251 | 251 | // This is the one request in the agent that is built by hand rather than |
| 252 | 252 | // 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 | |
| 254 | 254 | // credential still requires the Claude Code identity string as the FIRST |
| 255 | 255 | // system block. Skip it here and the API does not 401; it returns a 429 |
| 256 | 256 | // that (*APIError).Retryable treats as transient, so the request burns |
| @@ -10,7 +10,7 @@ import ( | ||
| 10 | 10 | "strings" |
| 11 | 11 | "testing" |
| 12 | 12 | |
| 13 | - "harness/internal/llm" | |
| 13 | + "codey/internal/llm" | |
| 14 | 14 | ) |
| 15 | 15 | |
| 16 | 16 | func TestCacheBreakpoints(t *testing.T) { |
| @@ -446,7 +446,7 @@ func TestCompactOnDemandDeclinesAShortConversation(t *testing.T) { | ||
| 446 | 446 | } |
| 447 | 447 | } |
| 448 | 448 | |
| 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 | |
| 450 | 450 | // well past the ordinary threshold. |
| 451 | 451 | func TestAutoCompactOffNeverFolds(t *testing.T) { |
| 452 | 452 | f := newFakeAPI(t) |
| @@ -8,10 +8,10 @@ import ( | ||
| 8 | 8 | "strings" |
| 9 | 9 | "sync" |
| 10 | 10 | |
| 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" | |
| 15 | 15 | ) |
| 16 | 16 | |
| 17 | 17 | // DefaultMaxTurns caps one Run. An agent that loops without converging burns |
| @@ -87,7 +87,7 @@ type Options struct { | ||
| 87 | 87 | // demand by the skill tool. |
| 88 | 88 | Skills []skills.Skill |
| 89 | 89 | // 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 | |
| 91 | 91 | // requires, and is empty for every other credential. |
| 92 | 92 | SystemPrefix string |
| 93 | 93 | } |
| @@ -319,7 +319,7 @@ func (a *Agent) dispatch(ctx context.Context, turn int, calls []llm.ContentBlock | ||
| 319 | 319 | |
| 320 | 320 | // authorizedExecute runs a tool once the approval policy allows it. A denial |
| 321 | 321 | // 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. | |
| 323 | 323 | func (a *Agent) authorizedExecute(ctx context.Context, call llm.ContentBlock) (string, error) { |
| 324 | 324 | if a.Approve != nil { |
| 325 | 325 | if err := a.Approve.Approve(ctx, call.Name, call.Input); err != nil { |
| @@ -14,10 +14,10 @@ import ( | ||
| 14 | 14 | "strings" |
| 15 | 15 | "testing" |
| 16 | 16 | |
| 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" | |
| 21 | 21 | ) |
| 22 | 22 | |
| 23 | 23 | // fakeAPI serves a scripted sequence of responses and records every request |
| @@ -53,7 +53,7 @@ func newFakeAPI(t *testing.T, responses ...string) *fakeAPI { | ||
| 53 | 53 | body, _ := io.ReadAll(r.Body) |
| 54 | 54 | var req llm.Request |
| 55 | 55 | 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) | |
| 57 | 57 | } |
| 58 | 58 | f.requests = append(f.requests, req) |
| 59 | 59 | if bytes.Contains(body, []byte("Summarize the conversation above")) { |
| @@ -62,7 +62,7 @@ func newFakeAPI(t *testing.T, responses ...string) *fakeAPI { | ||
| 62 | 62 | } |
| 63 | 63 | i := len(f.requests) - 1 - f.compactions() |
| 64 | 64 | 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)) | |
| 66 | 66 | w.WriteHeader(http.StatusInternalServerError) |
| 67 | 67 | return |
| 68 | 68 | } |
| @@ -468,7 +468,7 @@ func TestDeniedToolBecomesAToolError(t *testing.T) { | ||
| 468 | 468 | |
| 469 | 469 | got, err := a.Run(context.Background(), "read it") |
| 470 | 470 | 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) | |
| 472 | 472 | } |
| 473 | 473 | if got != "understood" { |
| 474 | 474 | t.Errorf("answer = %q", got) |
| @@ -538,7 +538,7 @@ func TestSystemPrefixBecomesTheFirstBlock(t *testing.T) { | ||
| 538 | 538 | t.Errorf("first block = %q, want the identity string", blocks[0].Text) |
| 539 | 539 | } |
| 540 | 540 | 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) | |
| 542 | 542 | } |
| 543 | 543 | } |
| 544 | 544 | |
| @@ -8,7 +8,8 @@ import ( | ||
| 8 | 8 | "sort" |
| 9 | 9 | "time" |
| 10 | 10 | |
| 11 | - "harness/internal/llm" | |
| 11 | + "codey/internal/llm" | |
| 12 | + "codey/internal/statedir" | |
| 12 | 13 | ) |
| 13 | 14 | |
| 14 | 15 | // Session is a conversation saved to disk so --resume can pick it up. |
| @@ -24,7 +25,7 @@ type Session struct { | ||
| 24 | 25 | } |
| 25 | 26 | |
| 26 | 27 | // SessionDir is where sessions live, relative to the working root. |
| 27 | - const SessionDir = ".harness/sessions" | |
| 28 | + const SessionDir = statedir.Dir + "/sessions" | |
| 28 | 29 | |
| 29 | 30 | // Save writes the session for id, overwriting any previous copy. |
| 30 | 31 | func (a *Agent) Save(dir, id, root, model string) error { |
| @@ -9,7 +9,7 @@ import ( | ||
| 9 | 9 | "testing" |
| 10 | 10 | "time" |
| 11 | 11 | |
| 12 | - "harness/internal/llm" | |
| 12 | + "codey/internal/llm" | |
| 13 | 13 | ) |
| 14 | 14 | |
| 15 | 15 | func TestSaveAndResumeSession(t *testing.T) { |
| @@ -10,8 +10,8 @@ import ( | ||
| 10 | 10 | "sync" |
| 11 | 11 | "time" |
| 12 | 12 | |
| 13 | - "harness/internal/llm" | |
| 14 | - "harness/internal/tools" | |
| 13 | + "codey/internal/llm" | |
| 14 | + "codey/internal/tools" | |
| 15 | 15 | ) |
| 16 | 16 | |
| 17 | 17 | // ReadRecord lives in tools so the edit tool can query the tracker without |
| @@ -1,5 +1,5 @@ | ||
| 1 | 1 | // 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. | |
| 3 | 3 | // |
| 4 | 4 | // This is Claude Code's private storage, not a documented interface: the |
| 5 | 5 | // schema can change without notice. Everything that knows its shape lives |
| @@ -24,7 +24,7 @@ const tokenURLEnv = "CLAUDECODE_TOKEN_URL" | ||
| 24 | 24 | |
| 25 | 25 | const defaultTokenURL = "https://console.anthropic.com/v1/oauth/token" |
| 26 | 26 | |
| 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 | |
| 28 | 28 | // ten minutes, which is right for a model response and badly wrong here: the |
| 29 | 29 | // refresh runs under a lock every other request is queued behind. |
| 30 | 30 | const refreshTimeout = 30 * time.Second |
| @@ -310,7 +310,7 @@ func NewSource(c *Credentials, h *http.Client, home string) *Source { | ||
| 310 | 310 | // |
| 311 | 311 | // The lock means concurrent requests — streaming and buffered can be in flight |
| 312 | 312 | // 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 | |
| 314 | 314 | // client's ten-minute model-response budget. |
| 315 | 315 | func (s *Source) Token(ctx context.Context) (string, error) { |
| 316 | 316 | s.mu.Lock() |
| @@ -2,7 +2,7 @@ | ||
| 2 | 2 | // |
| 3 | 3 | // It holds defaults, never facts: every field is something a flag or an |
| 4 | 4 | // 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 | |
| 6 | 6 | // run" rather than "broken install". |
| 7 | 7 | package config |
| 8 | 8 | |
| @@ -14,12 +14,19 @@ import ( | ||
| 14 | 14 | "os" |
| 15 | 15 | "path/filepath" |
| 16 | 16 | |
| 17 | - "harness/internal/llm" | |
| 17 | + "codey/internal/llm" | |
| 18 | 18 | ) |
| 19 | 19 | |
| 20 | - // Filename is the config's name inside the harness config directory. | |
| 20 | + // Filename is the config's name inside Codey's config directory. | |
| 21 | 21 | const Filename = "config.json" |
| 22 | 22 | |
| 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 | + | |
| 23 | 30 | // Version is stamped on every write. It exists so a future change of shape |
| 24 | 31 | // has something to branch on rather than guessing from which keys are present. |
| 25 | 32 | const Version = 1 |
| @@ -86,14 +93,20 @@ func (c Config) AutoCompactEnabled() bool { return c.AutoCompact != AutoCompactO | ||
| 86 | 93 | // that suppresses the wizard on startup. |
| 87 | 94 | func (c Config) Done() bool { return c.Setup == SetupDone } |
| 88 | 95 | |
| 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) { | |
| 92 | 105 | dir, err := os.UserConfigDir() |
| 93 | 106 | if err != nil { |
| 94 | 107 | return "", fmt.Errorf("config: no config directory: %w", err) |
| 95 | 108 | } |
| 96 | - return filepath.Join(dir, "harness", Filename), nil | |
| 109 | + return filepath.Join(dir, name, Filename), nil | |
| 97 | 110 | } |
| 98 | 111 | |
| 99 | 112 | // Load reads the config. The boolean reports whether a usable one was found; |
| @@ -104,12 +117,25 @@ func Path() (string, error) { | ||
| 104 | 117 | // found=false. The caller is expected to carry on with defaults and say so: |
| 105 | 118 | // what it must not do is treat the file as absent, because the next Save would |
| 106 | 119 | // 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. | |
| 107 | 125 | func Load() (Config, bool, error) { |
| 108 | 126 | path, err := Path() |
| 109 | 127 | if err != nil { |
| 110 | 128 | return Config{}, false, err |
| 111 | 129 | } |
| 112 | 130 | 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 | + } | |
| 113 | 139 | if errors.Is(err, fs.ErrNotExist) { |
| 114 | 140 | return Config{}, false, nil |
| 115 | 141 | } |
| @@ -125,7 +151,7 @@ func Load() (Config, bool, error) { | ||
| 125 | 151 | } |
| 126 | 152 | |
| 127 | 153 | // 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 | |
| 129 | 155 | // field rather than be forwarded to the API, which would reject the request |
| 130 | 156 | // with an error pointing at nothing the user can see. |
| 131 | 157 | func sanitize(c Config) Config { |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | package config |
| 2 | 2 | |
| 3 | 3 | import ( |
| 4 | - "harness/internal/llm" | |
| 4 | + "codey/internal/llm" | |
| 5 | 5 | "os" |
| 6 | 6 | "path/filepath" |
| 7 | 7 | "strings" |
| @@ -9,12 +9,99 @@ import ( | ||
| 9 | 9 | ) |
| 10 | 10 | |
| 11 | 11 | // 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. | |
| 13 | 13 | func isolate(t *testing.T) string { |
| 14 | 14 | t.Helper() |
| 15 | 15 | dir := t.TempDir() |
| 16 | 16 | 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 | + } | |
| 18 | 105 | } |
| 19 | 106 | |
| 20 | 107 | func TestLoadReportsMissingWithoutError(t *testing.T) { |
| @@ -17,7 +17,7 @@ const ( | ||
| 17 | 17 | cacheWriteMultiplier = 1.25 |
| 18 | 18 | ) |
| 19 | 19 | |
| 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 | |
| 21 | 21 | // that is not listed reports no cost rather than a wrong one. |
| 22 | 22 | var prices = map[string]Price{ |
| 23 | 23 | "claude-opus-5": {InputPerMTok: 5, OutputPerMTok: 25}, |
| @@ -42,7 +42,7 @@ func (u Usage) Cost(model string) (float64, bool) { | ||
| 42 | 42 | float64(u.CacheCreationInputTokens)*p.InputPerMTok*cacheWriteMultiplier) / 1_000_000, true |
| 43 | 43 | } |
| 44 | 44 | |
| 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. | |
| 46 | 46 | // The picker shows what it knows and says nothing about what it does not. |
| 47 | 47 | func PriceOf(model string) (Price, bool) { |
| 48 | 48 | p, ok := prices[basePrice(model)] |
| @@ -37,7 +37,7 @@ type Message struct { | ||
| 37 | 37 | Content []ContentBlock `json:"content"` |
| 38 | 38 | } |
| 39 | 39 | |
| 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. | |
| 41 | 41 | // Only the fields belonging to Type are serialized, so blocks survive the |
| 42 | 42 | // round-trip back to the API — which matters most for thinking blocks, whose |
| 43 | 43 | // signature must be echoed back unchanged. |
| @@ -12,10 +12,12 @@ import ( | ||
| 12 | 12 | "path/filepath" |
| 13 | 13 | "sync" |
| 14 | 14 | "time" |
| 15 | + | |
| 16 | + "codey/internal/statedir" | |
| 15 | 17 | ) |
| 16 | 18 | |
| 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" | |
| 19 | 21 | |
| 20 | 22 | // Event is one line of the log. Extra is flattened into the same JSON object so |
| 21 | 23 | // the file stays greppable with jq without nested lookups. |
| @@ -108,7 +108,7 @@ func TestNilLoggerIsANoOp(t *testing.T) { | ||
| 108 | 108 | } |
| 109 | 109 | |
| 110 | 110 | func TestOpenCreatesParentDirsAndAppends(t *testing.T) { |
| 111 | - path := filepath.Join(t.TempDir(), ".harness", "events.jsonl") | |
| 111 | + path := filepath.Join(t.TempDir(), ".codey", "events.jsonl") | |
| 112 | 112 | for i := 0; i < 2; i++ { |
| 113 | 113 | l, err := Open(path, "s") |
| 114 | 114 | if err != nil { |
| @@ -37,7 +37,7 @@ type Skill struct { | ||
| 37 | 37 | Source string // "personal" or "project" |
| 38 | 38 | } |
| 39 | 39 | |
| 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. | |
| 41 | 41 | // Unknown keys are ignored rather than rejected, so a skill written for |
| 42 | 42 | // another harness still loads here. |
| 43 | 43 | type frontmatter struct { |
| @@ -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 | + } | |
| @@ -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 | + } | |
| @@ -10,6 +10,8 @@ import ( | ||
| 10 | 10 | "strings" |
| 11 | 11 | |
| 12 | 12 | "github.com/bmatcuk/doublestar/v4" |
| 13 | + | |
| 14 | + "codey/internal/statedir" | |
| 13 | 15 | ) |
| 14 | 16 | |
| 15 | 17 | // maxResults caps how many paths or matches a search tool returns. The count of |
| @@ -23,7 +25,8 @@ var skipDirs = map[string]bool{ | ||
| 23 | 25 | ".git": true, ".hg": true, ".svn": true, |
| 24 | 26 | "node_modules": true, "vendor": true, "target": true, |
| 25 | 27 | ".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, | |
| 27 | 30 | } |
| 28 | 31 | |
| 29 | 32 | // GlobArgs is the glob input. |
| @@ -199,9 +199,9 @@ func jailWith(root string, extra []string, path string) (string, error) { | ||
| 199 | 199 | } |
| 200 | 200 | } |
| 201 | 201 | 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) | |
| 203 | 203 | } |
| 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) | |
| 205 | 205 | } |
| 206 | 206 | |
| 207 | 207 | // realpath resolves symlinks, falling back to a lexical clean for a path that |
| @@ -1,7 +1,7 @@ | ||
| 1 | 1 | // Package tools defines the tool interface the agent loop dispatches against. |
| 2 | 2 | // |
| 3 | 3 | // 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 | |
| 5 | 5 | // transcript. |
| 6 | 6 | package tools |
| 7 | 7 | |
| @@ -13,7 +13,7 @@ import ( | ||
| 13 | 13 | "sync" |
| 14 | 14 | "time" |
| 15 | 15 | |
| 16 | - "harness/internal/metrics" | |
| 16 | + "codey/internal/metrics" | |
| 17 | 17 | |
| 18 | 18 | "github.com/invopop/jsonschema" |
| 19 | 19 | ) |
| @@ -8,7 +8,7 @@ import ( | ||
| 8 | 8 | "strings" |
| 9 | 9 | "testing" |
| 10 | 10 | |
| 11 | - "harness/internal/metrics" | |
| 11 | + "codey/internal/metrics" | |
| 12 | 12 | ) |
| 13 | 13 | |
| 14 | 14 | type echoArgs struct { |
| @@ -15,11 +15,11 @@ import ( | ||
| 15 | 15 | func searchRepo(t *testing.T) string { |
| 16 | 16 | t.Helper() |
| 17 | 17 | 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") | |
| 20 | 20 | writeFile(t, dir, "internal/agent/loop.go", "package agent\n\nfunc Run() {}\n") |
| 21 | 21 | 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") | |
| 23 | 23 | writeFile(t, dir, "node_modules/junk/index.js", "function run() {}\n") |
| 24 | 24 | writeFile(t, dir, ".git/config", "[core]\n\trun = true\n") |
| 25 | 25 | 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) { | ||
| 54 | 54 | { |
| 55 | 55 | name: "bare pattern matches at any depth", |
| 56 | 56 | 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"}, | |
| 58 | 58 | unwanted: []string{"go.mod", "README.md"}, |
| 59 | 59 | }, |
| 60 | 60 | { |
| 61 | 61 | name: "pattern with a slash is anchored to the subtree", |
| 62 | 62 | args: GlobArgs{Pattern: "internal/**/*_test.go"}, |
| 63 | 63 | want: []string{"internal/agent/loop_test.go"}, |
| 64 | - unwanted: []string{"cmd/harness/main.go"}, | |
| 64 | + unwanted: []string{"cmd/codey/main.go"}, | |
| 65 | 65 | }, |
| 66 | 66 | { |
| 67 | 67 | name: "path narrows the search", |
| 68 | 68 | args: GlobArgs{Pattern: "*.go", Path: "cmd"}, |
| 69 | - want: []string{"cmd/harness/main.go"}, | |
| 69 | + want: []string{"cmd/codey/main.go"}, | |
| 70 | 70 | unwanted: []string{"internal/agent/loop.go"}, |
| 71 | 71 | }, |
| 72 | 72 | { |
| @@ -171,7 +171,7 @@ func TestGrep(t *testing.T) { | ||
| 171 | 171 | { |
| 172 | 172 | name: "path narrows the search", |
| 173 | 173 | args: GrepArgs{Pattern: "package", Path: "cmd"}, |
| 174 | - want: []string{"cmd/harness/main.go"}, | |
| 174 | + want: []string{"cmd/codey/main.go"}, | |
| 175 | 175 | }, |
| 176 | 176 | } |
| 177 | 177 | // Both engines must produce identical output, or eval numbers would depend |
| @@ -7,7 +7,7 @@ import ( | ||
| 7 | 7 | "sort" |
| 8 | 8 | "strings" |
| 9 | 9 | |
| 10 | - "harness/internal/skills" | |
| 10 | + "codey/internal/skills" | |
| 11 | 11 | ) |
| 12 | 12 | |
| 13 | 13 | // SkillArgs is the skill input. The name is constrained to the discovered |
| @@ -5,7 +5,7 @@ import ( | ||
| 5 | 5 | "strings" |
| 6 | 6 | "testing" |
| 7 | 7 | |
| 8 | - "harness/internal/skills" | |
| 8 | + "codey/internal/skills" | |
| 9 | 9 | ) |
| 10 | 10 | |
| 11 | 11 | func testSkills() []skills.Skill { |
| @@ -181,16 +181,16 @@ func TestIgniteFinishesEveryLetter(t *testing.T) { | ||
| 181 | 181 | |
| 182 | 182 | // At t=1 every letter must be painted the target colour exactly: the same |
| 183 | 183 | // 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) | |
| 185 | 185 | 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")) | |
| 188 | 188 | } |
| 189 | 189 | |
| 190 | - if got := stripANSI(lit); got != "harness" { | |
| 190 | + if got := stripANSI(lit); got != "codey" { | |
| 191 | 191 | t.Errorf("ignite changed the text to %q", got) |
| 192 | 192 | } |
| 193 | - if got := stripANSI(ignite("harness", colFaint, colCoral, 0, false)); got != "harness" { | |
| 193 | + if got := stripANSI(ignite("codey", colFaint, colCoral, 0, false)); got != "codey" { | |
| 194 | 194 | t.Errorf("at t=0 the text is %q, want it present but unlit", got) |
| 195 | 195 | } |
| 196 | 196 | } |
| @@ -5,9 +5,9 @@ import ( | ||
| 5 | 5 | |
| 6 | 6 | tea "github.com/charmbracelet/bubbletea" |
| 7 | 7 | |
| 8 | - "harness/internal/agent" | |
| 9 | - "harness/internal/approval" | |
| 10 | - "harness/internal/llm" | |
| 8 | + "codey/internal/agent" | |
| 9 | + "codey/internal/approval" | |
| 10 | + "codey/internal/llm" | |
| 11 | 11 | ) |
| 12 | 12 | |
| 13 | 13 | // The agent loop runs on its own goroutine and must not know a UI exists. It |
| @@ -9,8 +9,8 @@ import ( | ||
| 9 | 9 | |
| 10 | 10 | tea "github.com/charmbracelet/bubbletea" |
| 11 | 11 | |
| 12 | - "harness/internal/agent" | |
| 13 | - "harness/internal/approval" | |
| 12 | + "codey/internal/agent" | |
| 13 | + "codey/internal/approval" | |
| 14 | 14 | ) |
| 15 | 15 | |
| 16 | 16 | // recorder stands in for tea.Program: it captures what the bridge sends |
| @@ -8,7 +8,7 @@ import ( | ||
| 8 | 8 | "github.com/charmbracelet/lipgloss" |
| 9 | 9 | "github.com/muesli/termenv" |
| 10 | 10 | |
| 11 | - "harness/internal/llm" | |
| 11 | + "codey/internal/llm" | |
| 12 | 12 | ) |
| 13 | 13 | |
| 14 | 14 | // forceColour makes styles emit real escape sequences. Without it the test |
| @@ -31,7 +31,7 @@ var builtins = []command{ | ||
| 31 | 31 | {name: "clear", desc: "empty the transcript"}, |
| 32 | 32 | {name: "mouse", desc: "hand the mouse back to the terminal, or take it again"}, |
| 33 | 33 | {name: "allowed", desc: "what is permanently allowed this session"}, |
| 34 | - {name: "exit", desc: "leave the harness"}, | |
| 34 | + {name: "exit", desc: "leave Codey"}, | |
| 35 | 35 | } |
| 36 | 36 | |
| 37 | 37 | // commands is the catalogue: the built-ins, then every discovered skill. |
| @@ -5,7 +5,7 @@ import ( | ||
| 5 | 5 | "strings" |
| 6 | 6 | "testing" |
| 7 | 7 | |
| 8 | - "harness/internal/agent" | |
| 8 | + "codey/internal/agent" | |
| 9 | 9 | ) |
| 10 | 10 | |
| 11 | 11 | // /compact rewrites the history the in-flight request is built from, so it has |
| @@ -6,7 +6,7 @@ import ( | ||
| 6 | 6 | |
| 7 | 7 | tea "github.com/charmbracelet/bubbletea" |
| 8 | 8 | |
| 9 | - "harness/internal/skills" | |
| 9 | + "codey/internal/skills" | |
| 10 | 10 | ) |
| 11 | 11 | |
| 12 | 12 | // The menu belongs to a slash command being typed, and to nothing else: a |
| @@ -7,7 +7,7 @@ import ( | ||
| 7 | 7 | tea "github.com/charmbracelet/bubbletea" |
| 8 | 8 | "github.com/charmbracelet/lipgloss" |
| 9 | 9 | |
| 10 | - "harness/internal/llm" | |
| 10 | + "codey/internal/llm" | |
| 11 | 11 | ) |
| 12 | 12 | |
| 13 | 13 | // defaultEffort is where the marker sits when nothing has been chosen. The |
| @@ -6,7 +6,7 @@ import ( | ||
| 6 | 6 | |
| 7 | 7 | tea "github.com/charmbracelet/bubbletea" |
| 8 | 8 | |
| 9 | - "harness/internal/llm" | |
| 9 | + "codey/internal/llm" | |
| 10 | 10 | ) |
| 11 | 11 | |
| 12 | 12 | // levelAt is where a level sits on the track, so tests can talk about |
| @@ -7,7 +7,7 @@ import ( | ||
| 7 | 7 | "strings" |
| 8 | 8 | "unicode" |
| 9 | 9 | |
| 10 | - "harness/internal/llm" | |
| 10 | + "codey/internal/llm" | |
| 11 | 11 | ) |
| 12 | 12 | |
| 13 | 13 | // human renders a token count the way a status bar wants to read it. |
| @@ -10,8 +10,8 @@ import ( | ||
| 10 | 10 | tea "github.com/charmbracelet/bubbletea" |
| 11 | 11 | "github.com/charmbracelet/lipgloss" |
| 12 | 12 | |
| 13 | - "harness/internal/llm" | |
| 14 | - "harness/internal/tools" | |
| 13 | + "codey/internal/llm" | |
| 14 | + "codey/internal/tools" | |
| 15 | 15 | ) |
| 16 | 16 | |
| 17 | 17 | // localMode is which interpreter the prompt box is pointed at. Off is the |
| @@ -9,10 +9,10 @@ import ( | ||
| 9 | 9 | "github.com/charmbracelet/bubbles/spinner" |
| 10 | 10 | tea "github.com/charmbracelet/bubbletea" |
| 11 | 11 | |
| 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" | |
| 16 | 16 | ) |
| 17 | 17 | |
| 18 | 18 | // newLocalModel builds a model with a real agent behind it, so a local |
| @@ -8,8 +8,8 @@ import ( | ||
| 8 | 8 | tea "github.com/charmbracelet/bubbletea" |
| 9 | 9 | "github.com/charmbracelet/lipgloss" |
| 10 | 10 | |
| 11 | - "harness/internal/approval" | |
| 12 | - "harness/internal/llm" | |
| 11 | + "codey/internal/approval" | |
| 12 | + "codey/internal/llm" | |
| 13 | 13 | ) |
| 14 | 14 | |
| 15 | 15 | func TestPrettyModel(t *testing.T) { |
| @@ -60,7 +60,7 @@ func TestSplashDropsMascotWhenNarrow(t *testing.T) { | ||
| 60 | 60 | if strings.Contains(narrow, mascot[2]) { |
| 61 | 61 | t.Error("mascot survived into a terminal too narrow for it") |
| 62 | 62 | } |
| 63 | - if !strings.Contains(narrow, "harness") { | |
| 63 | + if !strings.Contains(narrow, "codey") { | |
| 64 | 64 | t.Error("narrow welcome block lost its name") |
| 65 | 65 | } |
| 66 | 66 | } |
| @@ -262,10 +262,10 @@ func TestNewModelOpensWithTheWelcomeBlock(t *testing.T) { | ||
| 262 | 262 | Banner: []string{"resumed session abc"}, |
| 263 | 263 | }) |
| 264 | 264 | out := m.tr.render() |
| 265 | - if !strings.Contains(out, "harness") { | |
| 265 | + if !strings.Contains(out, "codey") { | |
| 266 | 266 | t.Error("welcome block missing") |
| 267 | 267 | } |
| 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 { | |
| 269 | 269 | t.Error("the banner should follow the welcome block, not precede it") |
| 270 | 270 | } |
| 271 | 271 | } |
| @@ -288,7 +288,7 @@ func TestDiffStatCountsEditsNotHeaders(t *testing.T) { | ||
| 288 | 288 | } |
| 289 | 289 | } |
| 290 | 290 | |
| 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 | |
| 292 | 292 | // the status bar rather than only in the metrics file. |
| 293 | 293 | func TestStatusReportsCacheReadsSeparately(t *testing.T) { |
| 294 | 294 | m := newTestModel(t) |
| @@ -229,7 +229,7 @@ func chromaTheme(t theme, p termenv.Profile) string { | ||
| 229 | 229 | if p == termenv.Ascii { |
| 230 | 230 | return "" |
| 231 | 231 | } |
| 232 | - name := "harness-" + t.name | |
| 232 | + name := "codey-" + t.name | |
| 233 | 233 | if chromaThemes[name] { |
| 234 | 234 | return name |
| 235 | 235 | } |
| @@ -14,11 +14,11 @@ import ( | ||
| 14 | 14 | tea "github.com/charmbracelet/bubbletea" |
| 15 | 15 | "github.com/charmbracelet/lipgloss" |
| 16 | 16 | |
| 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" | |
| 22 | 22 | ) |
| 23 | 23 | |
| 24 | 24 | // Options wires the TUI to an already-built agent. The TUI owns no policy of |
| @@ -163,7 +163,7 @@ type model struct { | ||
| 163 | 163 | // say so while the agent unwinds. |
| 164 | 164 | interrupting bool |
| 165 | 165 | |
| 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 | |
| 167 | 167 | // mouse reporting at all: it costs the terminal's own text selection, so |
| 168 | 168 | // /mouse gives it back. sel is the selection that replaces it, copied is |
| 169 | 169 | // what the chrome says about the last copy, and the three click fields |
| @@ -174,7 +174,7 @@ type model struct { | ||
| 174 | 174 | clicks int |
| 175 | 175 | clickAt cell |
| 176 | 176 | 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 | |
| 178 | 178 | // watch what was copied without a clipboard, or a terminal, being there. |
| 179 | 179 | copyFn func(string) error |
| 180 | 180 | |
| @@ -800,7 +800,7 @@ func (m *model) setEffort(level string) tea.Cmd { | ||
| 800 | 800 | |
| 801 | 801 | // isSkill reports whether a slash command names a discovered skill. Matching |
| 802 | 802 | // 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. | |
| 804 | 804 | func (m *model) isSkill(line string) bool { |
| 805 | 805 | name := strings.TrimPrefix(line, "/") |
| 806 | 806 | if i := strings.IndexAny(name, " \t"); i >= 0 { |
| @@ -1036,7 +1036,7 @@ func (m *model) status() string { | ||
| 1036 | 1036 | } |
| 1037 | 1037 | |
| 1038 | 1038 | // 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. | |
| 1040 | 1040 | u := m.usage |
| 1041 | 1041 | if u.InputTokens+u.OutputTokens+u.CacheReadInputTokens > 0 { |
| 1042 | 1042 | io := fmt.Sprintf("↑ %s", human(u.InputTokens)) |
| @@ -10,7 +10,7 @@ import ( | ||
| 10 | 10 | tea "github.com/charmbracelet/bubbletea" |
| 11 | 11 | "github.com/charmbracelet/lipgloss" |
| 12 | 12 | |
| 13 | - "harness/internal/llm" | |
| 13 | + "codey/internal/llm" | |
| 14 | 14 | ) |
| 15 | 15 | |
| 16 | 16 | // modelFetchTimeout bounds the wait. Someone is sitting in front of a picker |
| @@ -10,7 +10,7 @@ import ( | ||
| 10 | 10 | |
| 11 | 11 | tea "github.com/charmbracelet/bubbletea" |
| 12 | 12 | |
| 13 | - "harness/internal/llm" | |
| 13 | + "codey/internal/llm" | |
| 14 | 14 | ) |
| 15 | 15 | |
| 16 | 16 | func someModels() []llm.Model { |
| @@ -22,7 +22,7 @@ func someModels() []llm.Model { | ||
| 22 | 22 | } |
| 23 | 23 | |
| 24 | 24 | // /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. | |
| 26 | 26 | func TestModelPickerAsksTheEndpoint(t *testing.T) { |
| 27 | 27 | m := newTestModel(t) |
| 28 | 28 | m.submit("/model") |
| @@ -7,10 +7,10 @@ import ( | ||
| 7 | 7 | |
| 8 | 8 | tea "github.com/charmbracelet/bubbletea" |
| 9 | 9 | |
| 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" | |
| 14 | 14 | ) |
| 15 | 15 | |
| 16 | 16 | // newTestModel builds a model far enough to drive Update/View without a |
| @@ -457,7 +457,7 @@ func TestFirstRunOpensTheWizard(t *testing.T) { | ||
| 457 | 457 | if m.wiz == nil { |
| 458 | 458 | t.Fatal("a first run did not open the wizard") |
| 459 | 459 | } |
| 460 | - if !strings.Contains(m.View(), "harness") { | |
| 460 | + if !strings.Contains(m.View(), "codey") { | |
| 461 | 461 | t.Error("the wizard is open but its screen is not what View returns") |
| 462 | 462 | } |
| 463 | 463 | } |
| @@ -7,7 +7,7 @@ import ( | ||
| 7 | 7 | tea "github.com/charmbracelet/bubbletea" |
| 8 | 8 | "github.com/charmbracelet/lipgloss" |
| 9 | 9 | |
| 10 | - "harness/internal/llm" | |
| 10 | + "codey/internal/llm" | |
| 11 | 11 | ) |
| 12 | 12 | |
| 13 | 13 | // 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) | ||
| 345 | 345 | // toggleMouse hands the mouse to the terminal or takes it back. With |
| 346 | 346 | // reporting on, the terminal stops sending drags to its own selection and |
| 347 | 347 | // 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 | |
| 349 | 349 | // is not what you want. |
| 350 | 350 | func (m *model) toggleMouse() tea.Cmd { |
| 351 | 351 | m.mouseOn = !m.mouseOn |
| @@ -15,8 +15,8 @@ import ( | ||
| 15 | 15 | |
| 16 | 16 | "github.com/muesli/termenv" |
| 17 | 17 | |
| 18 | - "harness/internal/approval" | |
| 19 | - "harness/internal/llm" | |
| 18 | + "codey/internal/approval" | |
| 19 | + "codey/internal/llm" | |
| 20 | 20 | ) |
| 21 | 21 | |
| 22 | 22 | func press(x, y int) tea.MouseMsg { |
| @@ -381,7 +381,7 @@ func TestClickingTheModeLineCyclesApprovals(t *testing.T) { | ||
| 381 | 381 | } |
| 382 | 382 | |
| 383 | 383 | // /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. | |
| 385 | 385 | func TestMouseCanBeHandedBackToTheTerminal(t *testing.T) { |
| 386 | 386 | m, clipboard := mouseModel(t, "the quick brown fox") |
| 387 | 387 | |
| @@ -7,7 +7,7 @@ import ( | ||
| 7 | 7 | "github.com/charmbracelet/x/ansi" |
| 8 | 8 | ) |
| 9 | 9 | |
| 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 | |
| 11 | 11 | // does for us. Asking for mouse reporting takes the terminal's text selection |
| 12 | 12 | // away — it stops sending drags to the clipboard and starts sending them here |
| 13 | 13 | // — so the alternative to drawing and copying a selection ourselves is not |
| @@ -7,7 +7,7 @@ import ( | ||
| 7 | 7 | tea "github.com/charmbracelet/bubbletea" |
| 8 | 8 | "github.com/charmbracelet/lipgloss" |
| 9 | 9 | |
| 10 | - "harness/internal/config" | |
| 10 | + "codey/internal/config" | |
| 11 | 11 | ) |
| 12 | 12 | |
| 13 | 13 | // The settings panel: /settings. One row per thing the config file holds, |
| @@ -284,7 +284,7 @@ func (m *model) backFromPicker() phase { | ||
| 284 | 284 | // Approvals Ask me first recommended |
| 285 | 285 | // Theme ember |
| 286 | 286 | // 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 | |
| 288 | 288 | func (m *model) settingsView() string { |
| 289 | 289 | width := maxInt(1, m.width) |
| 290 | 290 | |
| @@ -9,9 +9,9 @@ import ( | ||
| 9 | 9 | tea "github.com/charmbracelet/bubbletea" |
| 10 | 10 | "github.com/charmbracelet/lipgloss" |
| 11 | 11 | |
| 12 | - "harness/internal/approval" | |
| 13 | - "harness/internal/config" | |
| 14 | - "harness/internal/llm" | |
| 12 | + "codey/internal/approval" | |
| 13 | + "codey/internal/config" | |
| 14 | + "codey/internal/llm" | |
| 15 | 15 | ) |
| 16 | 16 | |
| 17 | 17 | // newSettingsModel builds a chat model with a config already on disk and a |
| @@ -6,13 +6,19 @@ import ( | ||
| 6 | 6 | "github.com/charmbracelet/lipgloss" |
| 7 | 7 | ) |
| 8 | 8 | |
| 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. | |
| 11 | 17 | var mascot = []string{ |
| 12 | - " ▄▄▄▄▄▄ ", | |
| 13 | - "▐▀█▀█▀█▌", | |
| 14 | - "▐██████▌", | |
| 15 | - " ▀▀ ▀▀ ", | |
| 18 | + "▄██████▄", | |
| 19 | + "█ ██ █", | |
| 20 | + "███▀▀███", | |
| 21 | + " ▀█▀▀█▀ ", | |
| 16 | 22 | } |
| 17 | 23 | |
| 18 | 24 | const mascotGap = " " |
| @@ -24,7 +30,7 @@ const mascotGap = " " | ||
| 24 | 30 | // On a narrow terminal the mascot is dropped rather than sliced: half a mascot |
| 25 | 31 | // reads as a rendering bug, where the three text lines alone read as a choice. |
| 26 | 32 | func splash(o Options, width int) string { |
| 27 | - name := styHeading.Render("harness") | |
| 33 | + name := styHeading.Render("codey") | |
| 28 | 34 | if o.Version != "" { |
| 29 | 35 | name += " " + styDim.Render(o.Version) |
| 30 | 36 | } |
| @@ -78,7 +78,7 @@ var ( | ||
| 78 | 78 | styModal lipgloss.Style |
| 79 | 79 | |
| 80 | 80 | // 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 | |
| 82 | 82 | // read as a selection at a glance in all four themes. |
| 83 | 83 | stySelect lipgloss.Style |
| 84 | 84 | ) |
| @@ -5,7 +5,7 @@ | ||
| 5 | 5 | // fixture, not part of the build. |
| 6 | 6 | // |
| 7 | 7 | // 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 | |
| 9 | 9 | // |
| 10 | 10 | // The script is fixed: the first turn streams a sentence and asks to run a |
| 11 | 11 | // shell command, which makes the TUI raise an approval prompt; once a |
| @@ -17,7 +17,7 @@ | ||
| 17 | 17 | // size of the request, so the gauge climbs as you talk; pair it with a small |
| 18 | 18 | // -context-window to reach a threshold in a handful of turns: |
| 19 | 19 | // |
| 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 | |
| 21 | 21 | // |
| 22 | 22 | // Flags: -addr to move it, -fail to make the command exit non-zero, -slow to |
| 23 | 23 | // stretch the stream out for watching the spinner and ctrl-c. |
| @@ -50,7 +50,7 @@ func main() { | ||
| 50 | 50 | func handle(w http.ResponseWriter, r *http.Request) { |
| 51 | 51 | body, _ := io.ReadAll(r.Body) |
| 52 | 52 | |
| 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 | |
| 54 | 54 | // be answered with the SSE script below. |
| 55 | 55 | if strings.Contains(string(body), "Summarize the conversation above") { |
| 56 | 56 | w.Header().Set("Content-Type", "application/json") |
| @@ -66,7 +66,7 @@ func handle(w http.ResponseWriter, r *http.Request) { | ||
| 66 | 66 | return |
| 67 | 67 | } |
| 68 | 68 | |
| 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 | |
| 70 | 70 | // command and fed the outcome back, so the next turn is the last one. |
| 71 | 71 | final := strings.Contains(string(body), "tool_result") |
| 72 | 72 | |
| @@ -3,7 +3,7 @@ package tui | ||
| 3 | 3 | import ( |
| 4 | 4 | "github.com/charmbracelet/lipgloss" |
| 5 | 5 | |
| 6 | - "harness/internal/config" | |
| 6 | + "codey/internal/config" | |
| 7 | 7 | ) |
| 8 | 8 | |
| 9 | 9 | // A theme is a palette, not a set of styles: the styles in styles.go are |
| @@ -103,7 +103,7 @@ var themes = []theme{{ | ||
| 103 | 103 | selBg: lipgloss.AdaptiveColor{Light: "#d0d0d0", Dark: "#484848"}, |
| 104 | 104 | }} |
| 105 | 105 | |
| 106 | - // defaultTheme is what an unconfigured harness looks like, and the fallback | |
| 106 | + // defaultTheme is what an unconfigured Codey looks like, and the fallback | |
| 107 | 107 | // for a theme name this build does not have. |
| 108 | 108 | const defaultTheme = "ember" |
| 109 | 109 | |
| @@ -8,8 +8,8 @@ import ( | ||
| 8 | 8 | "github.com/charmbracelet/bubbles/textinput" |
| 9 | 9 | tea "github.com/charmbracelet/bubbletea" |
| 10 | 10 | |
| 11 | - "harness/internal/config" | |
| 12 | - "harness/internal/llm" | |
| 11 | + "codey/internal/config" | |
| 12 | + "codey/internal/llm" | |
| 13 | 13 | ) |
| 14 | 14 | |
| 15 | 15 | // The setup wizard: a full-screen, one-question-at-a-time interface that owns |
| @@ -7,8 +7,8 @@ import ( | ||
| 7 | 7 | tea "github.com/charmbracelet/bubbletea" |
| 8 | 8 | "github.com/charmbracelet/lipgloss" |
| 9 | 9 | |
| 10 | - "harness/internal/config" | |
| 11 | - "harness/internal/llm" | |
| 10 | + "codey/internal/config" | |
| 11 | + "codey/internal/llm" | |
| 12 | 12 | ) |
| 13 | 13 | |
| 14 | 14 | // newTestWizard builds a wizard on a comfortable screen with the catalogue |
| @@ -10,7 +10,7 @@ import ( | ||
| 10 | 10 | |
| 11 | 11 | "github.com/charmbracelet/lipgloss" |
| 12 | 12 | |
| 13 | - "harness/internal/config" | |
| 13 | + "codey/internal/config" | |
| 14 | 14 | ) |
| 15 | 15 | |
| 16 | 16 | // ---- view --------------------------------------------------------------- |
| @@ -76,7 +76,7 @@ func (w *wizard) card() string { | ||
| 76 | 76 | // header is the mark and the word "setup", present on every step so the screen |
| 77 | 77 | // always says what it is. |
| 78 | 78 | func (w *wizard) header(width int) string { |
| 79 | - left := styHeading.Render("harness") + styDim.Render(" setup") | |
| 79 | + left := styHeading.Render("codey") + styDim.Render(" setup") | |
| 80 | 80 | right := styFaint.Render(fmt.Sprintf("step %d of %d", int(w.step), railSteps)) |
| 81 | 81 | return justify(left, right, width) |
| 82 | 82 | } |
| @@ -287,9 +287,9 @@ func (w *wizard) introCard() string { | ||
| 287 | 287 | } |
| 288 | 288 | b.WriteString("\n") |
| 289 | 289 | |
| 290 | - // "harness" ignites left to right once the mark is up. | |
| 290 | + // "codey" ignites left to right once the mark is up. | |
| 291 | 291 | 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") | |
| 293 | 293 | |
| 294 | 294 | // The tagline types itself underneath. The short form is not a truncation |
| 295 | 295 | // of the long one: a cut sentence reads as a bug, where a shorter sentence |
| @@ -337,7 +337,7 @@ func (w *wizard) doneCard() string { | ||
| 337 | 337 | } |
| 338 | 338 | |
| 339 | 339 | 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") | |
| 341 | 341 | |
| 342 | 342 | for i, r := range rows { |
| 343 | 343 | // Staggered: each row starts three frames after the one above, so |
| @@ -7,7 +7,7 @@ import ( | ||
| 7 | 7 | "io" |
| 8 | 8 | "strings" |
| 9 | 9 | |
| 10 | - "harness/internal/approval" | |
| 10 | + "codey/internal/approval" | |
| 11 | 11 | ) |
| 12 | 12 | |
| 13 | 13 | // Approver reads approval decisions from a terminal. |
| @@ -11,8 +11,8 @@ import ( | ||
| 11 | 11 | "sort" |
| 12 | 12 | "strings" |
| 13 | 13 | |
| 14 | - "harness/internal/agent" | |
| 15 | - "harness/internal/llm" | |
| 14 | + "codey/internal/agent" | |
| 15 | + "codey/internal/llm" | |
| 16 | 16 | ) |
| 17 | 17 | |
| 18 | 18 | // ANSI codes, blanked when the output is not a terminal or NO_COLOR is set. |
| @@ -6,8 +6,8 @@ import ( | ||
| 6 | 6 | "strings" |
| 7 | 7 | "testing" |
| 8 | 8 | |
| 9 | - "harness/internal/agent" | |
| 10 | - "harness/internal/llm" | |
| 9 | + "codey/internal/agent" | |
| 10 | + "codey/internal/llm" | |
| 11 | 11 | ) |
| 12 | 12 | |
| 13 | 13 | func TestSummarizeInputIsSortedAndBounded(t *testing.T) { |