Adding a Harness

How the automation-harness layer works across engines, and what it takes to support a new one.

Most pages in this section are about skill-bridge discovery: which file a tool reads (AGENTS.md, .cursor/rules/) so it can find your Knowledge Base. This page is about the layer above it, harness execution: configuration, authentication, model provider, headless mode, and who re-invokes the loop.

Bridge, Harness, Engine

Three words that are easy to conflate and mean different things:

TermWhat it isExample
BridgeThe file that lets a tool discover .pair/AGENTS.md, .cursor/rules/
HarnessAn environment configured to execute pair's process, config locations, skill paths, auth, model providerClaude Code, pi, opencode
EngineA process pair-cli run can drive headlessly, one iteration at a timepi, opencode, claude -p

A harness is usually also an engine, but the roles are independent: you may work interactively in one harness and drive unattended runs through another.

Fan-Out Has Three Realizations

Unattended delivery needs a way to run many cards without one context accumulating all of them. That capability has three realizations, in preference order (ADR-021):

  1. In-harness: the harness's own fan-out primitive (Claude Code's Workflow, Codex subagents). Cheapest where it exists: no second executable, no stream contract.
  2. External driver: pair-cli run: a fresh process per iteration on a chosen engine, re-invoked on the continue-token the skill prints. The portable baseline, and a legitimate choice even where subagents exist: a separate process shares no context, no tool state and no crash with its predecessor, which is stricter isolation than a subagent gives you.
  3. Degraded: one card per invocation, re-triggered by a human, CI job or cron. Only for a caller with neither of the above.

Whichever tier runs, the process logic lives in the skills and the policy lives in .pair/adoption/tech/automation.md. The driver decides how to invoke, never what to work on.

Supporting a New Harness

Two levels, and they are separable: a harness is useful after level 1 alone.

Level 1: Make it a harness (no pair code changes)

  1. Check skill discovery. If the tool reads AGENTS.md / .agents/skills/ / .claude/skills/, pair-cli install already put pair's skills where it looks. Most tools need nothing here.
  2. Write down what pair needs to know about it: config file locations and which are committable, skill-path declaration, context-file precedence, authentication (and whether it is CI-viable), access paths (MCP or not), model-provider configuration, headless execution, and what the tool explicitly does not support. That is exactly the shape of the per-harness guides in the agent-harness framework; add a sibling file there.
  3. Declare it, if your project restricts harnesses. tech/automation.md's ## Harness section lists the harnesses a project supports (and, optionally, Requires: mcp). Absent that section every harness is presumed supported.
  4. Provision it with /pair-capability-setup-harness, which verifies fitness against those declarations before writing any configuration.

Level 2: Make it an engine (a small, contained code change)

pair-cli run holds every engine as data: one entry per engine with its executable, headless and stream flags, how a skill invocation and a prompt are handed over, the autonomy and project-trust postures, and which stream event is terminal. Adding an engine is an entry in that map: there are no engine-specific conditionals anywhere else in the driver.

What you must be able to answer, with evidence rather than assumption:

  • What is the one-shot headless invocation, and which flag makes it emit machine-readable JSONL?
  • Which event ends a run, and how is a failure distinguishable from a success? pair-cli run reads an explicit terminal event and never the exit code: an unrecognised stream is a failed iteration, and the loop stops. Verify this against the real tool, not its docs alone.
  • How are confirmations expressed: a flag, or not at all? A tool with no confirmation mechanism is not treated as "safely confirming": the driver requires an explicit --autonomous for it.
  • Is there a project-trust decision, and where does it live? The driver reads such a store and never writes it.

Keeping the engine map as data is deliberate: it means moving it into the Knowledge Base later is a source swap rather than a refactor. Until then, adding an engine is a change to apps/pair-cli and a pull request: a conscious, recorded trade-off, not an oversight.

On this page