Skip to main content

ADR-0016: Stack profile plugin protocol in a2c_core

  • Status: Accepted
  • Date: 2026-06-22
  • Deciders: Michel Gillet

Context

A2C distinguishes stack profiles (technology-stack defaults) from task-body profiles (profiles/task-body/*.yaml — section templates for planning tasks). Task-body profiles are implemented: YAML data loaded and validated by a2c_core.profiles.task_body.

Stack profiles under profiles/ (for example cpp-cmake-conan) are mostly documentation today: expected tree extensions, ADR topics, CI expectations, and prompt hints. Bootstrap copies the profiles/ directory but does not:

  • select or apply a stack profile
  • scaffold stack-specific files (pyproject.toml, CMakeLists.txt, conanfile.py, …)
  • install stack tooling (Conan, editable Python installs, preset configure smoke tests)
  • merge stack-specific pre-commit hooks (Ruff, clang-format, …)
  • run stack-specific doctor checks

ADR-0015 closes the stack-agnostic adoption gap (standard .pre-commit-config.yaml, .venv/, pre-commit + gitlint hook registration). It explicitly leaves language and build-stack hooks to profiles.

Project config already exposes workflows.default_profile (initial-contracts.md C3; ProfilePointer in C1), and the TUI can edit it — but bootstrap and setup_dev_environment() do not read it.

LibESys-style portfolios repeat three stacks:

StackTypical tooling
Pythonpyproject.toml, Ruff/mypy, pytest, editable install
C++ / CMake / ConanCMake presets, Conan 2 profiles, ctest
Python + C++ extensionHybrid layout (CMake + Conan native core, Python packaging), SWIG / PySWIG as the LibESys default binding layer; Conan + Python dev env

A declarative-only profile format (for example a root profile.yaml with command lists) would still require Python to merge pre-commit config, branch on OS, detect existing toolchain state, and compose hybrid profiles. Behavior belongs in a2c_core; repository profiles/ should hold human guides and scaffold templates, not executable configuration.

The python-cpp-extension profile scaffolds and doctors the build and layout contract (native library + importable Python module). The choice of binding technology (SWIG, pybind11, Cython, etc.) is a project ADR — not hard-coded in the plugin protocol. Bundled v1 templates default to SWIG + PySWIG because that is the usual LibESys stack; consumers override via profiles/python-cpp-extension/templates/ after adoption.

Decision

1. Stack profiles are Python plugins in a2c_core

Introduce a stack profile plugin protocol implemented as Python classes under:

src/a2c_core/profiles/stacks/
├── __init__.py # public registry API
├── base.py # StackProfile protocol / ABC
├── registry.py # id → plugin class, load by id
├── python.py # bundled profile (v1)
├── cpp_cmake_conan.py # bundled profile (v1)
└── python_cpp_extension.py # bundled profile (v1; composes python + cpp)

Bundled plugins ship with a2c_core so pip install a2c-core and consumer bootstrap behave consistently without requiring the full method repository checkout.

The protocol is also designed for optional third-party profile packages installed separately (see §3). v1 ships only bundled profiles; the registry must support both sources from the first implementation.

Each plugin must expose:

Attribute / methodPurpose
id: strStable kebab-case identifier (for example python, cpp-cmake-conan, python-cpp-extension)
label: strHuman-readable name for CLI/TUI
scaffold(ctx)Write stack template files and directories under the target repo (skip existing paths — same semantics as bootstrap _write_new)
extend_pre_commit(base_yaml: str) -> strReturn merged .pre-commit-config.yaml text; must preserve ADR-0015 baseline hooks
setup(ctx) -> DevSetupResult-compatible outcomeStack-specific install/configure steps after base dev setup
doctor(ctx) -> list[ValidationIssue]Optional toolchain and layout checks
compatible_a2c_version: str | NoneExternal packages only — exact A2C semver this plugin targets; None for bundled plugins (implicit lockstep)

ctx is a shared context object (repository root, resolved Python for .venv/, bootstrap delivery mode, method-assets root) defined in a2c_core.profiles.stacks.base — not re-specified here.

Composition (hybrid stacks) is implemented in Python (delegate to other plugin instances in a defined order), not via YAML extends.

There is no profile.yaml execution schema. Optional small metadata files under profiles/<id>/ (README only, or future non-executable manifests) do not drive setup logic.

2. Repository profiles/ holds assets and documentation

The method-repository tree profiles/<stack-id>/ remains the consumer-visible profile folder (copied or mounted by bootstrap). Each stack profile directory contains:

ContentRole
README.mdAdoption guide, ADR topics, CI expectations (as today)
templates/Stub files the plugin copies during scaffold() (for example pyproject.toml.stub, CMakeLists.txt.stub)

Plugins resolve template paths from:

  1. Consumer repoprofiles/<id>/templates/ when present (allows local customization after adoption)
  2. Method assets — bundled under a2c-workflow/profiles/<id>/templates/ via method_assets_root()

Task-body profiles (profiles/task-body/) are unchanged — YAML data, separate loader, separate concern.

3. Registry and validation

a2c_core.profiles.stacks.registry provides:

  • list_stack_profiles() -> list[StackProfileInfo] — id, label, origin (bundled | package)
  • get_stack_profile(profile_id: str) -> StackProfile — raises a structured error (A2C_PROFILE_001 or equivalent) when unknown
  • resolve_stack_profile(config: A2CConfig) -> StackProfile | None — uses workflows.default_profile

Two registration sources (planned from v1)

SourceHow it registersExamples
BundledExplicit registration in a2c_core.profiles.stacks at import timepython, cpp-cmake-conan, python-cpp-extension
Installed packagesSetuptools entry points group a2c.stack_profiles — each entry names a StackProfile subclass or factoryFuture org-specific packages (for example a2c-profile-libesys-firmware)

Registry load order:

  1. Register bundled plugins (built into a2c_core).
  2. Discover and register entry points from the active Python environment (importlib.metadata.entry_points).
  3. Duplicate id across sources → hard error at registry init (fail fast; no silent override).

External profile packages:

  • Must declare a dependency on a2c-core (same major product line as the installed CLI).
  • Must implement the same StackProfile protocol as bundled plugins.
  • Must declare the A2C product version they were built for (see A2C version lock below).
  • Should ship default templates/ as package data inside the pip distribution; scaffold() resolves templates from the plugin package when consumer-repo profiles/<id>/templates/ is absent.
  • May ship a companion profiles/<id>/README.md in package data or document install separately; method-repo profiles/ copy remains optional for consumers who want docs in-repo.

A2C version lock (external packages only)

Operators commonly maintain many repositories on different A2C versions (ADR-0013). External profile wheels therefore carry two versions:

VersionMeaning
Profile package versionSemver of the profile distribution itself (for example a2c-profile-libesys-firmware 1.2.0) — independent release cadence
compatible_a2c_versionExact A2C product semver the profile was implemented and tested against (for example 0.6.3)

Each external plugin must expose compatible_a2c_version: str on the profile class (exact X.Y.Z, same form as a2c_core.__version__). Packaging should mirror it in pyproject metadata (for example [tool.a2c-profile] compatible-a2c = "0.6.3") for inspection without importing the plugin.

Compatibility rule: at registry load and on every get_stack_profile() / bootstrap / setup-dev / doctor use, compare compatible_a2c_version to the running A2C version (a2c_core.__version__ of the invoked CLI). Comparison is exact equality on normalized semver — not a floating range in v1.

SituationBehavior
compatible_a2c_version == running A2CProfile is usable — normal scaffold/setup/doctor
compatible_a2c_version != running A2CProfile may remain pip-installed and discoverable, but is not usable
a2c bootstrap --profile &lt;id&gt; with incompatible packageHard error (A2C_PROFILE_002 or equivalent) — profile steps do not run; message names running A2C, profile package version, and required compatible_a2c_version
workflows.default_profile points at incompatible packagea2c doctor reports error; setup-dev profile phase errors; other commands that invoke the profile error when they would call setup() / scaffold() / doctor()
list_stack_profiles()Includes incompatible packages with compatible: false and reason — so pip list + a2c help explain why a installed profile is inactive

Bundled profiles ship in lockstep with a2c_core — they inherit the running product version and skip the external version lock (always compatible with the installed CLI build).

Rationale: a profile compiled against A2C 0.6.x protocol and templates must not silently run under 0.7.x (or 0.5.x) where scaffold hooks, pre-commit merger, or adoption semantics may differ. Install-many-versions-on-one-machine is supported; use is gated per invocation.

v1 implementation includes entry-point discovery even when no third-party profile wheels are published yet — avoids a registry rewrite when the first external package ships.

Profile ids from either source are valid for workflows.default_profile and a2c bootstrap --profile.

Out of scope (still): loading profile classes from arbitrary filesystem paths in the consumer repo without a pip-installed package — that remains a future ADR if ever needed.

4. Bootstrap integration

a2c bootstrap gains an optional --profile &lt;id&gt; flag:

  1. Run existing stack-agnostic bootstrap (ADR-0015): config files, method dirs, base .pre-commit-config.yaml, setup_dev_environment().
  2. When --profile is set (or workflows.default_profile already in config on re-bootstrap — profile flag wins):
    • scaffold() — stack templates and directories
    • extend_pre_commit() — rewrite .pre-commit-config.yaml if the profile adds hooks (only when bootstrap wrote or would write that file)
    • setup() — stack toolchain steps
  3. Set workflows.default_profile in .a2c/config.yaml to the selected id.

When --profile is omitted, behavior stays identical to ADR-0015 (no stack scaffold, no stack setup).

Bootstrap records the selected profile in adoption metadata where appropriate (human-readable note in scaffold output; optional field in .a2c/adoption.yaml in implementation).

5. Pre-commit merge rules

Stack plugins append repo-local and hook-repo entries; they must not remove or reorder mandatory blocks from ADR-0015:

  • a2c-pre-commit-hooks (check-changelog, check-web-sources)
  • baseline pre-commit-hooks hygiene
  • tab removal for Markdown/YAML
  • gitlint on commit-msg

Implementation uses a shared merger in a2c_core (for example services/pre_commit_merge.py) — plugins return hook fragments or full merged YAML through the protocol, not ad hoc string concat in each plugin.

Pin policy for stack hooks (Ruff rev, clang-format rev) is owned by each bundled plugin and updated with a2c_core releases.

6. Setup and failure semantics

Stack setup() runs after base setup_dev_environment() so .venv/ and pre-commit registration exist before Python-stack steps.

Failure semantics match ADR-0015:

  • Missing host tools (Conan, CMake, compiler) → warning ValidationIssue, bootstrap continues
  • Hard misconfiguration (invalid merged pre-commit YAML) → error, bootstrap fails

Stack setup must not install OS-level toolchains (MSVC, Xcode, system GCC). Plugins may run detect/configure commands (conan profile detect, pip install -e ".[dev]", cmake --preset … smoke tests) and document prerequisites in profile README. For hybrid profiles, swig on PATH is a doctor warning when missing — same non-blocking semantics as Conan/CMake.

scripts/setup-dev.sh / .ps1 invoke base dev setup, then setup() for workflows.default_profile when set.

7. Doctor integration

a2c doctor (and TUI diagnostics that delegate to it) runs stack doctor() when workflows.default_profile is set, after existing repository checks.

Doctor checks are read-only (version probes, expected paths, preset names documented in profile README) — no mutating install.

8. Initial bundled profiles (v1 scope)

Implementation follows this ADR in separate commits. Bundled v1 ids:

Profile idIntent
pythonpyproject.toml stub, Ruff pre-commit hook overlay, editable dev install, pytest-oriented doctor
cpp-cmake-conanCMake/Conan stubs, Conan detect, preset/layout doctor
python-cpp-extensionComposes python + cpp-cmake-conan scaffold/setup/doctor with hybrid ordering; default templates use SWIG / PySWIG (.i interface files, generated wrapper sources, extension module layout). Binding choice remains documented in a project adr(build) — templates are swappable, not protocol-fixed

Additional profiles (monorepo, cross-compile firmware, etc.) add new bundled plugin modules or separate a2c.stack_profiles pip packages — no protocol change.

Binding-specific variants (for example python-cpp-extension with pybind11-oriented stubs) may ship as optional template packs under profiles/python-cpp-extension/templates/ without new profile ids, unless layout or setup order genuinely differs.

9. Explicit non-goals (v1)

  • Publishing third-party profile packages to PyPI/GitLab (protocol and discovery only; first external package can follow in a later release)
  • Consumer-defined plugins loaded from arbitrary repo paths without pip install (future ADR if needed)
  • profile.yaml as an executable DSL for setup commands
  • Replacing project build ADRs — profiles recommend defaults; toolchain pins and layout authority stay in project ADRs
  • CI job generation as fully automated GitLab YAML emit — v1 documents CI expectations in profile README; optional template fragments may follow later

Consequences

Positive

  • One a2c bootstrap --profile … path for Python, native, and hybrid LibESys repos
  • Stack behavior is testable Python (unit tests per plugin, registry, pre-commit merger) — same quality bar as dev_setup.py
  • Extensible registry — LibESys-common stacks ship with a2c-core; specialized stacks can ship as add-on pip packages without forking core
  • Safe multi-repo versioning — external profiles pin to an A2C release; mismatch is visible and blocks use without blocking pip install
  • Clear split: a2c_core executes; profiles/ documents and supplies templates consumers can customize
  • ADR-0015 baseline stays universal; stack hooks land via a defined extension point
  • workflows.default_profile becomes meaningful for bootstrap, setup-dev re-runs, and doctor

Negative

  • More code in a2c_core — bundled plugins must be maintained and versioned with releases
  • Each A2C bump may require republishing external profile packages with an updated compatible_a2c_version — intentional friction to avoid cross-version drift
  • Hybrid and native profiles cannot fully automate environment parity across Windows/Linux/macOS without operator/CI involvement
  • Repositories adopted before this ADR need a2c bootstrap --profile … (config refresh) or manual profile scaffold + scripts/setup-dev
  • Pre-commit merge logic must be kept correct when ADR-0015 baseline changes (regression tests required)

References

Supersedes

  • (none)

Superseded by

  • (none)