The Harness, Not the Horse

A broken npm symlink, a 400 from Claude extra usage, and a home directory full of every project on the machine. Then one repo, four tools, three commits.

A broken npm symlink, a 400 from Claude extra usage, and a home directory full of every project on the machine. Then one repo, four tools, three commits.

What Happened


Pi went from a broken global install on this Debian box to a working coding-agent harness aimed at one repo. Three commits landed on ~/projects/test-agent, branch main.

What the repo holds now

CommitPayload
64f199cInteger coercion for memory_size and llm_max_tokens; workspace checks with Path.resolve() and is_relative_to; twelve pytest tests; AGENTS.md; .gitignore
8e98a31The rest of the existing app — main.py, core.py, memory.py, config, requirements, README
6281d4fREADME section for pytest -q and AGENT_* overrides

Harness: Pi v0.84.2. Auth: Anthropic Console API key, not Claude.ai extra usage. Model sent on the wire: claude-opus-4-8.


Why it mattered

Why a harness at all

A model is only the weights. The harness is tools, cwd, context files, sessions, auth, compaction, the TUI. Pi keeps that layer small on purpose: four tools — read, write, edit, bash — and a short system prompt. Everything else is opt-in. Smaller context, visible tool calls, provider-agnostic, files on disk instead of a sealed personality.

Why the installer fought back

~/.npm-global/bin/pi already pointed at the old package name @mariozechner/pi-coding-agent. The current package is @earendil-works/pi-coding-agent. npm refused to overwrite the link (EEXIST). After the stale symlink was gone, PATH still had to include ~/.npm-global/bin or the binary sat on disk while the shell said pi: not found.

Why the first model call 400’d

Subscription OAuth in a third-party harness does not spend the Claude plan. Anthropic bills that traffic as extra usage. The Console API key is the path that actually answers.

Why $HOME was the wrong cwd

Launching Pi in /home/lvydvy exposed every tree on the machine — Zig caches, WordPress, tarballs, unrelated apps. No single stack, no git root, and the same permissions as the login user. Tokens burned on listing. Risk high. A harness is only as narrow as the working directory.

Why test-agent

A small Python REPL agent already on disk: YAML config, memory deque, file tools, Anthropic call. Enough surface to change, test, and commit without opening llama.cpp or a game engine.

Why those particular edits

The first read of the repo found real faults: env overrides arrive as strings (maxlen=”100″ is a trap); str.startswith treats work-evil as inside work; pytest was named and empty; execute_code is raw exec(). The patch fixed the first three and left exec() alone on purpose.


How the work actually ran

Install. Remove the May 3 mariozechner symlink, uninstall the old global package, install @earendil-works/pi-coding-agent, export PATH=”$HOME/.npm-global/bin:$PATH”. Confirm which pi lands on the earendil-works cli.js.

Auth and model. /login on the subscription produced the extra-usage warning and a 400. API key written to ~/.pi/agent/auth.json. /model claude-opus-4-8. The model’s own sentence “claude-opus-4-6-20251101” is not the router. Footer and /model are. From the 4.6 generation on, dateless IDs are pinned snapshots: claude-opus-4-8 is 4.8, not “whatever Opus is newest.”

Scope. A placeholder path the-actual-project was copied into the shell and failed. Real directories were listed from bash, not from a $HOME agent session. Chosen root: /home/lvydvy/projects/test-agent. Rule that held: pwd must print that path before pi starts.

Session one. Prompt asked for a one-screen summary and an AGENTS.md if missing. Layout, run/test commands, and fragilities went into that file. Later startups show [Context] AGENTS.md.

Session two. One constrained prompt: coerce ints, replace prefix checks, add tests, do not grow execute_code. Edits in agent/config.py and agent/tools.py; new tests/test_config.py and tests/test_tools.py; pytest installed into .venv; 12 passed.

Git, with a human still in the loop. git diff opened less. Keystrokes meant for the shell went into the pager — hence h, broken filenames, and the > continuation prompt. GIT_PAGER=cat keeps diffs in the terminal. First commit was only the new slice. Second commit added the pre-existing application files. Paste-accident files were deleted by find/name patterns, not by quoting em-dashes in rm. git add . stayed off until the junk was gone.

Session three. README-only prompt. Diff reviewed. 6281d4f. Working tree clean.

The loop that remains: one directory, one task, read the diff, commit. Pi edits. Ownership stays with the person at the keyboard.


Why Pi is worth keeping

Not a chat window with a terminal skin. Infrastructure for running a model against a tree:

  • Four tools, every call visible.
  • Project rules as files (AGENTS.md, skills, session trees).
  • Same harness across providers.
  • Extensions and packages when the core is too small.
  • Interactive, pi -p, JSON, RPC, SDK.

What this night proved on a smaller scale: a strong model can be aimed at one repo, changes can be test-backed, and git can keep the record. That is the difference between a teammate and a home-directory lawnmower.