Repository navigation
Releases: jurrebuunk/brainstem
Release list
v2.1.1
The first release published to npm by CI, through npm trusted publishing and with provenance. 2.1.0 was published by hand to create the packages. Nothing changes for users.
- The npm workflow publishes through OIDC only; no npm token is stored.
scripts/npm-publish.mjsis attached to the terminal, so a manual publish can answer npm's 2FA prompt.- The release checklist covers trusted publishing and how a new plugin package gets its first version.
v2.1.0
Brainstem on npm, with plugins as npm packages. The app installs with npm install -g @brainstem-dev/brainstem. Every plugin, the built-ins included, is its own package that can be installed, pinned, updated and removed, and third-party plugins work the same way. See Plugins and ADR-0010.
Upgrading:
- Nothing to change for existing installs. Configs with paths (
./plugins/matrix/index.mjs) keep working, Docker images contain every built-in plugin as before, and no migrations run. - The npm package is
@brainstem-dev/brainstem(the@jurrebuunk/brainstemname was never published). - The app's npm package no longer contains the plugins. After
npm install -g @brainstem-dev/brainstem, runbrainstem initto install them.
Changes:
- Plugins are npm packages:
@brainstem-dev/plugin-<name>for each of the 11 built-ins, each with its own dependencies. IMAP libraries now come only with email-imap, and the agentOS runtime only with agentos-runner. brainstem initcreatesbrainstem.config.mjsand the plugin folder, then installs the default plugins. These are http-health, tls-certificate, github-issues, log-file, static-observations, http-json, log-decisions and matrix.brainstem plugins list|add|remove|updatemanages the plugin folder (--plugins-dir,BRAINSTEM_PLUGINS_DIR, default./brainstem-plugins) with npm. It takes any npm spec, and built-ins can be given by short name (brainstem plugins add email-imap). Installed packages are checked to load as plugins, and rolled back when they don't.- The config names plugins by package (
module: "@brainstem-dev/plugin-matrix"). The loader looks in the plugin folder, next to the working directory, then next to Brainstem, and honoursexports/main. Local files and the built-ins' old paths still work. A missing package's error says how to install it. @brainstem-dev/sdkis the plugin API as its own package with no dependencies: defining and validating plugins,createObservation, contexts and plugin state. Its test kit (@brainstem-dev/sdk/testing:runInput,runDestination,createTestOutput) runs a plugin the way the runtime does.- Publishing: the
npmworkflow publishes all packages from the release tag with provenance, through npm trusted publishing after the first release (scripts/npm-publish.mjs, release checklist). - CI runs a packaging smoke test (
scripts/npm-smoke.mjs). It publishes every package to a throwaway local registry, installs the app globally, runsbrainstem init, removes and adds plugins, polls, and serves the Web UI. - The npm package includes the prebuilt Web UI (
brainstem --role web). - email-imap:
mailparser3.14, which fixes thenodemaileradvisories.npm auditreports no vulnerabilities in runtime dependencies.
v2.0.0
Brainstem 2.0: a scale-out architecture that keeps the single-node setup first-class. It covers one database for all state, a durable decide/deliver pipeline, multi-core operation on Postgres, and the Helm chart, metrics and tracing. It also adds hardening (roadmap Phase 5): chaos and load tests, fixes they found, and the upgrade guide, ops runbook and plugin migration guide.
Coming from 1.x? Read Upgrading from 1.x to 2.0. The release candidates below list every change since 1.4.0.
Upgrading from 2.0.0-rc.1:
- No migrations.
runtime.cores.heartbeatMsdefaults to 5 s (was 10 s), so a core that disappears counts as lost after 15 s instead of 30 s.
Changes since 2.0.0-rc.1:
- Chaos tests (
npm run test:chaos, in CI) run real core processes on Postgres, each behind a TCP proxy. The scenarios kill a core mid-poll and mid-delivery, freeze one past its lease (a GC pause), fail over Postgres for every core, and partition one core from the database. Every scenario checks that no observation is lost, none is emitted twice (lease fencing), and effects repeat only inside the documented at-least-once window, always under the same idempotency key. - Fix: a Postgres restart or failover crashed every Brainstem process. An idle connection dropped by the server emitted an unhandled pool
error. Processes now stay up and reconnect. - Fix: a network partition could hang queries for many minutes on dead connections. The pool has a 10 s connect timeout, a 30 s client-side query timeout and TCP keepalive (
runtime.database.pool.connectionTimeoutMillis,queryTimeoutMs,idleTimeoutMillis). The LISTEN connection gets keepalive and is pinged every 15 s; when a ping fails it reconnects and resyncs. - Faster failover: the heartbeat drops to 5 s, and runners scan for stalled jobs at least every 5 s instead of every half lease (30 s). A crashed core's jobs and input leases move on within about 20 s instead of up to a minute.
- Load test:
npm run test:load(scripts/load-test.mjs) measures pipeline throughput and latency over core counts and concurrency. Results and sizing advice are in Load testing. - Docs:
- upgrade guide 1.x → 2.0
- operations runbook: guarantees, alerts and what to do, upgrades, capacity
- plugin v2 migration guide
- load testing
- Topology fix: the Web UI shows plugin names for inputs again when cores run different queues. Inputs come from the cores that poll them, destinations from the cores that run them.
- Web UI fix: on the Deliveries page, a dead job's key sits on its own line below the job number.
- New README screenshots: the flow with the core pool, the core overview, deliveries, versioned config, stats, users and roles.
Install
# Single node (Docker)
docker pull ghcr.io/jurrebuunk/brainstem:v2.0.0
# Kubernetes
helm install brainstem oci://ghcr.io/jurrebuunk/charts/brainstem --version 2.0.0 --set database.existingSecret=brainstem-dbUpgrading from 1.x: see docs/upgrade-2.0.md. Images are multi-arch (amd64, arm64) and signed with cosign.
v2.0.0-rc.1
Kubernetes and operations (roadmap Phase 4): a Helm chart for single-node and cluster deployments, Prometheus metrics, OpenTelemetry traces across cores, a Compose file for cluster mode, and signed multi-arch images. See Kubernetes and Observability.
Upgrading:
- No migrations.
GET /metricsis on by default on the API port and the core health port. Keep it off public ingresses, setBRAINSTEM_METRICS_TOKEN, or turn it off withBRAINSTEM_METRICS=false. - Job payloads gain an optional
trace(W3Ctraceparent) when tracing is on. Older cores ignore it.
Changes:
-
Helm chart (
charts/brainstem, published asoci://ghcr.io/<owner>/charts/brainstemon release):- modes:
mode: single(onerole allpod, SQLite on a kept PVC or Postgres) andmode: cluster(api, web and core worker groups fromcores.groups, each running chosen stages); - autoscaling: KEDA
ScaledObjects on due jobs and oldest-job age (PostgreSQL or Prometheus trigger), an HPA for the API and poll-only groups; - database: CloudNativePG
Clusteror an external Postgres; - migrations as a pre-install/pre-upgrade hook (a regular Job with CloudNativePG), config seeded with
config import --if-empty; - the model mounted from the
brainstem-modelsimage (init container or image volume); - availability and security: PDBs, topology spread, preStop + drain, default-deny NetworkPolicies, non-root read-only pods without service account tokens;
- monitoring and access: ServiceMonitor, PrometheusRule alerts (no healthy core, API down, queue backlog, delivery failures, dead jobs, overdue inputs, event-loop lag), a Grafana dashboard ConfigMap, ingress with SSE-friendly defaults;
- safety nets:
values.schema.json, and ahelm testsmoke test.
- modes:
-
Prometheus metrics on
GET /metrics:- per core: polls, poll duration, observations, lease lag, decisions, job duration and failures, deliveries by destination and outcome, in-flight jobs;
- cluster-wide from the API: queue depth, delayed, running and dead jobs, oldest-job age, cores per status and queue, input states and schedule lag, the delivery log;
- process metrics, including event-loop lag.
An in-house registry, no dependencies (ADR-0009).
-
OpenTelemetry tracing:
poll → decide → deliverspans form one trace per envelope across cores, continued throughtraceparentstored in job payloads (handoffs included). Spans are exported as OTLP/HTTP JSON, configured by the standardOTEL_*variables; off without an endpoint. JSON log lines inside a span carrytrace_id/span_id. Destinations getctx.traceparent, andhttp-jsonforwards it as atraceparentheader. -
docker-compose.cluster.yml: Postgres, migrate + config seed, the model volume, API, Web UI and one core per stage.--profile observabilityadds Prometheus and Jaeger. -
CI:
- chart:
helm lint, helm-unittest,ct lint, kubeconform against several Kubernetes versions with CRD schemas, andct installon kind with CloudNativePG and KEDA; - images: multi-arch (amd64, arm64), cosign keyless signatures, SBOM and provenance, and Trivy scans (pull requests fail on fixable critical and high findings).
- chart:
-
brainstem config import --if-emptyseeds the config store only while it is empty. -
BRAINSTEM_SCHEMA_WAIT_MS: core and API processes wait for migrations (or the database) instead of exiting. -
BRAINSTEM_TELEMETRY_URL/BRAINSTEM_TELEMETRY_TOKEN: cores without a config file send telemetry to the API. -
The Dockerfile builds the Web UI once on the build platform, and installs native dependencies per target platform.
-
The runtime image no longer contains npm, npx, corepack or yarn. Brainstem doesn't use them, and they carried known vulnerabilities in their bundled dependencies.
Install
helm install brainstem oci://ghcr.io/jurrebuunk/charts/brainstem --version 2.0.0-rc.1 --set database.existingSecret=brainstem-db
docker pull ghcr.io/jurrebuunk/brainstem:v2.0.0-rc.1Images are multi-arch (amd64, arm64) and signed with cosign; see docs/kubernetes.md for cosign verify.
v2.0.0-beta.1 — multi-core
Multi-core (roadmap Phase 3). Brainstem runs as several processes on one Postgres database: input polling, decisions and deliveries are shared across cores, a lost core loses nothing, and config is versioned data that every core hot-reloads. Single-node mode (--role all, SQLite) works as before. See Cluster mode.
Upgrading (breaking):
- The API no longer writes or executes the config file.
PUT /api/configreturns 409 withconfigSource: file(the default), and the Web UI shows the file read-only. To edit config in the UI, runbrainstem config importand setBRAINSTEM_CONFIG_SOURCE=database. Mount the config file read-only. - Inputs are scheduled by leases in the database when the pipeline uses the database transport (the default). The schedule survives restarts, so a restart no longer polls every input immediately; it polls when the input is next due. Set
runtime.inputs.scheduling: "timer"for the 1.x behavior. --role coreand--role apinever migrate. Runbrainstem migrateonce per upgrade, or setBRAINSTEM_MIGRATE=true.--role all(the default) andbrainstem-api.mjsstill migrate on start.GET /api/pipelinereportstransport: "postgres"on Postgres.- Migration
0005_clusteraddsinputs,config_versions,active_configandcores.config_version.
Changes:
- Roles and queues:
brainstem --role all|core|api|weband--queues poll,decide,deliver(BRAINSTEM_ROLE,BRAINSTEM_QUEUES). Onlydecidecores load the Laya model. Cores register their role, queues and config version. - Postgres cluster driver:
LISTEN/NOTIFYwakeups for jobs, inputs, config and telemetry (src/db/notifier.mjs); claims keepFOR UPDATE SKIP LOCKED.runtime.queue.transport: "postgres"requires Postgres. - Lease-based input scheduling with fencing epochs: each input is polled by one core at a time, and its checkpoint commit is refused once another core holds the lease. Leases of stopped or lost cores are taken over at once. Inputs get
ctx.lease.GET /api/inputsshows the schedule and lease owners. - Advisory-lock singletons for delivery-log pruning and API maintenance (
src/db/locks.mjs). Newinputs.prunemaintenance task. - Laya worker pool:
runtime.laya.workers/BRAINSTEM_LAYA_WORKERS/--decide-concurrencyevaluates observations in parallel in worker threads, off the event loop.BRAINSTEM_LAYA_MODEL_DIRuses a mounted model. The model is published as thebrainstem-modelsOCI image (Dockerfile.models, CI). - Config as versioned data:
config_versionsvalidated against a JSON Schema (GET /api/config/schema); saves guarded byIf-Match; rollback by activating an older version;brainstem config import|export|list|show|activate|validate. Cores reload the active version without a restart and keep the previous one when a new one fails to load. - SSE across API replicas: telemetry is announced by id and streamed by every replica. Events carry ids, and reconnecting clients resume from
Last-Event-IDon any replica. - Graceful drain on SIGTERM: readiness fails, claiming stops, in-flight polls and jobs finish (
runtime.shutdown.timeoutMs,--drain-timeout), and leases are released. The API ends SSE streams and closes. - Probes on every role:
/livez,/readyzand/startupz(core:--health-port, default 8081; API: its own port). - Web UI: versioned config editor with history and rollback; the Cores panel shows each core's config version.
- Topology merges cores that run different queues and is consistent when running cores share a config version.
- Failed input polls emit
brainstem.input.poll.failedand count as input node errors. - Jobs held by a core that stopped or missed 3 heartbeats are reclaimed at once, like its input leases, instead of after the job lease expires.
- The multi-core acceptance tests pass (no longer
todo) on SQLite and Postgres, including killing a core mid-run.
Brainstem 1.4.0
Small agent tasks and the groundwork for multi-core: a generic agentOS runner for cheap, bounded agent work, destinations that hand results back to the core, a log file input, telemetry and a core registry that identify cores and destinations, and a Web UI flow built on them.
Upgrading:
- Migrations
0003_coresand0004_node_statsrun automatically on start. - The Docker image omits optional dependencies (the agentOS VM runtime). Build with
--build-arg INSTALL_OPTIONAL=trueto useagentos-runnerin Docker. - Handed-off observations no longer pretend to come from an input named
destination:<id>: theirinputis empty andoriginnames the destination. Consumers of telemetry or output envelopes that relied on that input id should readorigin. - Web UI destination nodes show
deliveredinstead ofoutputs. Per-node totals start counting from the upgrade; earlier history isn't backfilled.
Changes:
-
Per-node totals: the API counts polls, observations, decisions, completed deliveries, handoffs, errors and the last event time per flow node in a new
stat_nodestable (migration0004_node_stats) as telemetry arrives, and returns them asnodesinGET /api/stats. The Flow page shows these totals, so node counters no longer reset when events leave the 2000-event window. Destination nodes now showdelivered(completed deliveries) instead ofoutputs, which only ever counted the HTTP JSON destination's output events. -
agentos-runnerfails fast and clearly: a missing model API key is reported before a VM boots, and a sidecar that can't start (EPIPEand similar) becomes a permanent, readable error ("the agentOS sidecar could not start … on NixOS enable programs.nix-ld") instead of retried pipe errors. On NixOS, nix-ld is now the recommended setup; the real-VM test runs whenever nix-ld (orAGENTOS_SIDECAR_BIN) is available. -
Fix:
npm run checknow fails on syntax errors (it usedfind -exec, which ignored them) and also checksbrainstem-api.mjsand the Web UI flow model. -
Web UI Flow page brought up to date with the runtime: structure from
/api/topology(one node per configured id, so tasks of the same plugin are no longer merged), a core pool node and a Cores panel from the core registry, handoffs shown only while they happen (a short-lived "handing off" pill node linked to the destination) instead of fakedestination:<id>input nodes, and an inspector that matches destinations by id and shows each decision's origin and core. Falls back to snapshot events against an older backend. The graph logic moved into a pure, unit-testedweb/src/flow/model.js. -
Core registry and
GET /api/topology: each runtime registers in a newcorestable (migration0003_cores) with its role, queues, version and loaded topology, heartbeats everyruntime.cores.heartbeatMs(default 10 s) and marks itself stopped on shutdown. Cores that miss 3 heartbeats are reported aslost, and rows unseen for 24 h are pruned./api/topologyserves the cores plus the inputs and destinations (with ids) from the freshest running core, and flags running cores that loaded different configs. The Web UI no longer has to derive structure from snapshot events. -
Telemetry identity fields for multi-core and handoffs: every event carries
coreId(runtime.coreId/BRAINSTEM_CORE_ID, also the job worker id); decisions and output envelopes carryorigin({ kind: "input", id }or{ kind: "destination", id, depth, lineage }); the snapshot and destination plugin logs carry destinationids; a newbrainstem.destination.emittedevent marks each handoff. A handed-off observation no longer pretends to come from an input nameddestination:<id>. Itsinputis empty andoriginnames the destination. Jobs queued by the previous version are still understood. -
Destinations can hand observations back to the core with
ctx.emit(observation). They are enqueued in the same transaction that marks the delivery delivered, never routed back to a destination already in their chain, and limited byruntime.handoff.maxDepth(default 2) andmaxPerDelivery(default 5). -
agentos-runnergainshandoff: a successful run's report becomes anagent_reportobservation (withfacts.minimum_decision) that Matrix or a bigger agent can pick up. -
New
log-fileinput: watches a log file and emits new matching lines (defaultERROR|FATAL|CRITICAL|PANIC) as one bounded observation per poll, with offset checkpoints and truncation/rotation handling. -
Fix: the agentOS prompt and docs used
observation.summary, which doesn't exist; they now usetitle,messageandsource.type/source.name. -
New
agentos-runnerdestination: runs a configured task in a Rivet agentOS VM for each matching decision. One plugin serves many tasks; each destination entry has its own filters,command(or agentprompt), network allowlist, read-only mounts, env, and host-side tools. Tools are validated, limited bymaxCalls, can berequired, and are replayed instead of repeated when a delivery is retried.runOnce: "revision"skips re-emitted observations by default. Seedocs/plugins/agentos-runner.md. -
prompttasks run a host-side agent loop against any OpenAI-compatible endpoint (model: { baseUrl, name, apiKeyEnv }). The model getsrun_command(inside the VM) plus the entry's tools. Every run is bounded bylimits(turns, output tokens, commands, tool output size, prompt size, conversation size, request spacing), duplicate calls are refused, and a delivery that already talked to the model fails permanently instead of being retried (maxAgentAttempts: 1). -
@rivet-dev/agentos-coreis an optional dependency. The Docker image omits optional dependencies unless built with--build-arg INSTALL_OPTIONAL=true.
Brainstem 1.3.0
First release on the way to 2.0: one database for all state (SQLite or Postgres) and a durable decide/deliver pipeline. Includes the unreleased 2.0 Phase 0 and Phase 1 work. See "Behaviour changes" below before upgrading.
Brainstem 2.0 Phase 0 (safety net), see docs/roadmap-2.0.md:
- CI workflow running the syntax check, unit/integration tests, and acceptance tests on every PR and push to
main. - Tests for the runtime polling loop, shutdown and abort handling, poll-retry re-delivery (1.x behaviour), per-ID ordering and decision caching in the core, config saves, and the SSE stream.
- Multi-core acceptance harness (
npm run test:acceptance) that runs several real core processes against shared state. The 2.0 cluster guarantees are tracked astodotests until Phase 3. - Architecture decision records for the 2.0 design in
docs/adr/.
Brainstem 2.0 Phase 1 (storage abstraction):
- One database for all state, via Kysely: SQLite (default,
sqlite:data/brainstem.sqlite) or Postgres (BRAINSTEM_DATABASE_URL=postgres://...). The runtime and the embedded API share one connection. - Records, checkpoints, telemetry, stats, and auth stores are async and database-backed. Checkpoints are read from the database on every poll, with no per-process cache.
- New
ctx.stateplugin API for durable plugin state. The Matrix destination keeps its suppression state there, claiming a notification before sending so two processes can't both send it. - Record writes check the expected revision (groundwork for multi-core).
- Safe first-admin creation under concurrent setup requests; login rate limiting stored in the database and keyed by client IP behind trusted proxies (
BRAINSTEM_TRUSTED_PROXIES); retention pruning moved to a periodic maintenance job. brainstem migrate,migrate status,migrate import-v1, andmigrate copy/sqlite-to-pgcommands.- 1.x state (
web-stats.sqlite,checkpoints.json,matrix-notifications.json) is imported automatically on first start; legacy files are never modified. - CI runs every storage test against both SQLite and Postgres 17.
- Behaviour change: with no
records/checkpointsconfig, state now persists to the database (previously records were in memory and checkpoints in JSON). Explicitmemory,json, andsqlitetypes still work.
Brainstem 2.0 Phase 2 (durable pipeline), see docs/delivery-pipeline.md:
- The runtime is split into
poll,decideanddeliverstages connected by jobs in the newjobstable. Each stage commits its outputs and the next stage's jobs in one transaction. Checkpoints advance in the same transaction that stores the polled observations. - One deliver job per (decision, destination). A failing destination is retried on its own with exponential backoff and jitter (
runtime.queue.deliver.retry, orretryper destination). It no longer re-triggers other destinations, the decision or the poll, and a slow destination no longer stalls polling. - Jobs that run out of attempts become dead jobs. The new Deliveries page in the Web UI shows queue depth, the delivery log and dead jobs, with redrive and discard. API:
GET /api/pipeline,GET /api/jobs,GET /api/deliveries,POST /api/jobs/redrive,POST /api/jobs/discard. - Delivery log (
deliveriestable) keyed by an idempotency key, which is checked before sending. Delivered rows are pruned afterruntime.deliveries.pruneAfterDays(14). - Decide jobs are serialized per observation ID through the queue (
queue_key), and record writes are compare-and-set on the revision. - Jobs hold renewable leases. Jobs from crashed processes are reclaimed, and jobs interrupted by shutdown are handed back without using up an attempt.
- Destination plugin SDK v2 (
brainstem.destination/v2):handle(ctx, output)receives the serializablebrainstem.outputenvelope, plusctx.idempotencyKeyandctx.attempt. v1 destinations keep working through an adapter, which logs a deprecation warning, until 3.0. - Built-in destinations are on v2:
http-jsonsends anIdempotency-Keyheader.- Matrix uses the key as
txnId, replacing the random id per attempt. - opencode-runner resumes the session of a retried delivery instead of starting a new one.
- log-decisions is migrated.
- Migration
0002_pipelineadds thejobsanddeliveriestables.migrate copyincludes them. - New telemetry:
brainstem.core.failed.brainstem.destination.failednow carriesattempt,deadandretryInMs. New metrics:decisions.failed,decisions.deadanddestinations.dead. - Behaviour changes:
runtime.destinations.blockInputOnFailureis ignored, with a warning.- A failing decision no longer holds back the input checkpoint. It stays queued and is retried.
--onceruns every due job after polling, and exits non-zero if any poll or job failed.- Config callbacks of the built-in destinations (
notificationKey,titleForEvent,promptTemplate) receiveoutput.payload. - Give destinations an
id: it is part of the idempotency key.
Docker image: ghcr.io/jurrebuunk/brainstem:v1.3.0. Upgrade notes: docs/delivery-pipeline.md#upgrading-from-1x.
Brainstem v0.1.0
Brainstem v0.1.0
First non-alpha release of Brainstem as a local-first headless decision runtime with a protected operations UI.
Highlights
- Local-first headless runtime with SQLite records, checkpoints, telemetry history, stats, and event retention.
- Protected backend API and SSE with first-run administrator onboarding.
- Argon2id password hashing, persistent sessions, CSRF protection, and backend-enforced RBAC.
- Built-in Administrator, Operator, and Viewer roles plus custom role creation/deletion.
- User creation, disabling/enabling, deletion, and last-active-administrator protections.
- Presentation-only Web UI with Flow, Stats, Config, Users, and Roles pages.
- New README and documentation with fresh screenshots.
- Docker/Compose templates updated for
ghcr.io/jurrebuunk/brainstem:v0.1.0.
Validation
npm run checknpm test— 29 passingnpm run plugin:testnpm run web:buildnpm pack --dry-run
v0.1.0-alpha.5
Backend/Web UI decoupling and modularization release.
- Backend API now owns telemetry ingestion, event history, stats, retention, SSE, and config management.
- Web UI is presentation-only and proxies
/api/*to the backend viaWEB_BACKEND_URL. - Added standalone
brainstem-api.mjsandbrainstem-apibinary. - Runtime can start the backend API with
runtime.api.enabledand automatically send telemetry to it. - Added optional backend bearer-token auth via
BRAINSTEM_API_TOKEN/WEB_BACKEND_TOKEN. - Refactored shared telemetry selectors and metric derivation into
src/telemetry. - Further modularized runtime internals, Web server modules, node inspector code, and the opencode runner plugin.
- Fixed node inspector crashes for core and destination nodes.
Container image: ghcr.io/jurrebuunk/brainstem:v0.1.0-alpha.5
v0.1.0-alpha.4
Web UI node inspector and flow readability release.
- Replaced the node click log table with a larger operational node inspector.
- Input nodes now group activity by poll with poll logs and emitted observation envelopes.
- Core nodes now show observation input envelopes alongside decision output envelopes.
- Destination nodes now show received decision/observation envelopes with destination-scoped logs and output envelopes.
- Tightened the inspector into a compact table-like layout with less duplicated metadata.
- Standardized application font-size and line-height CSS tokens for more readable UI sizing.
- Increased flow graph row spacing so stacked nodes no longer overlap.
Container image: ghcr.io/jurrebuunk/brainstem:v0.1.0-alpha.4