Skip to content

Architecture decision records

Decisions that shape how the platform works, with the context and the alternatives that were rejected. Every ADR has a status line; superseded decisions keep their file and say which ADR replaced them.

Adding one: copy the structure of a recent ADR (context, decision, consequences, what is not included), use the next free number, and list it in the table below (scripts/shapes.py check fails when an ADR file is missing here).

ADR Decision
ADR-001: Two-channel distribution — Claude Code marketplace + Copier templates Two-channel distribution — Claude Code marketplace + Copier templates
ADR-002: Web testing stack — Vitest + Playwright opt-in Web testing stack — Vitest + Playwright opt-in
ADR-003: Web package manager — pnpm Web package manager — pnpm
ADR-004: Next.js router — App Router only Next.js router — App Router only
ADR-005: Node version policy — pin current LTS, bump via copier update Node version policy — pin current LTS, bump via copier update
ADR-006: gitops-app overlay structure — env-only gitops-app overlay structure — env-only
ADR-007: Cross-repo orchestration — plan-only in v1, execution in v2 Cross-repo orchestration — plan-only in v1, execution in v2
ADR-008: Shape detection — .copier-answers.yml with sniffing fallback Shape detection — .copier-answers.yml with sniffing fallback
ADR-009: service-java stack defaults service-java stack defaults
ADR-010: service-go stack defaults service-go stack defaults
ADR-011: Multi-repo plan format for /app:build-feature Multi-repo plan format for /app:build-feature
ADR-012: Per-plugin release agent Per-plugin release agent
ADR-013: Plugin auto-enable on /shared:new-service Plugin auto-enable on /shared:new-service
ADR-014: gitops-app composition spec gitops-app composition spec
ADR-015: Shape registry as code (shapes.yml) Shape registry as code (shapes.yml)
ADR-016: library-python distribution — git+https via uv library-python distribution — git+https via uv
ADR-017: The GitOps repo owns every Kubernetes manifest; services ship an image The GitOps repo owns every Kubernetes manifest; services ship an image
ADR-018: Secrets come from External Secrets Operator; git holds references, never values Secrets come from External Secrets Operator; git holds references, never values (generated values: a credentials Job instead, ADR-072)
ADR-019: Supply-chain hardening for generated repos Supply-chain hardening for generated repos
ADR-020: Addons: a stable contract for backing services Addons: a stable contract for backing services
ADR-021: Observability: an OpenTelemetry base, a UI stack as an option Observability: an OpenTelemetry base, a UI stack as an option
ADR-022: Kyverno guard rails, checked before merge and enforced in the cluster Kyverno guard rails, checked before merge and enforced in the cluster
ADR-023: Parallel execution of multi-repo plans Multi-repo plans run in parallel waves; merging and pinning stay human
ADR-024: Spec-driven bootstrap Spec-driven bootstrap: new repos start with plan-feature, build-feature consumes the plan (superseded in part by 026)
ADR-025: Issue triage Issue triage: /shared:triage classifies, discusses and routes issues; labels are the state
ADR-026: Feature specs drive the build Feature specs drive the build: spec folders, AC ids, tests first, cplat spec
ADR-027: dev follows main through pin PRs dev follows main: service CI asks the gitops-app for an auto-merging pin PR to sha-<7>
ADR-028: Unattended delivery without branch protection Unattended delivery without branch protection: merges wait for green CI in code, /svc:ship, a Stop hook, headless resumes
ADR-029: External services in services.yaml External services in services.yaml: hand-written kustomize packages or Helm charts, wired and pinned by render
ADR-030 — Image tags are the full commit sha Image tags are the full commit sha (sha-<40>); sha-<7> is accepted for one release
ADR-031 — Repo-owned companions for skeleton files Skeleton files stay platform-owned; repo additions go in repo-owned companions (ci-extra, own permissions, own workflows, project notes) that updates never touch
ADR-032 — Every run is recorded in a private run store Every run is recorded in a private run store: one file per run, one commit per flush, buffered locally when unreachable
ADR-033 — Failures become auto-filed issues, deduplicated by a signature line Failures become auto-filed issues, deduplicated by one Signature: <kind>:<16 hex> body line; one module (autoissue) for platform failures, smoke failures and alerts
ADR-034 — Fixable dependency findings are patched by the run Fixable dependency findings are patched by the run: deps recipes per shape, cplat deps patch, 2 rounds, exact revert
ADR-035 — Token usage is measured from the local session transcripts Token usage is measured from the local session transcripts: sessions claimed via CLAUDE_CODE_SESSION_ID, shared calls split evenly, costs estimated from a price table in the repo
ADR-036 — The coordinator runs on Sonnet; project and user settings choose otherwise The coordinator of a headless run uses Sonnet by default; a flag, a project setting (optionally per workflow) or a user setting choose otherwise
ADR-037 — cplat commands that bundle steps never widen the session’s permissions cplat commands that bundle several steps never widen the session’s permissions: each part runs only if the repo allowlist allows it
ADR-038 — Alerts reach GitHub through a scheduled poll on the gitops-app’s self-hosted runner Alerts reach GitHub through a scheduled cplat alerts poll on the gitops-app’s self-hosted runner; rules in app.yaml, lifecycle state in the issues, a dedicated ALERTS_TOKEN for service repos
ADR-039 — The weekly retro: a deterministic report in code, agent proposals checked against it The weekly retro: a deterministic report in code, agent proposals posted only when their evidence is in the report; a public issue with counts, the detail in the private run store
ADR-040 — Smoke tests run against a deployed environment through one recipe and one output contract Smoke tests run against a deployed environment: a (smoke) criterion marker, devbox run smoke with SMOKE_BASE_URL and one output contract per shape, cplat smoke reads it; red smoke in dev escalates as code, not-run never counts as passed
ADR-041 — Staging follows a green dev smoke; prod stays a manual promotion with evidence Staging follows a green dev smoke through a self-merging pin-staging PR (app-wide setting, per-service override); prod stays a manual /gitops:promote with an evidence summary
ADR-042 — A product ship coordinates one repo ship per component, gated per repo, with serialized gitops merges A product ship (/app:ship) coordinates one unchanged cplat ship per component repo: per-repo gates (smoke, Argo, merged PR) before dependents start, gitops merges serialized in plan order, one product-ship record
ADR-043 — One budget per run tree, kept in a local ledger and checked at safe points One budget per run tree (minutes, agent invocations, CI reruns, estimated cost) in a local ledger keyed by the work, checked at safe points; a stop escalates with cause decision and stays resumable
ADR-044 — Change-aware validation in the inner loop only, decided by rules in code Change-aware validation only in the inner loop, decided by deterministic rules (mandatory categories force everything, unmapped runs everything, risk only adds); the final gate and PR CI stay full
ADR-051 — Run state is per worktree Run state (ship, app-ship, run pointer, recovery snapshot, pending replans) lives in the worktree’s own git dir, so worktrees never block or attach to each other; the gitops merge slot and the evidence cache stay in the common dir
ADR-052 — Shared renders and round-robin shards for the platform test suite Tests that only need a rendered repo share one render per key and process and get private copies; the cplat suite runs in 4 round-robin shards whose coverage a CI job checks; render-subject tests still render
ADR-053: A domain model based on Backstage’s system model, with environments One domain model for the platform: Backstage’s system model (System, Component, API, Resource, Owner) plus Environment, Instance and Resource instance, and the delivery nouns; an internal model in cplat first (4.x), then system.yaml and noun-based commands in 5.0; cluster names never change
ADR-054: The 5.0 interface: system.yaml checked by the renderer, noun plugins with tombstones, and a migration that proves itself The 5.0 interface: system.yaml checked by the renderer, noun plugins with tombstones for every 4.7 command, and a migration that proves the render is unchanged before it merges
ADR-055: The local console is a stdlib web server over cplat JSON; it acts only through Claude Code The local console (cplat ui) is a Python stdlib server on 127.0.0.1 with server-rendered HTML and one dependency-free script; it reads only cplat JSON (subprocesses, snapshots of main), acts only through slash commands run by Claude Code, and delivers notifications locally
ADR-056: The platform repo declares its own shape: a template-less registry entry, set only in the committed settings file The platform repo declares shape: platform in its committed .platform-settings.yml: a template-less, declared-only registry entry that shapes.load() hides from every create, adopt, update and fleet path; never sniffed, no release role
ADR-057: Resources are named and typed; each type has a fixed contract; any resource instance may be external Resources are named and typed (database, bucket, cache, broker); each type has a fixed Secret-key and variable contract, prefixed <NAME>_ when a component uses two of one type; Secret <system>-<name>-app and Argo application addon-<name>-<env> for every resource; any resource instance may be external, synced from the secret store
ADR-058: Images and operators are pinned exactly per release; the platform installs the operators a cluster needs as Argo CD applications Images and operator charts are pinned exactly in the renderer’s versions table per release; the renderer writes the operators a cluster needs as Argo CD applications under operators/ (once per cluster, only when needed, no finalizer); environments.<env>.operators: owner opts out and doctor compares versions (shared clusters: amended by 060)
ADR-059: An in-cluster bucket is its own SeaweedFS, run by the platform-installed SeaweedFS operator; its keys are generated in the cluster and Argo CD never deletes it An in-cluster bucket is its own SeaweedFS (one Seaweed per bucket instance, a storage-sized volume, the filer’s S3 endpoint), run by the SeaweedFS operator that the platform installs under operators/; keys are written once by a credentials Job (ADR-072) and adopted by the operator; Argo CD never deletes a bucket (Delete=false, Retain); versioning: false renders Suspended
ADR-060: A cluster repo owns what a cluster shares and lists the systems it runs A cluster repo (template cluster-gitops, plugin cluster) owns what a cluster shares: the operators it lists (pinned from the renderer’s versions table, linked, never copied), External Secrets with the ClusterSecretStore, the Kyverno engine, the org’s Argo CD repo credential, and one root application per registered system; installing a system is a PR, a check finds missing operators, takeover keeps ADR-058’s application names
ADR-061: Databases are backed up by the CloudNativePG Barman Cloud plugin to a bucket resource; a point-in-time restore is a new generation next to the running one Databases are backed up by the CloudNativePG Barman Cloud plugin (with cert-manager, both platform operators) to a bucket resource; an external target bucket declares remote.s3 in git; a point-in-time restore is a new generation <system>-<name>-g<n> next to the live one, recorded in restore-state.yaml; a switch detaches the connection Secret <system>-<name>-app and repoints it
ADR-062: An environment is backed up by Velero, installed once per cluster; persistent volumes by file-system backup; databases, caches and the target bucket are left out An environment is backed up by Velero (one per cluster, a platform operator with the resources finalizer): per system and environment a location, a schedule and synced credentials in velero; file-system backup of persistent volumes, emptyDir skipped; databases, caches and the target bucket left out by kind and label; restore adds what is missing, --replace pauses and replaces
ADR-063: Autonomy is declared in system.yaml; an engine is bound by a contract and only signals can act before a merge Autonomy is declared per environment in system.yaml (levels, guardrails, pause) and rendered to autonomy.json; an engine such as ai-ops is bound by a contract (issue format, fix run record, audit event, HTTP signals) and only signals (pause, resume, lower) act before a merge
ADR-064: A cache is a platform-rendered single-node Valkey StatefulSet, without an operator; its password is generated once in the cluster A cache is one Valkey StatefulSet the renderer writes itself (official image pinned per major, ClusterIP on 6379, AOF volume only with persistence), with no operator, CRD or chart; maxmemory is 80 % of the container’s memory limit; the password is written once by a credentials Job (ADR-072) into <system>-<name>-app (url, host, port, password); every cache object carries the name label that keeps it out of backups
ADR-065: A broker is NATS JetStream from the official chart, with declared streams and consumers and one user per component A broker is NATS with JetStream from the official chart (a nested Argo CD application per instance, chart and image pinned); streams <system>-<api> and durable consumers named after the component are NACK objects (a platform operator) that renders and syncs never delete; each component has its own user, written once by a credentials Job (ADR-072), <system>-<name>-<component>-app with subject rights and its own inbox prefix; components reach a broker through the events APIs it carries
ADR-066: Instances scale with a plain HPA or with KEDA, and the platform installs KEDA per cluster when an instance needs it scale is an Instance property (component default, instance override, exclusive with replicas); CPU and memory scale with a plain HPA, queue depth (a broker’s durable consumer, a cache list) with KEDA, which the platform installs per cluster as a pinned operator exactly when an environment derives autoscaling: keda; the resource-metrics source is a declared answer of the gitops repo; timings are platform constants
ADR-067: Argo CD leaves the replica count of an autoscaled Deployment alone A scaled Deployment keeps spec.replicas: <min>; the environment’s component ApplicationSet ignores /spec/replicas of each scaled Deployment by name and syncs with RespectIgnoreDifferences=true; autoscaler objects are overlay files that prune when scale goes; KEDA objects sync in wave 1 with retry; no sync option recreates objects
ADR-068: Resource and operator upgrades roll through the environments in verified steps A resource major moves one environment per pull request with /resource:upgrade (dev, then staging once dev is verified, then prod by hand); a database step needs a backup from the last 60 minutes and upgrades in place with its own archive per major; a step counts only when its verification is recorded; platform updates move dev’s pins and leave staging and prod on held pins; operators move once per cluster with dev’s step
ADR-069: One versions table pins everything, and every release proves the upgrade from the previous one Every image and chart comes from one versions table with exact versions and digests, guarded by a pin check; releases list and classify changed rows; each release candidate passes an upgrade end-to-end test from the previous release before it is tagged; External Secrets, Kyverno and LGTM are rendered only for repos that answer property_operators: install
ADR-070: The Stop hook holds only the run’s owning session, and only while it makes progress The Stop hook holds only the session that started or resumed the run (its owner); a run without an owner holds nobody; two owner stops without a new commit or phase change escalate as no progress with a cause chosen by a fixed rule; one module decides for both gates and cplat headless
ADR-071: A failing check reaches an agent as a summary made by fixed rules, the full log stays on disk A failing check reaches an agent as a summary made by fixed rules (per-tool parsers, own-code paths, dedup with counts, fixed limits, redaction); the full log stays on disk with a .summary.json sidecar and agents open it only when the summary is not enough; failure_summary: off or cplat quiet --raw restores the plain tail; spec 102 signatures use the summary
ADR-072: A generated credential is written exactly once, by a create-only Job that cannot write it twice A generated credential is written exactly once by a create-only credentials Job (PreSync hook per owner; create if absent, add-missing-key patch under a resourceVersion test; Role with create and named get/patch, no update), because ESO CreatedOnce can write a generator-backed Secret twice (#316, ESO 2.12.0); an exception to ADR-018 for generated values; amends ADR-059, 064, 065
ADR-073: The platform release is one command that reads its progress from git and GitHub The platform release is one command (cplat platform release, /platform:release) that keeps no state file and reads its progress from git and GitHub (release branch and PR, squash commit, tag, GitHub release, release-guard run); it stops for the maintainer before the two irreversible steps (`CONFIRM: merge
ADR-074: A gate that checked nothing fails Every covered gate (test, test-fast, validate, render-check, secrets-scan) counts what the tool itself reports and fails on 0 with one line <gate>: checked nothing: <what was empty> - <how to fix>; shell recipes use one byte-identical scripts/gate.sh; the check lives in platform-owned files so /platform:update delivers it; no opt-out; tests/cplat/test_130_empty_gates.py fails for a template or gate without an entry
ADR-075: An unattended ship or build runs as one session per unit, each saved at a checkpoint An unattended /component:ship or /component:build under cplat headless runs as one fresh session per unit (plan, red tests, each task in dependency order, verification, land), one after the other; a checkpoint is detected from the committed feature branch and recorded per worktree in cplat-units.json; the Stop hook ends a unit session at its checkpoint and never escalates it; two sessions of a unit without a commit escalate; 5.5 runs get derived units
ADR-076: An unattended run gets its git and gh permissions per session from a checked run allowlist, never from a settings file An unattended run gets exactly its git and gh actions (feature branch, signed-off commit, push without force, gh pr create, merge only through the land step) per session through --allowedTools, with force pushes, default-branch pushes and other merges in --disallowedTools; a preflight checks every settings layer, the git identity, the gh login and push access before anything is spent; no settings file is ever written; cplat bundles read the grant from CPLAT_RUN_ALLOWLIST
ADR-077: Run budgets scale with the plan, and verification gets one grace Default budgets follow the plan (cost max(15, 10 + 1.5·T + 0.5·C), minutes max(180, 60 + 15·T + 5·C), invocations max(60, 20 + 5·T + C), reruns 6), one ledger part per spec, raised and never lowered when a plan appears; a run with every task done gets one grace of 20 % (at least $3, 30 min, 10 invocations) for verification only; an incomplete transcript read is an unknown cost, never a partial sum
ADR-078: Platform changelog entries are fragments the release assembles A platform changelog entry is one file docs/changelog.d/<id>-<slug>.<added|changed|fixed>.md; cplat platform release assembles them with the [Unreleased] text under the new heading (Added, Changed, Fixed) and deletes them in the release commit; direct [Unreleased] edits keep working; release-check and the version proposal read fragments; tests find a released entry by its version heading and pin plugin versions with >=
ADR-079: A PR shows how it was built, generated from the run records cplat (not the orchestrator) writes a collapsed “How this was built” block into build PRs from the run’s local records (cplat platform runs report --pr <run-id>, appended by cplat pr open): user decisions, hand-backs and fix rounds, accepted deviations, how it ran; only recorded facts, redacted and capped, never blocking the PR; facts no record holds get their own recording call first
ADR-080: A pull request runs only the e2e it needs, in parallel parts On a pull request the cluster e2e runs only when scripts/e2e-scope.py finds a change it exercises (gitops/cluster templates, Dockerfiles, e2e files, the cplat modules the e2e scripts call or import, closed over every import, computed at HEAD) or a release heading; it runs as three parallel parts (E2E_PART core, scaling, backups); nightly runs all in one job; the upgrade e2e decides on the pull request’s own diff