DeckRunner — KN-86 Engine & Run Architecture
Canonical hardware values: see the CLAUDE.md Canonical Hardware Specification (display, keyboard, coprocessor, power, case). This document references those values and never restates them.
What this is. A single comprehensive capture of the engine-and-gameplay architecture decided in the 2026-06-18 session. It names the engine layer (DeckRunner), fixes its boundaries, and positions it against the already-shipped Mission Control / Mission Runner / Capability Registry vocabulary. It is the detailed companion to ADR-0040; where ADR-0040 records the decision, this spec carries the detail.
0. Why this exists
Section titled “0. Why this exists”For a year the runtime has been called “nOSh, the orchestrator.” That single word hid two different things: the platform floor that drives the hardware, and the engine that runs the game on top of it. Conflating them is why “what owns the game loop?” had no clean answer, why the run loop is stranded in a host’s main.c, and why the player-facing scripting surface had no name. This spec separates the layers and names the missing one.
It captures both the architecture decisions (layers, repo discipline, the language boundary) and the gameplay-architecture decisions (state tiers, the two FFI surfaces, the verb/loadout model, the run pipeline) as one coherent record, because they interlock.
1. The layer model
Section titled “1. The layer model”Three layers, plus the language they all rest on. We were calling the middle two both “nOSh”; naming them apart is the spine of everything below.
┌───────────────────────────────────────────────────────────┐ │ Hardware — Pi Zero 2W · Pico 2 coproc · 7" panel · │ │ Ferris Sweep + 2 trackpoints · OLED │ (see CLAUDE.md) └───────────────────────────────┬───────────────────────────┘ │ ┌───────────────────────────────┴───────────────────────────┐ │ PLATFORM — the hosts (hosts/emulator / hosts/device) + │ │ nOSh's realtime C floor. Owns screen/audio/input/power, │ │ runs the seam (present / poll / push-audio), feeds device │ │ signals UP. Does not know what a mission is. │ └───────────────────────────────┬───────────────────────────┘ │ engine_step() / on_signal() ┌───────────────────────────────┴───────────────────────────┐ │ DECKRUNNER — the Engine. Owns the deck run loop, the │ │ screen router, the cart loader/executor, and the two FFI │ │ surfaces. CONTAINS Mission Control + Mission Runner. │ │ Doesn't know if it's on a Pi or a laptop. │ └───────────────────────────────┬───────────────────────────┘ │ runs ┌───────────────────────────────┴───────────────────────────┐ │ CONTENT — first-party system apps + carts. Lisp. │ └───────────────────────────────────────────────────────────┘
kec-lisp — the language — is embedded by DeckRunner (see §4).- Platform — boots the hardware, owns screen/audio/input/power, implements the host seam (
nosh_backend.h: present a frame, poll input, push audio), and feeds device signals up. Spans the two hosts (hosts/emulator,hosts/device) plus nOSh’s realtime/integrity C floor (renderer, audio path, input mechanism, save/economy integrity cores). It is mission-agnostic. - DeckRunner — the Engine. Owns the deck run loop, the screen router, the cart loader/executor, and the player- and cart-facing FFI surfaces. It contains the Mission Control and Mission Runner subsystems (§3). It is hardware-agnostic — it talks to the Platform only through the seam.
- Content — the thirteen first-party programs (Mission Board, Deck Hub, REPL, nEmacs, CONDUIT, Kinoshita Kommander, AmberCalc, RIPSAW, Keyring, kn9, DOSSIER, bzbx, knSALK — the canonical roster, ADR-0042), the four mission capability baselines (ADR-0030), and the carts. All Lisp.
Both DeckRunner and the Platform floor compile into libnosh (the runtime/ subtree of the kn-86 monorepo); the split is a tier boundary inside the library, not a repo split. The hosts (hosts/emulator, hosts/device) link libnosh and implement the seam beside it (per ADR-0041, which consolidated the seven-repo split of ADR-0039 into this monorepo).
2. What the device is: a fantasy console, not a terminal app
Section titled “2. What the device is: a fantasy console, not a terminal app”The device draws RGB565 pixels straight to the framebuffer (on hardware, straight into /dev/fb0). It is not a TUI — termbox2, a fixed text grid, and console fonts are retired (ADR-0036).
The “terminal” survives as exactly two things:
- an aesthetic — amber-on-black phosphor, 8×8 glyphs, the CRT feel; and
- a layout convention — the 128×75 cell grid is a placement grid (a CSS-grid in 8px units), not a hardware text mode.
Proof it is a framebuffer, not a terminal: HALF-BLOCK mode gives 128×150 sub-cell pseudo-pixels, and mixed-scale headlines compose beside 1× body text — both impossible on a real terminal.
The mental model is a fantasy console (the PICO-8 / TIC-80 lineage) with a diegetic terminal skin and real hardware underneath. PICO-8 is “Lua VM + draw/sound API + a fixed update/draw loop.” The KN-86 is “KEC VM + NoshAPI + the deck run loop.” Same animal, dressed as a deck. This reframe is what gives the engine layer (§3) its shape.
3. DeckRunner vs Mission Control vs Mission Runner — the containment
Section titled “3. DeckRunner vs Mission Control vs Mission Runner — the containment”This is the most important reconciliation in this document, because two of these names already ship.
- Mission Control (ADR-0028) — the subsystem that makes contracts: the Capability Registry, schema-driven generation, TTL/Opportunity contracts, the operator-facing mission board.
- Mission Runner (ADR-0029) — the post-acceptance environment: the REPL/nEmacs subsystem with mission-context bindings (
current-mission,(load-capability …),(complete-mission …)), where the operator drives an accepted contract.
DeckRunner does not rename or supersede either. DeckRunner is the engine layer that contains them both, plus the run loop, the screen router, the cart loader, and the FFI surfaces:
DECKRUNNER (the engine — owns the run loop + screen router + cart loader + FFI surfaces) ├── Mission Control (ADR-0028) — generates contracts → the board ├── Mission Runner (ADR-0029) — runs an accepted contract (REPL + bindings) ├── the programs — the thirteen first-party programs (ADR-0042) + the 4 mission baselines (ADR-0030) └── the cart loader / executor + NoshAPI + the DeckRunner interfaceSo: Mission Control is the front of the run (pick a contract); Mission Runner is the back (execute it); DeckRunner is the engine both live inside, and the thing the Platform actually calls each frame. When this spec says “DeckRunner owns the run loop,” the loop dispatches into Mission Control (board screen) and Mission Runner (active contract) as the operator moves through them.
4. The kec-lisp ↔ nOSh boundary (established context)
Section titled “4. The kec-lisp ↔ nOSh boundary (established context)”Unchanged from ADR-0037 / ADR-0004; restated here only to place DeckRunner relative to the language.
- kec-lisp is a general-purpose Lisp shipped as a library — reader, evaluator, GC, arena, KEC Core prelude, portable stdlib. It knows nothing of the KN-86. The boundary is one file:
kec-lisp/runtime/kec.h, exposingkec_open_with_arena(no-malloc),kec_eval_*, andkec_bind_fe. - DeckRunner embeds it — opens a context on an arena, binds the device primitives (§7), loads the system + cart Lisp, and runs it.
- The test for where code goes: would it make sense in a Lisp that never heard of the KN-86? → kec-lisp. Does it touch screen / audio / carts / missions? → nOSh (DeckRunner or the Platform floor).
5. DeckRunner: the engine boundary
Section titled “5. DeckRunner: the engine boundary”5.1 The host-facing API
Section titled “5.1 The host-facing API”DeckRunner’s public boundary is the existing nosh_host.h, renamed and owned as the engine API:
init(argc, argv)— boot the engine (open the KEC context, wire the coprocessor, load the default cart / bare deck, run the boot sequence).load_cart(cart)— mount a capability module (see §13 step 1).step()— advance one frame: pump input, drive the active surface (Mission Control board / Mission Runner / an app), render, present. The loop body.on_signal(sig)— consume a Platform signal (key, accelerometer, battery, coproc).shutdown()— tear down; persist deck state.- UDS read/write (§6).
5.2 Loop ownership
Section titled “5.2 Loop ownership”The Platform owns the while (timing, frame pacing, calling present). DeckRunner owns the body (step). The seam between them is that one call:
host main(): init(); while (step()) { } shutdown();This is the standard fantasy-console shape (PICO-8’s host owns the 30fps loop; the cart’s _update/_draw is the body). “DeckRunner owns the game loop” means it owns what happens each tick; the Platform owns the clock.
5.3 C engines, Lisp screens, seam binders
Section titled “5.3 C engines, Lisp screens, seam binders”The runtime is C engines + Lisp screens, glued by thin C “screen-seam” binders. The naming collision (mission_board.c vs board.lsp) hides this — they are different layers:
board.lsp= the screen (UI).mission_board.c= the contract generator (engine).board_screen.c= the seam thatkec_bind_fes the engine into the screen. Same shape for repl / nemacs / deck / sys.
Three buckets:
- C forever — renderer, language host, audio realtime (PSG), event bus, economy / phase / contract-generator, cart FSM, save / integrity. (Deterministic, realtime, GC-pause-free, integrity-sensitive.)
- Lisp by design — the screens, the UI kit, the baselines, all cart content. (Presentation + gameplay.)
- C now, data-to-Lisp later — e.g. the mission template pool: a static C table today, replaced by cart
defmission/defcontract-schemaparsing post-launch (ADR-0028). The engine stays C; only the data moves.
5.4 Current debt — the stranded loop
Section titled “5.4 Current debt — the stranded loop”The loop body (nosh_host_step) is declared in nosh_host.h but not yet lifted — its implementation still lives in the emulator’s SDL main.c, fused with SDL_PollEvent. Until it is lifted into libnosh, the device host can only paint a boot HUD. Lifting the loop into DeckRunner is the single refactor that lets the device run the real thing, and the precondition for everything in this spec to run on hardware. It is the highest-priority downstream item (§17).
6. State model — three tiers
Section titled “6. State model — three tiers”The boundary that keeps the player console from being a cheat console: you can mutate the run and your settings; you cannot forge your profile.
| Tier | What | Writable by player/REPL? | Written when |
|---|---|---|---|
| Run state (volatile) | in-flight mission, drone position, app working buffers, REPL-defined functions | Yes — this is the gameplay | continuously, during a run |
Config / options (nosh-config.toml) | phosphor color, glitch level, prefs | Yes — toggles | on setting change |
| Deck State / UDS (durable, game-significant) | handle, credits, reputation, cart history, phase chain | No — read-only to the player | only by DeckRunner, as the earned consequence of a sanctioned outcome |
UDS is written only by DeckRunner’s economy/phase cores as the result of a sanctioned outcome (a run completes, the payout posts) — never as a typed command. Durable progress changes as a result, never as an input. The arena that backs run state resets at the mission-instance boundary (per the runtime memory model), bounding it deterministically.
7. The two FFI surfaces & the privilege funnel
Section titled “7. The two FFI surfaces & the privilege funnel”Same engine, two audiences, two trust levels:
- NoshAPI — the cart-facing FFI (ADR-0005). What a cart’s Lisp may call.
- The DeckRunner interface — the player-facing surface: the console verbs + the scripting API. This includes the Mission Runner’s mission-context bindings (ADR-0029:
current-mission,(load-capability …),(complete-mission …)) and the broader console/inspection verbs. It is new vocabulary — the player-facing surface has not been named as a boundary before this spec.
The funnel — each layer narrows:
player REPL / scripts ──▶ ONLY the DeckRunner interface │ (DeckRunner re-exports SELECTED NoshAPI on its own terms) ▼ selected NoshAPI ──▶ Platform seamPlayer Lisp never reaches raw NoshAPI or the Platform. The DeckRunner interface is the sandbox wall for untrusted player code. A cart augmenting the player’s vocabulary (§11) does so by contributing verb-tools that DeckRunner chooses to expose through this interface — never by binding into the player’s hands directly.
8. The REPL — the engine console
Section titled “8. The REPL — the engine console”The REPL is a DeckRunner surface (not a cart, not Platform). It is a first-class player feature, not a debug backdoor — think a Quake ~ console with real power.
- Dropdown, Quake-style, over whatever surface you’re on (TERM key; context-polymorphic dispatch per ADR-0016).
- Scriptable — player-written functions execute parts of a run (e.g. script a short function to unlock a code). The set of verbs DeckRunner exposes is the game’s programming language. With a contract active, the REPL is the Mission Runner (ADR-0029) — same surface, plus mission bindings.
- Read: wide open — inspect the board, deck state, missions; dump anything.
- Write/control: drives volatile run activity and toggles options. Cannot write UDS (§6). Game-significant mutation is diegetic — it costs something and routes through the same guarded cores carts use. No backdoor.
9. Input & the keyboard — anchors + layers
Section titled “9. Input & the keyboard — anchors + layers”Hardware: the Ferris Sweep (34 keys, 17/side) + 2 trackpoints (see CLAUDE.md; ADR-0031 / ADR-0032). This section refines the prior “left hand = 14 fixed Lisp primitives, never remap” model (ADR-0022).
The invariant is not a fixed half of the board — it is a small set of anchor keys that mean the same thing in every layer, plus a layered remainder:
- Anchors (never swap): the human-control spine — BACK (up/escape), EVAL/ENT (commit), SYS (home/settings), and the layer-shift thumbs. A non-programmer navigates the entire deck on these.
- Layers (DeckRunner swaps by context): Nokia/T9 text · punctuation/symbols (its own layer — programming needs the punctuation) · Lisp primitives (the common subset; there are more verbs than keys) · gameplay verb palettes (§10) · per-app chords.
- Trackpoints: keys do what (verbs/grammar); the trackpoint does where (spatial pointing inside the visual apps — sonar cursor, ledger scrub, node topology, nEmacs caret). No verb mapping needed.
Mechanism vs policy: QMK holds the layers electrically; DeckRunner picks which layer is live and guarantees the anchors. Physical key identity is Platform/firmware; what a key means in context is the Engine’s.
10. The verb model — three tiers
Section titled “10. The verb model — three tiers”Reconciles “fixed Lisp keys” with “context layers”: grammar is fixed, vocabulary is layered.
| Tier | What | Owner | On the deck |
|---|---|---|---|
| 1 — Primitives (grammar) | the Lisp keys (CAR/CDR/CONS/EVAL/…) | firmware (fixed) | left hand, never layers |
| 2 — Verb-tools (vocabulary) | CRACK, SPIKE, PING, RECONCILE … nouns you select + CONS + EVAL | the cartridge, surfaced via DeckRunner | right-hand context palette, swapped per app/phase |
| 3 — Mission verbs (arc) | PENETRATE / OBTAIN / OBSERVE / DESTROY + affinities | DeckRunner’s Mission Control | not keys — the structure of a run (procgen + attainment) |
Tier 3 is the existing mission-composition verb vocabulary + affinities from Mission Control (ADR-0028). The Tier-2 domain vocabularies (netrun / drone / forensic; comms folds into an existing cart’s context — §16) are cart-contributed. Grouping verbs “by the arc of a run” IS the layer schedule — DeckRunner swaps the active palette as the run moves recon → entry → breach → exfil. IceBreaker already ships all three tiers.
11. Capability model — baseline vs cart-deepened
Section titled “11. Capability model — baseline vs cart-deepened”A two-layer capability stack that extends the Capability Registry tiers (ADR-0028/ADR-0030):
- DeckRunner ships shallow, generic base apps — a basic spreadsheet, basic nav, basic sysop/antivirus, basic blackhat. First-party, inside DeckRunner (same shelf as the REPL and nEmacs; this is the System tier of the Capability Registry — runtime-shipped, never decays). Makes the bare deck playable; provides a shared verb floor.
- A cart deepens its domain — Depthcharge turns “basic nav” into the full submersible/sonar rig; Black Ledger turns “basic spreadsheet” into the forensic suite; IceBreaker turns “basic blackhat” into full netrun. A cart augments (adds verbs to the palette) and enhances (deepens verbs, unlocks the full interface). This is the
:supersedesweighting already in ADR-0028’s amendment, generalized to the app/verb surface.
Carts augment the player-facing vocabulary only through the DeckRunner interface (§7) — never reaching the player’s hands directly.
A capability, once entered, presents one of two shapes — a grammar interaction (Tier-2 verb-tools) or a mini-game (a bespoke framebuffer loop) — both (load-capability …) targets behind the same result-struct contract (§13). The cart chooses per capability; missions may mix shapes across phases. See ../cartridges/authoring/capability-shapes.md.
12. Lambda Slots — loadout & the swap mechanic
Section titled “12. Lambda Slots — loadout & the swap mechanic”Because enhanced capability swaps (loading one un-loads another — full swap, confirmed, not additive), capability management becomes the core strategic layer. The thing you manage is Lambda Slots = your loadout.
- Limited slots; equip enhanced verb-tools + scripted lambdas; loading one swaps another out. Slot count is a progression unlock.
- The swap has a cost, and it is a coupled mixture — not pick-one. Time-pressure and exposure feed each other; which dominates is context-dependent on the capability — e.g. missing the cryptographic capability at the wrong moment slows a hack, and the slowdown is what raises exposure. Sometimes the clock, sometimes the heat, usually one bleeding into the other. (Concrete numbers/feel are deferred to gameplay design — §16.)
- Never fully bare: unslotted, you fall back to the shallow baseline (slower, louder). Baseline = always-on floor; enhanced = loadout-gated ceiling.
- The run’s phase-arc is the swap schedule: commit a loadout per phase, live with its gaps. Sequencing loadout against obstacle order is the run.
Open reconciliation (§16): ADR-0029 treats a
(load-capability …)call as opaque — the Mission Runner does not observe the cart’s internal loop during a call. The loadout/verb-palette/multi-modal play described here happens inside that call (cart-side gameplay), while the Mission Runner sequences between calls. Where exactly the slot/swap boundary sits relative toload-capabilityis a follow-on reconciliation, not settled here.
13. The run execution pipeline
Section titled “13. The run execution pipeline” Mission Control Mission Runner DeckRunner cores ─────────────── ───────────────────────────── ───────────────── 1. cart first-load ─▶ (installs templates+vocab to UDS) 2. procgen → board 3. select → instantiate run (volatile) 4. execute — multi-modal challenge 5. attainment tracking 6. resolution → UDS write (sanctioned) 7. progression → next board- Cart first-load (one-time per deck): DeckRunner reads the cart’s manifest (mission templates, CIPHER vocabulary, verb-tool defs, generation tables) and installs them into UDS — a sanctioned engine write (not a REPL write). The cart’s templates join the deck’s generation pool permanently (the
cartridge_historycapability gate, ADR-0028). - Procgen → mission board: Mission Control seeds from UDS + installed templates and generates contracts (the schema/
defmission-fed generator that replacesmission_board.c’s static pool). - Select → instantiate a run: the volatile run-state object (arena resets at the mission-instance boundary) — objectives, active verb palettes, obstacles, win/fail. This is the Mission Runner taking over.
- Execute — multi-modal challenge: DeckRunner
stepin gameplay mode, across all channels at once: typing/scripting (REPL + Lambda Slots), visual (framebuffer apps), sound (PSG cues, sonar returns), cipher line (OLED voice). One obstacle can demand all four. - Attainment tracking: an objective ledger; DeckRunner flips each task as it is met; all-required-met → resolve.
- Resolution → sanctioned UDS write: the economy/phase cores write credits / reputation / phase advance / unlocks — the only path durable progress changes (§6).
(complete-mission …)/(abandon-mission)finalize (ADR-0029). - Progression: those unlocks (more slots, harder templates, deeper verbs) feed the next board.
14. Accessibility — two skill bands
Section titled “14. Accessibility — two skill bands”“Not everyone is a programmer” resolves into two paths to the same goals:
- Floor: anchors + Nokia text + select-and-fire verb-tools + loadout timing. A non-programmer plays the entire game here.
- Ceiling: writing lambdas to automate / combine / unlock what the verb path can’t.
Programming is the power amplifier, never the gate. The deck is winnable without it and better with it — keeping the Lisp identity for those who want it without locking out those who don’t.
15. Design guardrails
Section titled “15. Design guardrails”- The ‘86 is a skin, not a constraint engine. Pick limits for feel, not for period authenticity — like a fantasy console picks tasteful limits instead of emulating a real 8-bit machine. “Could a 1986 box do this?” does not get a veto.
- Scripts stay sandboxed by the funnel (§7). Powerful inside a run; structurally unable to touch UDS or the hardware.
- No spec duplication. Canonical hardware values live in CLAUDE.md; reference, never restate.
16. Decisions deferred / open
Section titled “16. Decisions deferred / open”| Item | Disposition |
|---|---|
| Palette swap: full or additive | Full swap — confirmed (it is what makes loadout strategic). |
| Swap currency (numbers/feel) | Deferred to deeper gameplay design. Model agreed: coupled time↔exposure, context-dependent per capability. |
| Trackpoint per-app bindings | Deferred to deeper gameplay design. Lane agreed: spatial pointing in visual apps. |
| Comms verb set ownership | Resolved — no separate comms cart; fold comms into an existing cartridge’s context. |
| DeckRunner interface / console naming | Open — the engine is “DeckRunner”; whether the player-facing interface and the console get their own names is undecided. |
| Loadout/swap vs ADR-0029 opaque-call boundary | Open reconciliation — where slot/swap management sits relative to (load-capability …) (§12). Needs a follow-on. |
17. Consequences & downstream work
Section titled “17. Consequences & downstream work”This spec is the input to a significant amount of build work. In rough dependency order:
- Lift the run loop into
libnosh(nOSh) — implementnosh_host_step()by moving the body out of the emulator’s SDLmain.c. Gates the device running the real thing; gates everything else here. (Highest priority.) - Name the engine tier “DeckRunner” across docs and code — a terminology sweep, phase-gated like the ADR-0039 renames (decision now; sweep follows). Update CLAUDE.md’s runtime sections (kinoshita repo) as a follow-on.
- Specify the DeckRunner interface — the player-facing FFI surface (sibling to NoshAPI). It needs its own spec and an ADR-0005 amendment; today it exists only as the Mission Runner bindings (ADR-0029) without a named boundary.
- The vendoring fix (repo discipline) — resolved by ADR-0041: the seven-repo split + copy-vendoring is consolidated into the single
kn-86monorepo. Components reference each other by path (runtime/include/is the contract); the only remaining external dependency iskec-lisp, fetched at build (never vendored). Tracked separately from the engine work. - Detailed gameplay design — swap currency numbers, trackpoint bindings, and the loadout/
load-capabilityreconciliation (§16).
Related
Section titled “Related”- ADR-0040 — the decision this spec backs.
- ADR-0028 — Mission Control (a DeckRunner subsystem).
- ADR-0029 — Mission Runner (a DeckRunner subsystem).
- ADR-0041 — repo topology (supersedes ADR-0039); DeckRunner lives in
libnosh(runtime/), the hosts inhosts/. - ADR-0036 — the framebuffer renderer (why it’s a fantasy console, not a TUI).
- ADR-0005 — NoshAPI (the cart-facing surface).
- ADR-0031 / ADR-0022 — keyboard (anchors + layers refines these).