runward

RW™ · V0.22.0

Docs · Décisions · ADR-0030

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.

  1. 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, plus CLAUDE_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 — not CURSOR_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 standard AI_AGENT=1). A detection-first design would silently privilege the two harnesses it can see.

  2. The universal baseline already carries agent operability. init writes, unconditionally and before any tool profile, AGENTS.md and 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 of init, runward check is fully runnable in any harness with no channel installed. A per-harness channel (a turn-end hook, the CI Action) only triggers that same check automatically; it adds no capability (confirmed against ADR-0028 and the packaging manifests, all of which invoke runward check --strict verbatim).

  3. The CLI is already ~95% agent-operable, with two real gaps. Every command is non-interactive except init, whose prompts are guarded by isNonInteractive(). The gaps that actually bite an autonomous agent are (a) the interactive guard keyed on stdout.isTTY only, so an agent in an allocated pty without --yes can hang on init; and (b) the absence of --json on check/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. init with no explicit --tools writes only the vendor-neutral core (AGENTS.md + .agents/skills/) — under --yes the 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 when stdin is not a TTY or when CI is 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 (init auto-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 --json on every command in one move. Deferred by need: check --json is the load-bearing one for autonomous operation; status/doctor/manifest/compliance follow 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 --json gives 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 check itself. That is the correct degradation, not a failure. The best-effort detection surface ships as runward wire (read-only, recommendation-only, wires:false invariant); the wider --json coverage beyond check (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.
← Tous les ADR