HarnessAgentAdapter

License

The Harness.AgentAdapter behaviour plus six adapters for headless coding-agent CLIs — Claude Code, Cursor, Codex, Grok, Antigravity, and Pi — all driven over OTP Ports. No per-agent SDK, no output normalization: an adapter builds the agent's headless command line, spawns it, classifies the port's messages as raw output / termination / failure, and can kill an in-flight run. It does not parse the agent's output.

Why

Every one of these CLIs ships its own JSON-ish transcript shape and revises it across releases. A normalization layer chases that churn forever and still loses information the consumer wanted. This package's adapters pass raw output straight through — the consumer is expected to be an AI that reads a transcript natively, not a program pattern-matching on a schema. What stays code is mechanical: spawn the process, capture its bytes, detect that it stopped, kill it on a deadline. What agent output means is left to whoever reads it.

Harness.AgentAdapter.invoke/2 does the generic Port spawn shared by every adapter; Harness.AgentAdapter.Driver.run/3 drives a spawned run to completion under total/idle/progress deadlines. Termination is derived from the port closing or a deadline firing — never from the process exit code, which every adapter here treats as advisory-only.

Installation

Add it to your dependencies in mix.exs:

def deps do
[
{:harness_agent_adapter, "~> 0.1.0"}
]
end

Docs: hexdocs.pm/harness_agent_adapter.

Usage

alias Harness.AgentAdapter
alias Harness.AgentAdapter.Driver
alias Harness.AgentAdapter.Invocation
invocation = %Invocation{
prompt: "Fix the failing test in lib/foo.ex",
cwd: "/path/to/isolated/worktree",
log_tag: "run-123",
model: "gpt-5.6-sol",
rule_content: "# Operational rules\n\nRun the project's checks before finishing.\n"
}
{:ok, outcome} = Driver.run(AgentAdapter.Codex, invocation)
outcome.kind # :exited | {:timed_out, :idle | :total} | {:reflex_halted, reason} | {:error, reason}
outcome.output # the agent's raw transcript, unparsed
outcome.exit_status # advisory only — never branch success on this

Driver.run/3 is the composed entry point most callers want (spawn + drive-to-completion under deadlines). AgentAdapter.invoke/2 is the lower layer — it just spawns and returns a Harness.AgentAdapter.Run handle for a caller that wants to drive the receive loop itself.

Adapters

Adapter Headless CLI Raw output format
Harness.AgentAdapter.Claude claude -p --output-format stream-json
Harness.AgentAdapter.Cursor cursor-agent -p --output-format stream-json
Harness.AgentAdapter.Codex codex exec --json
Harness.AgentAdapter.Grok grok -p / agent subcommand --output-format streaming-json
Harness.AgentAdapter.Antigravity agy -p plain text
Harness.AgentAdapter.Pi pi -p --mode json

All six adapters declare worktree_isolation: true — their headless mode edits only the port's cwd, never a directory outside it. Model and agent are orthogonal: Invocation.model threads to each adapter's --model flag independently of which adapter (assignee) you dispatch to, so e.g. Cursor is a multi-model front end, not "the Composer agent."

Configuration

config :harness_agent_adapter, :run,
total_timeout: 1_800_000,
idle_timeout: 300_000,
progress_timeout: 300_000,
terminate_grace_ms: 1_000

All keys are optional; see Harness.AgentAdapter.Driver's moduledoc for the shipped defaults and what each deadline guards (total-run budget, idle-output window, no-mechanical-progress window). :terminate_grace_ms is the SIGTERM-to-SIGKILL window OSProcess.kill_tree/1 waits so agent CLIs can flush their transcript tail. Per-call opts passed to Driver.run/3 override the application config.

Live integration tests

mix test --include integration drives each real agent CLI end to end. The model is required, never defaulted, so pin one per adapter — any id the CLI lists works:

Env var List models with
HARNESS_AGENT_ADAPTER_LIVE_MODEL_CLAUDE claude --help (--model)
HARNESS_AGENT_ADAPTER_LIVE_MODEL_CODEX model in ~/.codex/config.toml
HARNESS_AGENT_ADAPTER_LIVE_MODEL_CURSOR cursor-agent --list-models
HARNESS_AGENT_ADAPTER_LIVE_MODEL_GROK grok models
HARNESS_AGENT_ADAPTER_LIVE_MODEL_ANTIGRAVITY agy models
HARNESS_AGENT_ADAPTER_LIVE_MODEL_PI pi --list-models

A missing CLI or unset variable fails the test with the exact fix. Your own adapter run through Harness.AgentAdapter.Testing.ConformanceCase reads HARNESS_AGENT_ADAPTER_LIVE_MODEL_<NAME>, where <NAME> is the module's last segment, upcased.

Public API

The package's public surface is intentionally small — an adapter and its callers should need nothing beyond these modules:

Two seams make the package standalone rather than harness-coupled:

Primary Consumer

harness is the OTP-native orchestration engine this package was extracted from and the primary consumer: it dispatches implementer and reviewer agents into isolated git worktrees through these adapters as part of its implement → review → land loop.