Add the pi harness: lightweight local-model agents with full gate parity

Model backend rows gain a harness column (claude | pi). A pi-harness row runs
the agent through the pi coding agent instead of the claude binary — pi speaks
the OpenAI Completions API natively, so a bare vLLM/llama.cpp/Ollama endpoint
needs no LiteLLM/claude-code-router translation proxy, and the loop is far
lighter for slow local token throughput. The Claude subscription and existing
claude-harness backends are untouched.

Parity comes from generated per-agent artifacts under ~/.handler-pi (outside
the repo tree, so the clean-tree gate never trips): models.json + settings.json
render the row as a pi provider pinned as the default model; a bundled bridge
extension (pi_bridge.ts) adapts pi's events to the exact stdin/stdout contract
of `python -m handler.hooks` — the Stop/completion gate re-prompts pi with
blockers via a follow-up message, git push runs the test/build/approval gates
and denies on failure, questions defer through an ask_operator tool into the
normal answer/resume flow, and memory recall is injected at session start. The
memory tools are registered natively (pi has no MCP), shelling to a new
`python -m handler.mcpserver --call <tool>` seam that reuses the MCP server's
implementations. Skills reuse the same ~/.claude/skills sync (pi implements the
same SKILL.md standard) plus the repo's committed .claude/skills.

Sessions are single JSONL files pre-assigned via --session, so cross-worker
resume archives/materializes exactly like claude's; the prompt travels on stdin
(pi has no -- separator). The supervisor normalizes pi's event stream on the
fly: assistant message_end feeds last_output, the final agent_end becomes the
run result. The whole chain was validated live against pi 0.84.1 with a stub
OpenAI endpoint: memory injection, push-gate denial (including the protected-
branch approval gate), stop-gate block loop, and ask_operator pause all ran
end to end through the real hooks and DB.

Also: harness selector in the dashboard Models form, pi baked into the control
image (NodeSource 22 for pi's node >= 22.19 floor), PI_BIN override, docs in
docs/local-models.md, fake_pi fixture + 12 tests (361 total green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KdGv3u3DfTsP1S188KDhVH
This commit is contained in:
Claude
2026-08-12 18:21:35 +00:00
parent 356fa276a2
commit a3a5c272a2
24 changed files with 1417 additions and 84 deletions
+13 -9
View File
@@ -132,7 +132,7 @@ Configuration is entirely environment-driven (see [`.env.example`](.env.example)
| `WEBHOOK_URL` | Generic target for the `Notification` hook (ntfy, Slack, …) | unset → no-op |
| `HANDLER_SECRET_KEY` | Fernet key encrypting git-server tokens + SSH keys at rest (set the same value on API and control) | unset → secret store disabled |
| `PROJECTS_ROOT` | Base dir for per-project roots / worktrees / auto-clones | `./projects` |
| `CLAUDE_BIN` / `MISE_BIN` / `TMUX_BIN` / `FORGE_BIN` / `GIT_BIN` | Binary overrides | `claude` / `mise` / `tmux` / `forge` / `git` |
| `CLAUDE_BIN` / `PI_BIN` / `MISE_BIN` / `TMUX_BIN` / `FORGE_BIN` / `GIT_BIN` | Binary overrides | `claude` / `pi` / `mise` / `tmux` / `forge` / `git` |
| `FORGE_VERSION` | Pinned forge version verified at spawn (Phase 2) | unset → skip check |
| `PROTECTED_BRANCHES` | Branches a direct push needs an approval to reach (Phase 2) | `main,master` |
@@ -257,14 +257,18 @@ What the dashboard can now do (all state-changing actions require `ADMIN_TOKEN`)
- **Claude** — the management page for the Claude Code install agents run on. The account
login lives here (see below), plus web-managed **model backends**, **skills**,
**MCP connectors**, **plugins**, and **permission overrides**. Model backends are
Anthropic-API-compatible endpoints (a local Qwen/Llama behind LiteLLM or
claude-code-router, an LLM gateway) offered in the spawn form's **Model** dropdown next
to the Claude subscription: the same `claude` binary is pointed at the endpoint via
`ANTHROPIC_BASE_URL`/`ANTHROPIC_MODEL` env at launch, so hooks, skills, connectors, and
gates apply unchanged, and the agent stays pinned to its backend across resumes. API
keys are stored encrypted (`HANDLER_SECRET_KEY`) and never returned. See
[`docs/local-models.md`](docs/local-models.md) for working local stacks (and why bare
OpenAI-compatible servers break tool calling). These are plain DB rows the control container
alternative endpoints offered in the spawn form's **Model** dropdown next to the
Claude subscription, and each picks a **harness**: `claude` (the same `claude` binary
pointed at an Anthropic-API-compatible endpoint — a local model behind LiteLLM or
claude-code-router, an LLM gateway — via `ANTHROPIC_BASE_URL`/`ANTHROPIC_MODEL` env at
launch) or `pi` (the lightweight [pi coding agent](https://github.com/badlogic/pi-mono),
which speaks bare OpenAI-compatible endpoints — vLLM, llama.cpp, Ollama — natively, no
translation proxy, with handler's hooks/gates/memory/skills bridged in via a bundled pi
extension). Either way hooks, skills, and gates apply, and the agent stays pinned to
its backend across resumes. API keys are stored encrypted (`HANDLER_SECRET_KEY`) and
never returned. See [`docs/local-models.md`](docs/local-models.md) for working local
stacks (and why bare OpenAI-compatible servers break tool calling *on the claude
harness*). These are plain DB rows the control container
applies at every launch: skills sync to each worker's user-level `~/.claude/skills`
(marker-file managed, so hand-installed skills survive), enabled connectors become the
run's `--mcp-config` file (nothing lands in the repo tree), and plugins/permissions fold