ADR-0041: Consolidate the product repos into a single kn-86 monorepo
Context
Section titled “Context”ADR-0039 split the monolith into seven focused repos (kec-lisp, nOSh, kn86-emulator, kn86-ui, kn86-sdk, kn86-carts, kn86-device) with copy-vendoring between them — every consumer holding a copied tree plus a sync.sh recording the source commit. In practice that produced:
- Transitive copy explosion. The Fe kernel
fe.cexists as four live source-of-truth copies (kec-lisp, nOSh/vendor, kn86-ui’s vendored runtime copy, kn86-device/vendor/nosh/vendor);cell_api.c— the file that is the cart-facing contract — exists three times. A one-line kernel fix has to be walked by hand through a dependency-ordered tree ofsync.shscripts, and nothing tells you when two repos are building against different commits. - No contract artifact. ADR-0039 says consumers “depend only on the published contract, never on nOSh’s implementation,” but there is no contract artifact to depend on — so they copy the whole implementation (
kn86-ui/vendor/literally carries a copy of the runtime’srender.c+cell_api.c). The ADR spends a paragraph proving “this is not a cycle”; that proof is the tell that the boundary is misplaced. - A billing constraint driving the architecture. GitHub Free org secrets don’t reach private repos, so each CI that fetched a private dep needed a repo-level PAT — which is partly why vendoring-by-copy was adopted.
- A half-finished extraction. P4 of ADR-0039 never completed:
kn86-emulatoris an empty husk (the real SDL host source is stranded in a worktree).
The benefit ADR-0039 paid for — independent release cadence per repo — does not exist. There is one developer, one shipping target (the device), pre-launch; the runtime, hosts, ui, sdk, and carts always ship together as “the KN-86.” The multi-repo coordination cost was bought with no cadence return.
The disease is not “multi-repo” — it is copy-vendoring as the universal dependency mechanism, plus drawing repo boundaries on concerns instead of release boundaries. The one real release boundary is the language.
Decision
Section titled “Decision”Consolidate to two repos: kec-lisp (unchanged) and a new kn-86 monorepo.
kn-86(new, KEC org) absorbs the six code repos + the docs: nOSh, kn86-emulator, kn86-device, kn86-ui, kn86-sdk, kn86-carts, and kn86-docs.kec-lispstays a separate, public repo — the general language is the one genuine release/visibility boundary.kn-86consumes it via fetch-at-build (CMakeFetchContenton a pinned tag), not a copy. This is the only remaining external dependency, and it is no longer vendored.- Internal layout (the “better layout”):
kn-86/├── runtime/ libnosh — src/ (C floor + DeckRunner tier) · system/ (Fe userland)│ └── include/ THE CONTRACT — NoshAPI + cell-API + render-tier headers├── hosts/ emulator/ (SDL3) · device/ (/dev/fb0 + pi-gen image + rust/ + firmware/)├── ui/ Fe component kit → #includes ../runtime/include directly├── sdk/ .kn86 format + grammar + kn86cart + sdk crate (Rust)├── carts/ launch titles (Lisp), built by sdk├── docs/ the kn86-docs tree (Starlight + ADRs) — its own directory├── CMakeLists.txt top-level: runtime + hosts + ui└── Cargo.toml top-level Rust workspace: sdk + device crates
- All internal vendoring is eliminated.
uistops carrying its vendored copy of the runtime;sdk/cartsstop vendoringui;devicestops vendoringnosh. They reference each other by path. The contract isruntime/include/— a path every consumer#includes directly. Nosync.shexists insidekn-86. - One build per language: a top-level CMake project (runtime + hosts + ui) and a top-level Cargo workspace (sdk + device crates). A cross-cutting change — add a NoshAPI primitive, touch
runtime+sdk+ a cart + a test — becomes one atomic commit, green together. - Non-runtime assets stay out.
attract/,idleware-remotion/,marketing/,mockups/,prompts/,art/remain in the existing umbrella; Josh handles their placement separately. Out of scope for this ADR. - Clean curated move, no history preservation —
kn-86is seeded with one assembled commit of the source repos’ current trees in the new layout. History stays addressable in the archived source repos. This is the ADR-0039 extraction mechanic run in reverse.
Options Considered
Section titled “Options Considered”Option A: Consolidate to kn-86 + kec-lisp. (ACCEPTED)
Section titled “Option A: Consolidate to kn-86 + kec-lisp. (ACCEPTED)”Two repos: the language, and the product. Internal references replace all vendoring. Chosen because it removes copy-vendoring entirely, makes cross-cutting changes atomic, collapses CI to one repo (the private-PAT problem evaporates), and keeps the one real boundary (the language). YAGNI on repo boundaries pre-launch; re-splitting later is the same clean-curated move forward if a second consumer ever appears.
Option B: Keep the seven-repo split; fix vendoring (extract a contract repo + fetch-at-build).
Section titled “Option B: Keep the seven-repo split; fix vendoring (extract a contract repo + fetch-at-build).”Rejected. Still pays seven repos’ coordination cost for cadence that doesn’t exist, and a standalone contract repo is more machinery than one developer needs pre-launch. Right only if sdk/carts needed to be independently public now — they don’t.
Option C: Single monorepo including kec-lisp.
Section titled “Option C: Single monorepo including kec-lisp.”Rejected. kec-lisp is genuinely general (a language with its own published standard, plausibly reusable outside KN-86, public). It is the one real release/visibility boundary; folding it in would erase the one distinction that actually matters.
Trade-off Analysis
Section titled “Trade-off Analysis”| Dimension | A — kn-86 + kec-lisp (chosen) | B — 7 repos + contract repo | C — one repo incl. kec-lisp |
|---|---|---|---|
| Internal vendoring | ✓ none | ◐ less, not none | ✓ none |
| Atomic cross-cutting change | ✓ | ✗ still cross-repo | ✓ |
| CI / private-PAT burden | ✓ one repo, gone | ✗ persists | ✓ |
| Preserves the real (language) boundary | ✓ | ✓ | ✗ erases it |
| Coordination cost | ✓ low | ✗ high | ✓ low |
| Re-split later if needed | ✓ clean move | n/a | ◐ harder |
Consequences
Section titled “Consequences”Positive
- No internal vendoring; the four
fe.ccopies and threecell_api.ccopies collapse to one each. - Cross-cutting changes are one atomic, green commit.
- One build, one CI; the GitHub-Free private-repo-token problem disappears.
- The half-finished ADR-0039 extraction (the
kn86-emulatorhusk) gets reconciled during assembly.
Costs / follow-ons
- A one-time assembly + build re-wiring (CMake + Cargo workspace, vendoring → path refs, kec-lisp
FetchContent). - ADR-0039 is superseded; references across the docs to the seven-repo topology need a phase-gated sweep (lands with the move, same disposition ADR-0039 used for its own renames).
- The six source repos get archived with a pointer to
kn-86— only afterkn-86is verified green. - The Starlight docs-site deploy repoints from the kn86-docs repo to
kn-86/docs/. - CLAUDE.md’s topology + the DeckRunner naming update (the deferred phase 3) fold into the post-move CLAUDE.md rewrite.
- ADR-0040’s references to ADR-0039 / “the hosts link libnosh per ADR-0039” are updated in the same pass.
Migration sequence (locked)
Section titled “Migration sequence (locked)”| Step | Action |
|---|---|
| 1 | This ADR — supersede 0039; no code moves. |
| 2 | Create the kn-86 repo (KEC org). |
| 3 | Assemble the layout (curated copy of the six trees), wire the CMake project + Cargo workspace, replace every vendor/ with path refs, add kec-lisp FetchContent. |
| 4 | Green gate — ctest passes, the desktop host runs, kn86cart builds the launch carts. |
| 5 | Move kn86-docs in as kn-86/docs/; repoint the site deploy. |
| 6 | Archive the six source repos (pointers to kn-86) — only after step 4. |
| 7 | CLAUDE.md topology + DeckRunner naming; ADR-0040 reference fixups. |
Green-before-archive invariant: the source repos are not archived or deleted until kn-86 builds and tests green. The move is reversible up to that point.
Documentation Updates (REQUIRED — Spec Hygiene Rule 3)
Section titled “Documentation Updates (REQUIRED — Spec Hygiene Rule 3)”-
docs/adr/ADR-0041-monorepo-consolidation.md— this file. -
docs/adr/ADR-0039-repo-topology.md— Status marked Superseded by ADR-0041. -
docs/adr/README.md— index row added; 0039 row marked superseded. - Topology sweep (docs) —
definitive-guide.md(no seven-repo references found) andsoftware/runtime/*references to the seven-repo split reframed to thekn-86monorepo layout:deckrunner-engine-architecture.md(Platform/host repo links →hosts/emulator/hosts/devicepaths; “the vendoring fix” item marked resolved by this ADR),kec-lisp-runtime-architecture.md(vendor/fe→kec-lispfetched at build),orchestration.md(“vendored KEC Lisp” → fetched-at-build; emulator README path). TheCLAUDE.mdtopology sections (in the kinoshita repo, outsidedocs/) remain a follow-on. The broadkn86-emulator/src/*.csource-path rename (→runtime/src/*etc.) across the api-reference / cartridge-authoring / runtime docs is tracked separately as path-debt. - ADR-0040 — its ADR-0039 cross-reference (the Related “repo topology” pointer) now points at this ADR’s monorepo topology, with ADR-0039 retained as the superseded record.
Narrative
Section titled “Narrative”ADR-0039 split one repo into seven to buy independent release cadence, and paid for it in copy-vendoring — four copies of the language kernel, three of the cart contract, a tree of sync.sh scripts, and a private-repo token burden, all to coordinate code that has exactly one developer and ships as one device. The cadence never materialized. This ADR runs the move in reverse: the six product repos become one kn-86, the language stays its own public repo and is fetched rather than copied, and the contract becomes a directory everything includes instead of a tree everything duplicates. The dependency direction ADR-0039 drew is correct and preserved; only the packaging — the thing that actually hurt — is undone.