Skip to content

ADR-0041: Consolidate the product repos into a single kn-86 monorepo

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:

  1. Transitive copy explosion. The Fe kernel fe.c exists 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 of sync.sh scripts, and nothing tells you when two repos are building against different commits.
  2. 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’s render.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.
  3. 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.
  4. A half-finished extraction. P4 of ADR-0039 never completed: kn86-emulator is 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.

Consolidate to two repos: kec-lisp (unchanged) and a new kn-86 monorepo.

  1. 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.
  2. kec-lisp stays a separate, public repo — the general language is the one genuine release/visibility boundary. kn-86 consumes it via fetch-at-build (CMake FetchContent on a pinned tag), not a copy. This is the only remaining external dependency, and it is no longer vendored.
  3. 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
  4. All internal vendoring is eliminated. ui stops carrying its vendored copy of the runtime; sdk/carts stop vendoring ui; device stops vendoring nosh. They reference each other by path. The contract is runtime/include/ — a path every consumer #includes directly. No sync.sh exists inside kn-86.
  5. 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.
  6. 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.
  7. Clean curated move, no history preservationkn-86 is 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.

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.

DimensionA — kn-86 + kec-lisp (chosen)B — 7 repos + contract repoC — 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 moven/a◐ harder

Positive

  • No internal vendoring; the four fe.c copies and three cell_api.c copies 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-emulator husk) 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 after kn-86 is 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.
StepAction
1This ADR — supersede 0039; no code moves.
2Create the kn-86 repo (KEC org).
3Assemble the layout (curated copy of the six trees), wire the CMake project + Cargo workspace, replace every vendor/ with path refs, add kec-lisp FetchContent.
4Green gatectest passes, the desktop host runs, kn86cart builds the launch carts.
5Move kn86-docs in as kn-86/docs/; repoint the site deploy.
6Archive the six source repos (pointers to kn-86) — only after step 4.
7CLAUDE.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) and software/runtime/* references to the seven-repo split reframed to the kn-86 monorepo layout: deckrunner-engine-architecture.md (Platform/host repo links → hosts/emulator / hosts/device paths; “the vendoring fix” item marked resolved by this ADR), kec-lisp-runtime-architecture.md (vendor/fekec-lisp fetched at build), orchestration.md (“vendored KEC Lisp” → fetched-at-build; emulator README path). The CLAUDE.md topology sections (in the kinoshita repo, outside docs/) remain a follow-on. The broad kn86-emulator/src/*.c source-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.

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.