Skip to content

feat(adapters): coding-CLI adapter registry (Seam A) — out-of-tree adapters with zero core edits#239

Draft
pbean wants to merge 1 commit into
mainfrom
feat/adapter-registry
Draft

feat(adapters): coding-CLI adapter registry (Seam A) — out-of-tree adapters with zero core edits#239
pbean wants to merge 1 commit into
mainfrom
feat/adapter-registry

Conversation

@pbean

@pbean pbean commented Jul 21, 2026

Copy link
Copy Markdown
Collaborator

What & why

The transport axis has long been extensible out-of-tree (register_multiplexer + the bmad_loop.mux_backends entry-point group). The coding-CLI adapter axis had no equivalent seam: a CLI needing a new adapter class forced a name-branch in cli._make_adapters (if profile.hookless: …) and a hardcoded valid-kinds set. This PR lands the missing registry so a new adapter family — and its selecting profile — plugs in with zero core .py edits, exactly like a transport backend.

This is Session 1 (Seam A) of the PR #226 review plan. No Hermes-specific code, no hook-scope changes, no relay changes (those are Sessions 2–4).

Design

Mirrors adapters/multiplexer.py, with two deliberate asymmetries (documented in the module):

  • No lru_cache/cache_clear — adapters are built per run in _make_adapters (its own by_cfg cache), so there is nothing to cache.
  • No configure_*/matches(platform)/platform defaults — an adapter kind is selected by the profile.adapter field alone, not a policy knob or sys.platform predicate.

Two dataclasses:

  • AdapterKind(name, needs_mux, load)load is a lazy thunk, so registration/validation/listing never import a heavy adapter module (nor an optional dep like httpx).
  • AdapterBuilder(plain, dev, construct_error) — the plain class, the _DevSynthesisMixin-composed dev class, and the family's construction-failure type(s) (() = none; (OpencodeServerError,) for HTTP).

Changes

  • New adapters/registry.py: register_adapter (first-wins so builtins can't be shadowed), builtin loader (generic needs_mux=True, opencode-http needs_mux=False), the bmad_loop.adapters entry-point scan degrading into _EXTERNAL_ERRORS (never raises), get_adapter_kind (fail loud, naming known kinds), known_adapter_kinds, external_adapter_errors, detect_adapters.
  • cli._make_adapters rewritten to a registry lookup keyed on profile.adapter; the shared mux is resolved + usability-checked only when kind.needs_mux; the sole hookless selection branch is gone; the synthesizes/paths axis stays a documented cross-family pipeline contract; construct_errorSystemExit.
  • profile.py: CLIProfile.adapter (default "generic"), parsed but not parser-validated (validity is enforced against the live registry). opencode.toml migrated to adapter = "opencode-http". load_profiles gains a bmad_loop.profiles entry-point scan (precedence packaged < entry-point < project), degrade-not-crash into external_profile_errors().
  • bmad-loop adapters command + cmd_validate adapter.kind finding (via known_adapter_kinds(), never a hardcoded set) + external-error surfaces; three new check ids registered in checks.VALIDATE_CHECKS.
  • Docs: the out-of-tree registration recipe (two entry points), reaching parity with the transport axis.

Guardrails honored

  • Validity of profile.adapter enforced via the registry (known_adapter_kinds), never a hardcoded set.
  • registry.py imports adapter classes only inside lazy load thunks.
  • profile.py does not validate adapter at parse time.
  • opencode-http migrated to a registered builtin, with a regression pin that its dispatch is unchanged.

Tests

  • New tests/test_adapter_registry.py (fresh_adapter_registry fixture + scan helper): builtin+external registration, builtins-first-wins, unknown-kind fail-loud, broken-external degrades/recorded, one-bad-package-doesn't-hide-the-rest, detect_adapters, the by_cfg (cfg, synthesizes) cache, needs_mux gating, construct_errorSystemExit, and opencode-http dispatches unchanged.
  • Profile entry-point discovery tests in tests/test_profile.py.

Verification

  • Full pytest under GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null on both TMUX-set and TMUX-unset: 2732 passed, 7 skipped, 1 failed — the single failure (test_validate_findings_multiline_message_keeps_column_alignment) is a pre-existing env-sensitive RED confirmed on unmodified main, unrelated to this change.
  • Full trunk check clean.
  • Zero-core-edit proof: co-installed a scratch out-of-tree adapter package (two entry points) — bmad-loop adapters lists it (scratch external yes scratch), known_adapter_kinds()/get_profile("scratch") resolve it, and _make_adapters selects it for all three roles — with no edits to any core .py.

The transport axis has long been extensible out-of-tree (register_multiplexer
+ the bmad_loop.mux_backends entry-point group); the CLI adapter axis had no
such seam, so a CLI needing a new adapter *class* forced a name-branch in
cli._make_adapters and a hardcoded valid-kinds set. This lands the missing
registry so a new adapter family — and its selecting profile — plugs in with
zero core .py edits, exactly like a transport backend.

- New adapters/registry.py mirroring multiplexer.py: AdapterKind(name,
  needs_mux, load-thunk) + AdapterBuilder(plain, dev, construct_error),
  register_adapter (first-wins), builtin loader (generic, opencode-http),
  the bmad_loop.adapters entry-point scan degrading into _EXTERNAL_ERRORS
  (never raises), get_adapter_kind (fail loud), known_adapter_kinds,
  detect_adapters. Deliberately NO lru_cache/cache_clear and NO
  configure_*/platform machinery — adapters are built per-run and selected by
  profile.adapter data, not a policy/platform choice (documented in the module).
- cli._make_adapters rewritten to a registry lookup keyed on profile.adapter:
  the shared mux is resolved+usability-checked only when kind.needs_mux; the
  sole `hookless` selection branch is gone; the synthesizes/paths axis stays a
  documented cross-family pipeline contract; construct_error -> SystemExit.
- profile.py: CLIProfile.adapter (default "generic"), parsed but NOT
  parser-validated (validity is enforced against the live registry); opencode
  migrated to adapter = "opencode-http". load_profiles gains a
  bmad_loop.profiles entry-point scan (precedence packaged < entry-point <
  project), degrade-not-crash into external_profile_errors().
- `bmad-loop adapters` command + cmd_validate adapter.kind finding (via
  known_adapter_kinds, never a hardcoded set) + external-error surfaces; the
  three new check ids registered in checks.VALIDATE_CHECKS.
- Tests: new tests/test_adapter_registry.py (fresh_adapter_registry fixture +
  scan helper) covering registration, builtins-first-wins, unknown-kind
  fail-loud, external degradation/isolation, detect_adapters, the by_cfg
  (cfg, synthesizes) cache, needs_mux gating, construct_error->SystemExit, and
  the opencode-http dispatch-unchanged regression pin; profile entry-point
  discovery tests in test_profile.py.
- docs/adapter-authoring-guide.md: the out-of-tree registration recipe (two
  entry points), reaching parity with the transport axis.

No Hermes-specific code, no hook-scope changes, no relay changes (Sessions
2-4). Full suite green on both TMUX-set and TMUX-unset (one pre-existing
env-sensitive failure unrelated to this change); trunk check clean.
@coderabbitai

coderabbitai Bot commented Jul 21, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 008be34a-2260-4ba4-b2e3-f9e08b7cf95a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/adapter-registry

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant