Skip to the content.

ADR 0011: HostAdapter owns host verbs; Operations compose them; CLI verbs compose Operations

Status: Accepted Date: 2026-05-02

Context

CLI::Fork (and, by delegation, CLI::Fix) currently fuses four concerns into one class:

  1. CLI shell — argv parsing, usage errors, summary, post-clone hooks.
  2. Host-API ceremony — call adapter.fork, poll adapter.fork_ready? in a 12×5s loop, branch on adapter.already_forked?.
  3. VCS work on the local filesystem — git clone into <root>/<owner>/<repo>, reuse if .git exists, add an upstream remote.
  4. Host-specific URL templating — https://github.com/<owner>/<repo>.git is hardcoded at fork.rb:51.

ADR-0001 (and the design doc) already commit to GitLab and Codeberg adapters as a near-term goal. The hardcoded github.com literal and the GitHub-shaped readiness loop both block that. A previous refactor (commit 3d53ffc, “dissolve ForkClone into Fork”) pulled in the opposite direction: it merged the bootstrap primitive into the CLI verb. That made the multi-host port harder, not easier.

Two reasonable shapes for splitting:

  1. Wider adapter, thicker CLI verbs. Push everything host-specific into HostAdapter (fork, comment, pull_request_url, clone_url); CLI verbs compose adapter calls and Git calls directly.
  2. Adapter + Operations layer. Same wider adapter, plus a thin layer of host-agnostic primitives (Operations::Fork, Operations::Clone) that compose HostAdapter and Git. CLI verbs compose Operations.

Decision

Adopt shape 2: a three-layer split.

Three sub-decisions that shape the adapter’s surface:

Reasoning

The data layer / TUI layer split in docs/design.md is built around adapters being swappable. That bet pays off only if “swap in a GitLab adapter” really is a weekend project. Today it isn’t — a GitLab port would have to fork (no pun intended) the GitHub-shaped readiness loop, the hardcoded clone URL, and the same-shape compare URL out of CLI::Fork and CLI::Submit. With this split, a GitLab port is: implement HostAdapter#fork (likely no poll), #clone_url (return https://gitlab.com/...), #pull_request_url (return the GitLab MR URL form), #comment. Operations and CLI don’t change.

The Operations layer (rather than a fatter CLI) earns its keep because two things in the bootstrap aren’t host-API and aren’t raw git either: the clone-or-reuse policy (skip clone if .git exists) and the upstream remote policy (always add upstream pointing at the canonical repo’s clone_url). Those are gem-contribute conventions, not git primitives, and they’re shared between fix and fork. Putting them in their own class makes them testable in isolation and keeps the CLI verbs honestly thin.

This direction supersedes the merge in 3d53ffc. That commit was right that ForkClone and Fork had drifted into near-duplicates; it was wrong that the resolution was to dissolve the primitive into the verb. The correct resolution was to re-extract the primitive at a sharper boundary — which is what this ADR does.

Alternatives considered

Consequences