BeeverForge core architecture

Extension model

Every external dependency of the core except the engine, Temporal, sits behind an adapter, and every adapter is chosen by a registry from configuration. A new provider is added by installing a package that declares an entry point, without changing the core.

Axes#

An axis is one kind of thing the core can be extended with. The published list is AXES in beeverforge/plugins.py:

tracker  board  forge  ci  harness  box  event  metrics  channel
notifier  identity  role  session_store  token_pool  box_runner
credential  board_setup  extract  embed  preview  credential_store

A test holds this list to exactly what the registries ask for, and another checks that every registry consults the entry-point loader.

Built-in kinds on the main axes
AxisBuilt-in kinds
harnessclaude_code, codex, kimi, opencode
trackerlocal, github, jira, azure_devops
boardlocal, github, jira, azure_devops
forgelocal, github, azure_devops
cinone and local, github and github_actions, azure_devops and azure_pipelines; each pair names one observer
boxcontainer, worktree, fargate (description only; its runner is an add-on)
box_runnerNone
channelpanel
notifierpanel
identitylocal, oidc
metricsnull, sqlite, memory
previewcompose, none
credential_storeenv, file

The entry-point group#

  • A package declares its adapters in one entry-point group, beeverforge.adapters. Each name is <axis>.<kind>, for example forge.gitlab. A name of any other shape is skipped with a warning that shows the expected form.
  • Lazy. Registries look in their own table first. Entry points are loaded once, on the first lookup of a kind that is not built in, and cached; a deployment that uses only built-in kinds never loads one. Listing installed add-ons reads package metadata without importing them.
  • A broken add-on is logged and ignored; the others still load.
  • Built-ins win a collision. Declaring forge.github does not replace the shipped row: the built-in answers and the collision is logged. An add-on cannot change what an existing kind means for every project on a deployment.
  • An unknown kind refuses by name. The registry raises with the kind, the known kinds and, where the kind belongs to a known add-on package, which package ships it. It never falls back to a default provider.
How a registry resolves a kindA registry first looks in its own table, and a built-in row always answers. Only for a kind that is not built in are the entry points loaded, once. An add-on row named after the axis and the kind answers; otherwise the registry refuses, naming the kind.yesnoyesnoA registry looks upa kind on its axisBuilt in?The built-in row answersAn add-on row withthe same name isignored and reportedThe beeverforge.adaptersentry points are loadedonce and cachedA malformed name is skippedwith a warning; a brokenadd-on is logged and ignoredA row named<axis>.<kind>?The add-on’s builder answersRefusal: the kind, theknown kinds and the packagethat ships it, if known.No default provider.
How a registry resolves a kind. Its own table is read first, so a deployment that uses only built-in kinds never loads an entry point.

Born with two#

ADR-0022 sets the bar: an axis is agnostic not because it declares an interface, but when it is born with more than one implementation and a seam that dispatches between them. Protocols are @runtime_checkable and tests check them with isinstance.

The rule is not met on every axis today. The channel, notifier, session_store and token_pool axes have one built-in kind each, and box_runner has none. No test requires two built-ins per axis; per-axis tests exist for the preview and credential_store axes.

Adapter conformance#

beeverforge conformance-adapter <kind> <factory> runs an adapter against the platform’s conformance suite, beeverforge/conformance/adapters.py. The factory is an import path, pkg.module:attr, naming an instance, a class or a function that returns one.

  • The checks are plain functions that return findings. They construct nothing remote and change nothing remote; they exercise the local contract: signatures, degradation rules and the shape of refusals.
  • There are checks for channels, notifiers, identity, boards, trackers, forges, harnesses, CI observers, boxes and previews.
  • Examples: a tracker read must return None when it could not read and an empty list when there is nothing; a forge must send a credential only to the host that issued it; an observer’s health check returns a boolean and never raises.

beeverforge conformance <project> is a different command: it checks a project’s manifest, not an adapter.

The stack needs no adapter#

The core names no build tool. The manifest’s setup: and validate: entries are shell commands, and the box runs each one as written: sh -c in a container, a shell in a worktree. A project in any language is supported by declaring its own commands, with no adapter.

Example: a notifier add-on#

From docs/writing-an-addon.md. The package declares one entry point:

[project.entry-points."beeverforge.adapters"]
"notifier.acme" = "beeverforge_acme:build_notifier"

and implements what the core calls:

class AcmeNotifier:
    def notify(self, *, message: str, level: str = "info",
               about: str = "") -> str | None:
        ...                      # your provider
        return None              # or the provider's own handle for the message


def build_notifier(project=None, **_kw):
    return AcmeNotifier()

Each axis calls its builder with its own arguments; the guide lists them per axis, and covers how a row declares its display name and the remedies the core shows for that provider.

Sources#