ADR-0030.
the agent operates runward — neutral baseline, best-effort harness detection, machine-readable surface
Date: 2026-07-16 Status: accepted Deciders: Thibault Souris (maintainer) Method: decision-loop — grounded in a runtime-signal survey across 11 harnesses and a full CLI-operability audit, cross-read against the vendor-neutral and operator-installs invariants, challenged against the "just detect the harness and wire it" reflex, durable position
Context
runward is installed and operated almost entirely by an AI agent, not by a human typing at a prompt. The honest test of the product is therefore: can an agent discover runward, stand it up correctly, and run the whole chain with no human at the keyboard? Three findings, each verified against the code, frame the answer.
Harness detection cannot be the load-bearing mechanism. Identifying the harness a CLI runs inside — without assuming its config files are already in the repo — depends on runtime signals (environment variables the harness injects into the child process). The survey found reliable signals for only Claude Code (
CLAUDECODE, plusCLAUDE_CODE_ENTRYPOINT, which also covers Claude Cowork) and Gemini CLI (GEMINI_CLI); a usable-but-observed signal for Cursor (CURSOR_AGENT, the agent-mode marker — notCURSOR_TRACE_ID, which is broader than agent mode); an unreliable one for Codex (CODEX_SANDBOX, present only in sandbox mode); and nothing for Copilot CLI, Windsurf, Kiro, Continue, Aider, and Trae. There is no cross-harness convention (no standardAI_AGENT=1). A detection-first design would silently privilege the two harnesses it can see.The universal baseline already carries agent operability.
initwrites, unconditionally and before any tool profile,AGENTS.mdand the vendor-neutral phase skills under.agents/skills/(src/commands/init.ts:134,138). These are the converged surfaces read by 14+ harnesses. From the end ofinit,runward checkis fully runnable in any harness with no channel installed. A per-harness channel (a turn-end hook, the CI Action) only triggers that samecheckautomatically; it adds no capability (confirmed against ADR-0028 and the packaging manifests, all of which invokerunward check --strictverbatim).The CLI is already ~95% agent-operable, with two real gaps. Every command is non-interactive except
init, whose prompts are guarded byisNonInteractive(). The gaps that actually bite an autonomous agent are (a) the interactive guard keyed onstdout.isTTYonly, so an agent in an allocated pty without--yescan hang oninit; and (b) the absence of--jsononcheck/status/doctor, forcing the agent to parse coloured human text instead of a stable contract.
A fourth, pre-existing defect surfaced alongside: init defaults to the claude profile — ["claude"] under --yes, pre-checked in the wizard (init.ts:51,54) — which contradicts the vendor-neutral invariant every other surface upholds.
Decision
The agent operates runward from a strictly neutral, machine-readable baseline; harness detection is a best-effort convenience layered on top, never a prerequisite, and it never installs anything itself.
Four concrete positions:
- Neutral baseline.
initwith no explicit--toolswrites only the vendor-neutral core (AGENTS.md+.agents/skills/) — under--yesthe tool-profile list is empty, and the interactive wizard pre-checks nothing. No harness is privileged by default. A channel is an opt-in the operator (or the agent, on the operator's approval) adds afterward. - Detection is best-effort and read-only. Where a runtime signal exists (Claude Code / Cowork, Gemini CLI, Cursor) or a config file is already present, runward may recognise the harness and recommend the matching channel — but recognition prints a suggestion, it never writes a hook, a
.git/entry, or a settings file. Where no signal exists, runward says so plainly and asks; it does not guess. This preserves ADR-0012 ("the operator installs") exactly: detection informs, the operator's approved gesture wires. - Machine-readable surface.
check(and, over time, the other read paths) gains--json: a stable, deterministic contract carrying the verdict, current gate, deliverable states, and conformance gaps — so an agent drives on data, not on scraped text. The exit-code contract (0/1/2) is unchanged and remains the primary signal. - Hardened non-interactivity.
isNonInteractive()also returns true whenstdinis not a TTY or whenCIis set, so an autonomous run never hangs on a prompt it cannot answer.
Claude Cowork is treated as a member of the Claude family: same .claude/ adapter, same CLAUDECODE runtime signal. It is named explicitly because it was verified to run the plugin to a green verdict, not assumed.
Alternatives discarded
- Detection-first (
initauto-selects the profile for the detected harness). Reliable for two harnesses, blind for six. It would dress a partial capability as a general one and privilege Claude/Gemini by accident — the opposite of the neutral invariant. - Ship a cross-harness marker and rely on it. No such standard exists; inventing one runward-side helps nobody, since the harnesses would have to adopt it.
- runward wires the channel once detected. Violates ADR-0012. The write stays the operator's gesture; runward at most prints the exact command.
- Full
--jsonon every command in one move. Deferred by need:check --jsonis the load-bearing one for autonomous operation;status/doctor/manifest/compliancefollow as the driving loop demands them, additively, without breaking the text default.
Consequences
- Positive. The agent-operability story becomes true rather than aspirational, and true without depending on a detection layer that can only ever be partial. The neutral default closes a standing vendor-neutrality breach.
check --jsongives the operating agent a contract to branch on. The framing — "runward runs in every harness through the baseline, and suggests automation where it can recognise the tool" — is both honest and stronger than "runward detects your tool." - Negative, accepted. An agent in a harness with no runtime signal must be told which channel it is in (a one-line question), or it simply runs the baseline
checkitself. That is the correct degradation, not a failure. The best-effort detection surface ships asrunward wire(read-only, recommendation-only,wires:falseinvariant); the wider--jsoncoverage beyondcheck(status/doctor/manifest/compliance) is the remaining follow-on, additive and non-breaking. - On other boundaries. Reinforces ADR-0012 (operator installs), ADR-0018 (vendor-neutral skills as the baseline), ADR-0028 (channels as convenience triggers of one exit-code port). No conflict; this ADR names the operating model those three imply.
Reevaluation trigger (mandatory, dated)
Reopen if a cross-harness runtime convention for agent identity emerges (a standard environment marker adopted by more than two major harnesses), or if the autonomous driving loop needs a read path that has no --json yet — then extend detection and machine output accordingly, still without runward writing a channel itself.
Trigger set on: 2026-07-16 · Watched via: harness environment-variable documentation across versions, and the agent-operability audit rerun each minor release.
References
- ADR-0012 — the operator-installs invariant this preserves.
- ADR-0018 — the vendor-neutral baseline this makes the default.
- ADR-0028 — the channels this reframes as convenience triggers of one port.
- The harness runtime-signal survey and CLI-operability audit (2026-07-16) — the two findings this rests on.