ADR-0004: Single monorepo over poly-repo for the OpenRAL open-core
- Status: Accepted
- Date: 2026-05-24 (retroactive — documents a Week-1 decision already in code)
- Amended: 2026-05-24 (see Amendments below)
Context
OpenRAL's open-core today spans ten Python workspace members
(openral-core, openral-cli, openral-hal, openral-sensors,
openral-world-state, openral-rskill, openral-runner, openral-sim,
openral-detect, openral-observability) plus five ROS 2 packages
(openral_msgs, openral_world_state_ros, openral_hal_so100/franka/ur5e/ur10e),
plus a tools/ tree, an examples/ tree, an rskills/ catalogue, a
robots/ catalogue, a benchmarks/ catalogue, and the docs site. The
contracts in openral_core are normative and consumed by every other
package; the ROS IDL in openral_msgs is normative and consumed by every
ROS node.
A change that touches a schema typically touches at least three trees in
the same commit: the schema (openral_core), a real test fixture
(robots/ or rskills/), and the consumer (e.g., openral-rskill or
openral-runner). While we are pre-publish the on-disk schemas sit at
schema_version: "0.1" and the surface evolves in place without
migrators (CLAUDE.md §1.6). CLAUDE.md §1.14 elevates the cross-tree
workflow to a rule — "Docs travel with the code" — and §1.13 makes
docs/METHODS.md update mandatory in the same PR. Cross-package atomic
commits are the default workflow, not the exception.
External, deliberately-separated repos exist and are documented in CLAUDE.md §2:
huggingface.co/openral/skill-*— skill weights & manifests (one HF Hub repo per published rSkill; license-gated).huggingface.co/openral/dataset-*— LeRobotDatasets.openral/cloud— hosted observability / fleet control plane (separate repo; different release cadence and audience).openral/contrib-closed-shims— adapters for closed third-party vendor SDKs that cannot be redistributed (the SDK is closed, not OpenRAL).openral/awesome-ros— community curation.
These are split precisely because they have different licensing, different release cadence, or different audiences from the open core — not because the open core wanted finer granularity.
Decision
The open core lives in one monorepo (openral/openral) with three
build systems coexisting at the root:
- uv workspace for Python —
pyproject.toml:13-14declaresmembers = ["python/*"]. New Python packages land by creating a directory and listing it in[tool.uv.sources]. One lockfile (uv.lock) at the root. - colcon for ROS 2 —
packages/<name>/directories built withament_cmake/ament_python. Oneinstall/after a colcon build. justas the canonical task runner — every workflow that touches more than one package has ajustrecipe so contributors do not have to know which build system owns a given file.
Concrete rules:
- Atomic schema evolution. A schema change touches
openral_core, the real fixture underrobots//rskills//scenes/, and the consumer in the same commit. The on-diskschema_versionstays at"0.1"while we are pre-publish (CLAUDE.md §1.6). - Single CI surface.
.github/workflows/exercises every workspace member from one place — there is no cross-repo CI to coordinate. - Single ADR catalogue.
docs/adr/is the canonical record for the whole open core. ADRs that affect a deliberately-separated repo (e.g.,openral/cloud) cross-reference but live here. - Single CHANGELOG generator —
release-pleaseconsumes Conventional Commits across the workspace and produces one release. - The whole monorepo is Apache-2.0. Every package — including the
reasoner, wam, dispatcher, skill_catalog, and fleet orchestration
layers when they land — is Apache-2.0 (CLAUDE.md §1.9 + ADR-0012);
there is no commercial, source-available, or BSL tier. Adapters for
closed third-party vendor SDKs live in the separate
openral/contrib-closed-shimsrepo because the upstream SDK is closed, not OpenRAL.
Consequences
- Pros
- Cross-cutting changes (a schema bump, a layer rename, a Justfile recipe rewrite) land atomically. No "PR 4-of-7 stuck across repos" failure mode.
- One CI dashboard, one lockfile, one
just testto verify the whole open core. - New contributors clone one URL and have the entire normative surface in their editor's project root — including ADRs, schemas, METHODS.md, and the repo state map.
-
docs/METHODS.md(CLAUDE.md §1.13) is feasible: a flat, layer-ordered index overpython/,packages/, andtools/. A poly-repo split would either give up the index or maintain it cross-repo. -
Cons
git logonmainis high-volume; readers filter by path (git log -- python/sim/) more than by branch.- Workspace builds can be slow if every package is touched; uv's
incremental resolver +
just testfilters mitigate this. - A contributor only interested in
openral-corestill clones the whole tree. Acceptable price for the atomicity guarantees above. - The whole tree is one license (Apache-2.0), so reviewers need no
per-subtree licensing discipline (ADR-0012).
ral check-licensesurfaces weight posture for installed skills; it is not a source-tree license gate (none is needed).
Alternatives considered
- One repo per workspace member (full poly-repo). Rejected — every schema bump becomes a coordinated multi-PR landing across 10+ repos; the atomicity guarantee disappears.
- Two repos:
openral-pythonandopenral-ros. Rejected — the Python ↔ IDL bridge (packages/msgs/) and the lifecycle nodes that wrap Python services (packages/world_state/) would still need cross-repo PRs. We'd inherit poly-repo pain without poly-repo ownership clarity. - Monorepo with sparse-checkout discipline. Possible at scale but premature — the repo is ~50k LoC today; the ergonomics of a single checkout still win.
- Subtree splits per workspace member (autoupdated mirror repos). Considered for the eventual PyPI publishing story. Deferred — when PyPI trusted publishing is wired (roadmap "Org / publishing" item), per-package wheels publish from the monorepo directly; mirrored read-only repos are an option for visibility but not necessary for distribution.
Why this ADR is retroactive
The monorepo decision is encoded in pyproject.toml:13-14
(members = ["python/*"]), in the colcon layout under packages/,
in the Justfile's recipe set, and in CLAUDE.md §2 (the repo map). This
ADR records the reasoning so future "should we split this out" proposals
have a paper trail to push against (CLAUDE.md §7.9).
References
- CLAUDE.md §2 (repo map), §1.13 (METHODS.md), §1.14 (docs travel), §1.9 / ADR-0012 (licensing).
- Root
pyproject.toml— uv workspace + dependency-group conflicts. packages/— ROS 2 colcon workspace.Justfile— the canonical task runner.- ADR-0012 — uniform Apache-2.0 licensing across the monorepo.