runward

RW™ · V0.22.0

Docs · Operate · Wire the gate

Wire the gate.

runward's gate is a small deterministic, zero-LLM check that reads your runward/ mission and answers one question with an exit code: is every expected deliverable filled (and, under --strict, every mapped rule accounted for)? By default that check runs only when a human types runward check. "Wiring the gate" means installing a thin sample so the same check also runs at the moment it matters: a commit, a CI run, or the moment an AI agent finishes a turn. The load-bearing rule is that runward hands over these samples but never installs them. It writes nothing into .git/, edits no harness config, registers no CI. You (or an agent acting on your explicit say-so) do the wiring. This page covers every channel, the exact exit-code contract they read, and why the boundary is drawn where it is.

The port contract: what every channel reads

Each channel does the same thing — it runs runward check and reacts to the exit code. That exit code is the entire contract:

Exit Meaning
0 current gate clean: every expected deliverable is filled (and under --strict, every CRITICAL/HIGH rule mapped to a build phase is accounted for)
1 gaps: a deliverable is unfilled, a --strict rule-conformance gap remains, or an enabled --hooks hook failed
2 no runward/ mission found here or above

Where the codes come from

These are set in the check command itself:

  • exit 2 when no mission is found
  • exit 1 on any gap
  • exit 0 when clean

The same code appears in the machine-readable --json payload as exitCode, but the process exit code stays the primary signal. The gate that a CI job reads is the same one a human reads by typing runward check — no channel adds gate logic or influences the verdict.

It is runward's own gate, not arbitrary shell

The command inside every adapter is runward's own gate, not arbitrary shell:

  • runward check runs the deliverable audit.
  • runward check --strict additionally enforces the floor rule-conformance manifests.

The channels you can wire

After runward init, the inert samples live in runward/adapters/ (copied there from the templates by init). Each file is text until you install it. The universal channels, valid whatever AI harness produced the code, are declared in code as pre-commit and ci-required-check.

Local: git pre-commit hook

runward/adapters/pre-commit is a POSIX sh script that runs runward check --strict (preferring an installed runward, falling back to npx --yes runward) and lets a non-zero exit abort the commit. You install it yourself, two supported ways.

Copy it into .git/hooks/:

cp runward/adapters/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit

Or keep hooks in-tree:

mkdir -p .githooks && cp runward/adapters/pre-commit .githooks/pre-commit
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks

Bypass a single commit with git commit --no-verify when you have a reason.

CI: required status check

Two forms ship:

  • runward/adapters/github-actions.yml — a job that checks out, sets up Node 20, and runs npx --yes runward check --strict on pull_request and on push to main. Copy it into .github/workflows/, then make it a required status check on your protected branch so no gap merges.
  • runward/adapters/gitlab-ci.yml — the same one line (npx --yes runward check --strict) on merge-request and default-branch pipelines. Merge it into .gitlab-ci.yml and require the pipeline on your protected branch.

The published composite action

There is also a published composite GitHub Action at the repo root, action.yml (ADR-0028), for teams who prefer a versioned marketplace action over a copied job:

- uses: stranxik/runward@<sha>   # pin by commit SHA
  with:
    path: .           # dir containing the runward/ mission
    strict: 'true'    # verify the rule-conformance manifests
    version: latest   # an npm version or dist-tag

Its three inputs are path, strict, and version. Their handling is hardened:

  • Inputs are passed through the environment and never interpolated into the run script, so a malicious version/path cannot break out and inject shell (CWE-78).
  • version is additionally validated against an allowlist (latest, next, or a three-part semver) before it reaches npx, exiting 2 on a bad value.

The action needs nothing but the repo and Node ≥ 20 — deterministic, zero-LLM, no secrets, no model key.

CI is the audit-evidence seam: a dated, versioned record that the gate passed on every merge, feeding a regulated pipeline as audit-ready supporting evidence.

Agent turn-end hooks (per-harness, non-privileged)

Where a harness can run a command when the agent finishes a turn, the same one line — runward check --strict — closes the loop so an agent cannot finish with the gate never run. These turn-end hooks surface the verdict in the agent's loop; the hard-blocking enforcement points remain pre-commit and CI.

runward ships example samples for harnesses that expose a clean seam; none is privileged over another:

  • runward/adapters/claude-code-settings.json — a Stop-hook block you merge into .claude/settings.json (or .claude/settings.local.json); it runs the gate at turn-end and surfaces the verdict. The sample command is runward check --strict 2>&1 || true, so it reports the gap into the loop but does not itself fail the turn — advisory, not blocking.
  • runward/adapters/kiro-hooks.json — a Stop-trigger hook you copy into .kiro/hooks/, same one line.
  • runward/adapters/bmad-review-layer.toml — adds the gate as one review layer BMAD calls beside its own reviewers; a complement, not a competitor.

These are explicitly examples, not the privileged path: any harness that can run a command at turn-end wires the same line, and where a harness offers no such seam the pre-commit and CI channels already gate the code whatever agent produced it.

runward wire: the read-only advisor

runward wire helps an operator (or an agent) pick the right channel. It detects which AI harness is running the command and prints the matching auto-trigger channel plus the always-available universal channels. It is strictly read-only: it detects and prints, it never wires anything, and its exit code is always 0. It never prompts, so an agent-driven run never hangs.

Two signal strengths

Detection has two signal strengths:

  1. Runtime signal — a verified environment marker the harness injects into the child process (e.g. CLAUDECODE=1, GEMINI_CLI=1, CURSOR_AGENT=1), reported as status: "detected". Strong: it attests this run.
  2. Config file — a tool-profile marker already in the repo (e.g. .cursor/rules/runward.mdc, GEMINI.md), reported as status: "config-detected". Weaker: a folder's presence does not prove that tool is running this command.

When neither resolves, the status is undetermined — a normal outcome, not an error; the expected case for harnesses with no marker (Copilot CLI, Windsurf, Kiro, Continue, Aider, Trae). The agent then simply asks the operator which tool it is.

Flags

  • -p, --path <path> — the project directory containing the runward/ mission.
  • --json — a stable machine payload: { runward, mission, schemaVersion, status, harness, label, family, detectedVia, signal, recommendedChannel, candidateChannels, wires, operatorAction }.

In every JSON payload, wires is the literal false — the ADR-0012 invariant made machine-checkable, so a caller can assert runward wrote nothing. The operatorAction field tells the driving agent what to do next:

  • offer-to-wire-sample when a channel is recommended.
  • ask-operator-which-harness when undetermined.

What wire does and does not do

What wire outputs:

  • the detected harness (or "undetermined")
  • the recommended channel with its sample path (or a note when the channel lives in a distribution packaging rather than a mission adapter — e.g. Gemini's extension, Cursor's advisory hook)
  • the two universal channels
  • a "Next" line telling the agent to offer to wire the sample on the operator's approval

What it never does: write a file, edit .git/, install a hook, or prompt. Its own closing line states it: "runward wires nothing — you are the operator's hands (ADR-0012)".

The ADR-0012 invariant: why runward never auto-wires

The design problem was real: a gate that is not called is not a gate. The opening incident was exactly this — an agent finished a turn, the gate was never invoked, nothing surfaced the gap.

What was rejected

The tempting cure was rejected outright:

  • a runward daemon
  • a file-watcher
  • a managed hook runtime
  • silent installation into .git/hooks/

Each would turn runward from a frame into a runtime, the one thing the doctrine forbids it to become. Auto-installing the hook or editing .claude/settings.json on init was rejected on security grounds: surprise execution wired into a repo without the operator's act.

The decision: gate as port, channels as inert samples

So the decision: treat the gate as a port and ship the channels as inert, copy-in samples. runward emits them; the operator wires them. Nothing executes on init, on clone, or on check; runward writes no file into .git/, edits no harness config, registers no CI. This is the same opt-in posture as the --hooks seam (ADR-0008), one step stronger: runward does not even run these — it hands them over.

The 2026-07-12 agentic amendment

A 2026-07-12 amendment operationalizes this for agentic workflows: AGENTS.md directs the operating agent to proactively offer to wire a channel, acting only on the operator's explicit approval and never silently.

This does not weaken the invariant — runward still installs nothing and writes nothing into .git/. The difference from the rejected auto-install is consent: the agent proposes and the operator decides, versus runward acting on its own. "Agent-operable" means an agent can drive runward end-to-end, including performing the wiring on your say-so; it does not mean runward modifies your repo behind your back.

Adapters are runward-owned templates

The adapters are runward-owned templates, treated like workflows/ and rules/:

  • emitted by init
  • refreshed by runward update (which may overwrite them)
  • verified by runward doctor

They are never mission state.

The gate wired on runward itself (a worked example)

runward's own repository carries a runward/ mission and makes the strict gate a required CI check on itself. Its CI runs node dist/cli.js check --strict as a "Self-gate — runward gates runward" step, and again inside a network-isolated namespace to prove the gate needs nothing but the repo — any network call fails the job. That is the CI channel in production: dogfooding as a required check, not a claim.

Quick reference for an operating agent

  • Detect the channel: runward wire --json (read-only, exit 0; read recommendedChannel, candidateChannels, operatorAction).
  • The gate itself: runward check (deliverable audit) or runward check --strict (also enforce rule-conformance); exit 0/1/2.
  • Samples to hand the operator (never install yourself): runward/adapters/pre-commit, runward/adapters/github-actions.yml, runward/adapters/gitlab-ci.yml, runward/adapters/claude-code-settings.json, runward/adapters/kiro-hooks.json, runward/adapters/bmad-review-layer.toml.
  • Refresh/verify samples: runward update, runward doctor.
  • Always act only on the operator's explicit approval before copying a sample into their harness.

Why this choice

ADR-0012 deliberately makes runward emit inert channel samples that the operator (or an agent on the operator's explicit approval) installs, rather than having runward auto-wire them.

The rejected alternative — a daemon/file-watcher, a runward-managed hook runtime, or silent install into .git/hooks or .claude/settings.json on init — would turn runward from a frame into a runtime and wire surprise execution into a repo without the operator's act, the exact security foot-gun the doctrine forbids.

Consent is the whole distinction: the agent proposes, the operator decides. The invariant is even made machine-checkable via the constant wires:false in every wire --json payload.

What comes next

ADR-0012 sets a dated reevaluation trigger: reopen if a harness the community actually uses cannot be wired by a static sample (i.e. wiring it would require runward to run, watch, or install something itself). At that point the team would re-examine whether a thin adapter library — still invoked by the harness, never a daemon — is warranted, rather than expanding runward's runtime surface.

← Docs