A Go toolkit for running CLI agent harnesses (Claude Code, Codex, OpenCode, pi, …) under supervision and exposing them as programmable chat sessions. The repository layers in four steps:
pkg/wrapper/— supervises a harness process under a PTY, streams its output, and classifies the run into a small vocabulary of normalized states (idle,failed,interrupted,waiting_for_input,blocked_by_cost,retry_later, …).pkg/screen/— a vt100 terminal emulator (per ADR-001 we wrapvt10x) that turns the harness's raw PTY byte stream into queryable screen state.pkg/turns/— per-harness adapters that translate screen state- wrapper status into a small set of chat events (
TurnComplete,ToolCall,Blocked,Errored).
- wrapper status into a small set of chat events (
pkg/chat/— the Go-level chat API:Conversation.Open,AcquireControl,Send,Events,History. Storage is pluggable via theStoreinterface;pkg/chat/memstoreships the in-memory default.
Transport layers stay out of the core packages and live in separate
binaries that import pkg/chat; this repo ships one such gateway,
cmd/harness-chatd (HTTP + SSE) — see Use over HTTP.
┌──────────────────────────────┐
│ pkg/chat (Conversation API) │
└──────────────┬───────────────┘
│
┌──────────────────┴──────────────────┐
│ │
┌────────▼──────────┐ ┌──────────▼──────────┐
│ pkg/turns │ │ pkg/transcript │
│ +harness/codex │ │ +codex │
│ +harness/cc │ │ +claudecode │
│ +harness/opencode│ │ +pi │
│ +harness/pi │ │ (read-only JSONL) │
│ +generic │ └─────────────────────┘
└────────┬──────────┘
│
┌────────▼──────────┐
│ pkg/screen │ vt10x emulator
└────────┬──────────┘
│
┌────────▼──────────┐
│ pkg/wrapper │ PTY supervisor + status classifier
└───────────────────┘
📖 Full documentation lives under
docs/md/(canonical markdown, renders on GitHub) and builds into a themed, dark/light HTML site with SVG diagrams:make docs # build docs/md/ → docs/html/ make docs-serve # preview at http://localhost:4321Start at the Getting Started guide or the Architecture overview.
go get github.com/olesho/harness-wrapper/pkg/chat
go get github.com/olesho/harness-wrapper/pkg/wrapper # supervisor onlyimport (
"context"
"github.com/olesho/harness-wrapper/pkg/chat"
"github.com/olesho/harness-wrapper/pkg/chat/memstore"
)
func main() {
ctx := context.Background()
conv, err := chat.Open(ctx, chat.Options{
Harness: "codex",
BinaryPath: "/usr/local/bin/codex",
WorkingDir: "/path/to/project",
Store: memstore.New(),
})
if err != nil { panic(err) }
defer conv.Close(ctx)
release, err := conv.AcquireControl(ctx)
if err != nil { panic(err) }
defer release()
turnID, err := conv.Send(ctx, "summarize this project")
if err != nil { panic(err) }
for ev := range conv.Events() {
if ev.Type == chat.EventTurn && ev.Turn.ID == turnID && ev.Turn.State == chat.TurnStateComplete {
break
}
}
history, _ := conv.History(ctx)
_ = history // [{Role:"user", Text:"summarize this project"}, {Role:"assistant", Text:"..."}]
}See the Chat API reference for the full library reference.
import (
"context"
"os"
"github.com/olesho/harness-wrapper/pkg/wrapper"
)
func main() {
res, err := wrapper.Run(context.Background(), wrapper.Config{
BinaryPath: "/usr/local/bin/claude",
Args: []string{"--print", "hello"},
Stdout: os.Stdout,
})
if err != nil { panic(err) }
_ = res.Status // wrapper.StatusIdle, StatusFailed, etc.
}go install github.com/olesho/harness-wrapper/cmd/harness-wrapper@latest
harness-wrapper claude -- --print helloSee the CLI guide for one-shot (run) and tmux-detached modes.
cmd/harness-chatd exposes pkg/chat over HTTP + Server-Sent Events so non-Go
clients can drive multi-turn conversations across a process boundary:
go run ./cmd/harness-chatd --bind 127.0.0.1:8080v1 has no auth — bind to localhost only. See the HTTP Gateway guide for the endpoint reference and ready-to-run Python and TypeScript example clients.
| Harness | Status detection | Turn detection | Session ID | Transcript reader |
|---|---|---|---|---|
| codex | ✅ | ✅ Token usage: footer |
✅ codex resume <uuid> |
✅ ~/.codex/sessions/ |
| claude-code | ✅ | ✅ ✻ <verb> for Ns line |
✅ assigned: --session-id |
✅ ~/.claude/projects/ |
| opencode | ✅ | ⏳ via waiting_for_input |
⏳ (no on-screen UUID known) | ⏳ (on-disk store in flux: JSON → SQLite) |
| pi | ✅ | ⏳ idle + Busy spinner |
✅ assigned: --session-id |
✅ ~/.pi/agent/sessions/ |
| generic | ✅ (fallback) | ✅ via waiting_for_input |
— | — |
The per-harness detail and "adding a harness" workflow are in the
Adapter Matrix. Other harnesses can be supported by implementing
turns.Adapter (and optionally the SessionIDExtractor / TranscriptReader capability interfaces).
pkg/wrapper/— PTY supervisor + Status vocabularypkg/wrapper/trace/— diagnostic event vocabularypkg/screen/— vt100 emulator wrapper (vt10x per ADR-001)pkg/turns/— turn-detection interface,genericfallback, per-harness adapters underharness/pkg/transcript/— read-only harness JSONL parsers (codex, claudecode, pi)pkg/chat/— Conversation API, Store interfacepkg/chat/memstore/— in-memoryStoreimplementationpkg/harness/— per-harness capability profiles, hook installation,RunTurnpkg/oneshot/— one typed turn, headless, with an auto-accept policypkg/turnproto/— the frozen structured-turn wire format + exit codespkg/env/— run a structured turn inside a workspace (internal/envholds the environment core)pkg/versions/— read API for the embeddedversions.json(pinned upstream versions per harness)pkg/discovery/— "is harness X installed on PATH, at what version?";models/adds the offline model registrycmd/harness-wrapper/— thin CLI front-end for the wrappercmd/harness-chatd/— HTTP + SSE gateway exposingpkg/chatto non-Go clientscmd/check-versions/— offline drift check against the npm registryclients/— Python + TypeScript example clients forharness-chatdinternal/screenbench/— bake-off harness used to choose the vt100 emulator + scripted recordertest/corpus/— recorded byte streams used by the bake-off and the adapter compatibility testsdocs/md/— canonical documentation sources;docs/gen/— the Go static-site generator
Hermetic suite (vet + gofmt + race + corpus replay):
make testPer-harness adapter tests double as the compatibility test suite: they replay byte streams from
test/corpus/ and assert that turn detection still fires correctly. The full strategy — five tiers
from golden-snapshot API freezes to nightly live conformance — is documented in
Testing Tiers.
When an upstream CLI ships a new version, the TUI markers / classifier strings / transcript schemas
can shift and break our adapters. The Makefile defines a local, developer-on-demand pipeline that
catches drift before users do:
make check-versions # offline pinned-vs-latest check via the npm registry (~2s, free)
make rebake-corpus HARNESS=<name> SCENARIO=<name> # refresh one scenario
make rebake-corpus-all # refresh all 18 scenarios (paid for codex/claude)pkg/versions/versions.json pins each harness to the upstream version its adapter was last verified
against. When drift is detected, Versions & Drift walks through
diagnosing, re-baking the corpus, tightening the regex, and bumping the pin.