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
Agent runs are now always worker-owned 'claude -p' subprocesses; tmux
survives only for the interactive /login flow.
- deleted: worker.capture_agent_output/_pane_tail + the capture loop
arm (the empty-/log bug's home), spawn's tmux launch/_claude_command,
the tmux resume/kill branches (the silent-send-keys bug's home),
tmux.session_name/list_sessions, the CLI attach subcommand, the
'runner' setting
- spawn: task is now a hard requirement (headless has no idle REPL) -
enforced in spawn (SpawnError) and the API (400); onboarding seeding
dropped (-p skips the trust dialog)
- resume: single headless path; pre-headless agent rows (no session_id)
degrade to the context-re-injection fresh run
- settings_gen: permissions allowlist is always emitted
- credsync: change-triggered uploads key on .claude/.credentials.json
only (claude touches ~/.claude.json every run - keying on it would
ping-pong uploads between workers); logins still publish explicitly
- cli list: liveness from agent_runs in the DB, not tmux
- tests: spawn/kill/resume re-pointed at the fake_launch seam
(conftest); integration test now drives API -> worker -> real fake
claude subprocess -> events endpoint; README documents the headless
model + multi-worker deployment invariants
Suite 295 green; frontend unchanged since phase 3.
The mise-init agent wedged on launch and the UI reported it green. Three
distinct problems, fixed together:
1. Onboarding wedge (the proximate bug). A freshly-installed claude opens
interactive setup — theme picker, then a folder-trust prompt — before
the REPL. A detached tmux agent has no one to answer it, so it sat on
the theme picker forever while agents.status said 'working'. New
control.claude_config.ensure_onboarded() marks onboarding complete and
trusts the working dir in ~/.claude.json (merge-only, so the login
flow's oauthAccount survives); spawn() calls it before launching.
2. Config-name gate. control/mise.py only recognized `.mise.toml`, so a
repo shipping `mise.toml` (no dot) — or config under `.config/mise/` —
failed the [tasks.test] gate even when healthy. It now accepts the
filenames mise itself reads and scans them all for the test task.
3. "Done" != done (the design gap). A spawned agent's real state lives in
its tmux pane, but the socket is control-container-only, so the API
couldn't see it. The worker now snapshots each working agent's pane
tail (last ~40 lines) into two new agents columns (last_output,
output_at, migration 0007) on its existing poll loop; the API
serializes them and AgentsSection renders a live-output <pre> under
each running agent. A wedged agent now shows the theme picker instead
of a misleading green badge.
Tests: home-dir writes are isolated to tmp in conftest; added coverage for
claude_config seeding/merge, the mise filename set, the worker capture
(including dead-session skip), and the API serialization.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BxKY28XKCM6o4ag3nmaVsZ
Reproduced the failure against a real claude 2.1 in tmux. Two root causes,
both now fixed (the URL was never wrong — claude genuinely emits
`claude.com/cai/oauth/authorize`, so extraction was fine):
1. Submit race (the actual failure). `send_keys` sent the code and Enter
together; for a long real code the Enter is processed before Ink commits the
paste, so nothing submits — the session sits at "Paste code here > ****…",
exactly what the activity log showed. Fix: deliver the code as a bracketed
paste (tmux set-buffer/paste-buffer, new tmux.send_text), let it settle, then
send Enter separately (tmux.send_enter). Verified end-to-end: the separate
Enter submits and claude proceeds to the exchange.
2. Fragile onboarding. A fresh claude shows a theme picker, then the
login-method menu, before any URL — the old blind /login+Enter+Enter only
reached the menu by luck. Fix: start() now reads the pane each pass and reacts
— accept theme/trust/continue prompts, pick the default subscription option on
the login-method menu, and send /login once only when already onboarded at the
REPL.
Also: confirm login by watching ~/.claude.json (where claude stores the account
on Linux) plus a success-text fallback, and fail fast on an "OAuth error /
Press Enter to retry" screen instead of waiting out the poll. Tests updated to
the real TUI screen text. Suite green (200).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
Testing the web login surfaced a truncated authorization URL
(…client_id=9d1c250a-e61b-44d9-88) and a "missing redirect_uri" error: the
login session ran at the default 80 columns, so claude clipped the long URL and
capture-pane read it back cut off.
- Launch the login session with a very wide, tall window (500x50) via new
optional width/height on tmux.new_session, so claude prints the URL on one
unclipped line.
- Harden URL extraction to stop at box-drawing glyphs (U+2500–U+257F) in case
the TUI renders the link flush against a border.
Tests: assert the wide window is requested, and that extraction keeps a full
redirect_uri/PKCE URL intact and strips a trailing box border. Suite green (197).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
Phase 2 configures forge for the agents (operator only sets a credential_ref +
optional version pin) and lets them drive a junior→senior→deploy workflow:
- Credential resolution/injection (control/credentials.py): credential_ref pointers
(env:/file:/cmd:) resolved only at spawn, injected as FORGE_TOKEN + host var, with a
forge-host-scoped git credential helper reading the token from env (never on disk / in
the DB). Resolution is a fail-fast spawn gate.
- Role-based forge skills committed into the managed repo (control/skills_gen.py,
`handler forge-init`): forge-junior/senior/deploy + a workflow overview.
- Hard approval gate (hooks/gate.py, approvals table, migration 0002): merge/deploy —
and direct pushes to protected branches — are denied unless a DIFFERENT agent has an
`approved` record for the branch, pinned to the reviewed commit (approved_sha). Senior
records verdicts via `handler approve`/`reject`.
- forge/git seams (control/forge.py, control/gitops.py) matching the Phase 1 seam pattern.
- CI status poller (control/poller.py, `handler poll-ci [--watch]`) backfilling
ci_status/ci_checked_at via `forge ci list`.
- Fix: migrations/env.py commits explicitly after run_migrations — pysqlite on Py 3.12+
was rolling back the final migration's DDL + alembic_version stamp (latent in Phase 1).
Reviewed via a separate code-reviewer pass; gate-bypass and credential-scoping findings
addressed. 106 tests, ruff clean, verified end-to-end against real git + migrations.
Implements the Phase 1 MVP from the README: a stateless control layer + HTTP
API over a centralized database, with hook-enforced test/push gates.
- DB layer: SQLAlchemy Core, one schema rendering both Postgres (BIGSERIAL /
TIMESTAMPTZ / JSONB) and SQLite (INTEGER PK / TEXT / JSON) via portable types;
native ON CONFLICT DO UPDATE checkmark upsert on both dialects.
- Alembic dual-dialect migrations (render_as_batch for SQLite); tests run a real
`alembic upgrade head`.
- FastAPI: projects/agents/checkmark/log/answer/resume + shared log/context
routes, single global bearer token, higher-trust token gating shared-context
writes, project isolation on every route.
- Hooks (`python -m handler.hooks <event>`): Stop test gate (block on red),
PreToolUse AskUserQuestion defer + `git push` gate (tests then throwaway
build), Notification generic webhook (no-op without WEBHOOK_URL). Identity via
env injected at spawn; verify is the mock seam.
- Control CLI: spawn/list/attach/kill, hard `.mise.toml [tasks.test]` gate,
generated per-agent settings.json, identity + DATABASE_URL injected via tmux;
tmux is the mock seam.
- 45 tests (SQLite), ruff clean. Live claude/tmux/mise spawning deferred behind
the mocked seams.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5ZuS5pV1NS6eKsRZHXonY