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
Draft
feat(adapters): coding-CLI adapter registry (Seam A) — out-of-tree adapters with zero core edits#239pbean wants to merge 1 commit into
pbean wants to merge 1 commit into
Conversation
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.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
The transport axis has long been extensible out-of-tree (
register_multiplexer+ thebmad_loop.mux_backendsentry-point group). The coding-CLI adapter axis had no equivalent seam: a CLI needing a new adapter class forced a name-branch incli._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.pyedits, 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):lru_cache/cache_clear— adapters are built per run in_make_adapters(its ownby_cfgcache), so there is nothing to cache.configure_*/matches(platform)/platform defaults — an adapter kind is selected by theprofile.adapterfield alone, not a policy knob orsys.platformpredicate.Two dataclasses:
AdapterKind(name, needs_mux, load)—loadis a lazy thunk, so registration/validation/listing never import a heavy adapter module (nor an optional dep likehttpx).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
adapters/registry.py:register_adapter(first-wins so builtins can't be shadowed), builtin loader (genericneeds_mux=True,opencode-httpneeds_mux=False), thebmad_loop.adaptersentry-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_adaptersrewritten to a registry lookup keyed onprofile.adapter; the shared mux is resolved + usability-checked only whenkind.needs_mux; the solehooklessselection branch is gone; thesynthesizes/pathsaxis 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.tomlmigrated toadapter = "opencode-http".load_profilesgains abmad_loop.profilesentry-point scan (precedence packaged < entry-point < project), degrade-not-crash intoexternal_profile_errors().bmad-loop adapterscommand +cmd_validateadapter.kindfinding (viaknown_adapter_kinds(), never a hardcoded set) + external-error surfaces; three new check ids registered inchecks.VALIDATE_CHECKS.Guardrails honored
profile.adapterenforced via the registry (known_adapter_kinds), never a hardcoded set.registry.pyimports adapter classes only inside lazyloadthunks.profile.pydoes not validateadapterat parse time.opencode-httpmigrated to a registered builtin, with a regression pin that its dispatch is unchanged.Tests
tests/test_adapter_registry.py(fresh_adapter_registryfixture + 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, theby_cfg (cfg, synthesizes)cache,needs_muxgating,construct_error→SystemExit, and opencode-http dispatches unchanged.tests/test_profile.py.Verification
pytestunderGIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/nullon 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 unmodifiedmain, unrelated to this change.trunk checkclean.bmad-loop adapterslists it (scratch external yes scratch),known_adapter_kinds()/get_profile("scratch")resolve it, and_make_adaptersselects it for all three roles — with no edits to any core.py.