nEmacs Structural Editor — Design
Supersedes inline spec in: ADR-0002 §“nEmacs — Structural Editor”, ADR-0008 (partial; see Open Questions §1 for row-layout reconciliation) Hardware reference: see CLAUDE.md Canonical Hardware Specification — grid, font, colors, keys, row layout.
This document designs the runtime-level nEmacs structural editor for the KN-86 Deckline and specifies the explicit cross-capability contracts with Mission Board, Cipher voice, and the dev REPL. Every buffer nEmacs shows is an AST, not a character stream: the “program is always well-formed” invariant is the whole point of the editor’s existence and it is the reason a 31-key device can edit Lisp at all.
Responsibilities & owned data
Section titled “Responsibilities & owned data”nEmacs is a nOSh-runtime-owned, singleton subsystem. The nOSh runtime instantiates exactly one nEmacs session at a time, occupying a 16 KB editor arena. When nEmacs is active, the rest of the nOSh runtime treats it like any other foreground orchestrator (mission board, REPL): nOSh still owns Row 0 and Row 24, the Cipher voice, and the dispatch back to the home screen.
Owned data
Section titled “Owned data”| Structure | Arena | Persistence | Notes |
|---|---|---|---|
Edit buffer AST (nemacs_buffer_t) | Editor arena (16 KB) | Volatile unless SYS-saved | Pool of tree nodes; each node carries tag, type token id, literal bytes (if any), child head, next sibling, parent backptr. |
Cursor (nemacs_cursor_t) | Editor arena | Volatile | Pointer to AST node + editing-mode enum + scroll anchor. |
Palette state (nemacs_palette_t) | Editor arena | Volatile | Ranked token array (top N, N ≥ 8), scroll offset, open/closed flag, filter predicate handle. |
| Recency ring | Editor arena | Volatile per session | 20-token FIFO of tokens inserted this editing session. |
| Local bindings index | Editor arena | Volatile | Hash-of-symbol-id → AST node ref, rebuilt on binding insert/delete. |
| Literal-entry scratch | Editor arena (256 B cap) | Volatile | Multi-tap buffer + cursor within buffer. |
| Grab-mode clipboard | Editor arena | Volatile across a single nEmacs session, discarded on teardown | Subtree root + shallow copy semantics. |
| Snippet library index | Deck state flash | Persistent | Name → blob offset + size; limit 32 snippets at v1 (see §Snippet library). |
| Snippet library blobs | Deck state flash (separate page) | Persistent | Serialized AST (canonical Lisp source text, UTF-8 CP437) per snippet. |
| Last-mission resume slot | Deck state flash (1 slot) | Persistent across suspends | Mission id + serialized buffer + cursor path. See §Lifecycle. |
What nEmacs does NOT own
Section titled “What nEmacs does NOT own”- The VM / evaluator. EVAL on a top-level form is a dispatch to the REPL arena (24 KB); nEmacs does not compile or execute Lisp itself.
- The status bar and action bar (Rows 0, 24). nOSh owns these. nEmacs only requests slot updates via a published firmware API.
- The mission acceptance contract. Mission Board owns the contract predicate; nEmacs only knows the contract handle and the error-scope format.
- The domain vocabulary. Cartridges register vocabulary at load; nEmacs reads it through a published query API. nEmacs never mutates the vocabulary table.
- Persistent deck state (credits, reputation, cartridge history). The restricted FFI model from ADR-0007 still applies: nEmacs can read-only-display these via nOSh, but scripted missions mutate only mission-local state.
UI zones
Section titled “UI zones”The usable content area is Rows 1–23 (23 rows × 80 cols), per CLAUDE.md Canonical Hardware Specification. Row 0 and Row 24 are firmware territory. nEmacs does not draw them.
Layout within Rows 1–23
Section titled “Layout within Rows 1–23”Row 1: [nEmacs title bar: "nEmacs: <buffer-name>" ... <MODE> ... <DIRTY?>]Rows 2–20: [Code buffer viewport — 19 rows of AST rendering]Row 21: [Local status: path-from-root | depth | position-kind | literal-scratch echo]Row 22: [Palette row 1: up to 4 tokens, slots [1]..[4], each up to 16 cols wide]Row 23: [Palette row 2: up to 4 tokens, slots [5]..[8] + scroll/hint indicator]This is a 5-zone layout inside content (title, code, local status, palette A, palette B). It fits inside Rows 1–23 and leaves Row 0 and Row 24 to nOSh.
- Row 1 (title bar) is nEmacs’s title line, not the nOSh runtime status bar. It carries
buffer-name, current editing mode (NORM/PAL/LIT/GRAB), and a dirty marker*. - Rows 2–20 (code buffer viewport, 19 rows) is the AST render. Indentation is derived from tree depth, not literal characters. Parens are rendered as structural box-drawing glyphs from the CP437 layer (
├,│,└,┐,┤) — see §Rendering rationale. - Row 21 (local status) shows cursor path (e.g.
defn > filter > body > if > cond), depth, position kind (FUNCTION_POS / ARGUMENT_POS / BINDING_POS / ROOT), and — when literal-entry mode is active — the growing literal buffer echo (e.g.lit> thresh_). - Rows 22–23 (palette, 2 rows) render the top-8 ranked tokens in two rows of four 16-col slots. When the palette scrolls (more than 8 candidates), Row 23 shows a scroll indicator in the rightmost slot.
Reconciliation with firmware rows (Row 0 / Row 24)
Section titled “Reconciliation with firmware rows (Row 0 / Row 24)”nEmacs publishes a display intent to nOSh on every frame:
nemacs_publish_status_hint(struct { const char *mode_label; /* "EDIT", "PAL", "LIT", "GRAB" */ const char *buffer_label; /* e.g., "filter-list.lsp" */ bool dirty; const char *key_hints[6]; /* CAR, CDR, CONS, EVAL, NIL, BACK — one short verb each */}) -> void;- Row 0 (firmware status bar): nOSh retains operator handle + credits + reputation at left; right-half is mode + buffer name + dirty marker from the hint.
- Row 24 (firmware action bar): nOSh renders the six most-relevant key hints sourced from nEmacs’s hint struct. The label set rotates with mode (see §Input grammar).
This is strictly cartridge-style: nEmacs hints, nOSh renders. nEmacs never writes Rows 0 or 24 directly.
Rendering rationale (why the palette belongs in Rows 22–23, not 23–25)
Section titled “Rendering rationale (why the palette belongs in Rows 22–23, not 23–25)”ADR-0008’s mockups put the palette in “rows 23–25” and describe a bottom 3-row strip. That is incompatible with the CLAUDE.md row layout (content ends at Row 23; Row 24 is firmware). This design collapses the palette into 2 content rows (22–23) and uses nOSh’s action bar (Row 24) for the key legend that ADR-0008’s third palette row was duplicating. See §Open questions for the explicit reconciliation.
Error rendering
Section titled “Error rendering”When Mission Board returns an error scope from an acceptance contract evaluation, nEmacs receives a list of AST node ids that are “in-scope” for the failure. It wraps those nodes with CP437 box glyphs (╌ ┌─, ╌ ┘) and inserts a one-line error banner into Row 21 (local status). The palette row then auto-filters to tokens legal-at-cursor AND likely-to-resolve the error (delegating the “likely-to-resolve” check to a contract-supplied optional fix_hint_fn, if provided; otherwise fall back to pure legal filter).
Input grammar
Section titled “Input grammar”nEmacs has four editing modes: default, palette-open, literal-entry, and grab. All 31 keys (14 function + 16 numpad + 1 TERM) have defined behaviors in each mode; the table below is the full contract. Behaviors not listed for a mode default to “beep (SFX_ERROR) and no-op.”
Mode: default (NORM)
Section titled “Mode: default (NORM)”| Key | Behavior |
|---|---|
| CAR | Descend into first child of cursor node. If leaf, beep. |
| CDR | Move to next sibling. If last sibling, beep. |
| CONS | Open palette for current position; enter palette-open mode. |
| NIL | Delete current node; push to grab-clipboard (cut-semantics single-step undo). If node was the only child, the parent gets an empty-placeholder node to preserve well-formedness. |
| BACK | Ascend to parent. If at root, beep. |
| QUOTE | Enter grab mode rooted at the current node. |
| LAMBDA | Enter literal-entry mode. Scratch buffer starts empty. |
| EVAL | Dispatch current top-level form (nearest enclosing defn/defmission/top-level expression) to REPL for evaluation. On mission buffers, this triggers acceptance-contract check (see §Scripted-mission flow). |
| ATOM | Query node type; write one-line type summary to Row 21 (local status). Read-only. |
| SYS | Save and exit. Calls nemacs_persist_or_teardown() (see §Lifecycle). |
| INFO | Longer introspection — opens type/position/size overlay occupying Rows 21 bottom half only (still leaves palette rows visible). |
| Numpad digits (1–8) | Quick-select from palette top 8 WITHOUT opening palette — insert token at that palette index. (This is a power-user shortcut; palette still re-ranks after insertion.) |
| Numpad 9, 0, . | Reserved in NORM mode (no-op). |
| PAD arrows (if present on hardware) | Scroll palette rows 22–23 for candidate beyond top 8. |
Mode: palette-open (PAL)
Section titled “Mode: palette-open (PAL)”| Key | Behavior |
|---|---|
| Numpad 1–8 | Select token at palette slot N; insert at cursor; return to NORM; cursor moves to new node. |
| EVAL | Confirm current highlighted palette entry (for when player used PAD to navigate within palette). |
| BACK | Dismiss palette without insertion; return to NORM. |
| PAD up/down | Scroll palette candidate list (reveals ranks 9–16, 17–24, etc.). |
| CAR/CDR | Re-filter palette on a textual prefix? No. Prefix-filter is a v1.1 nice-to-have. In v1, CAR/CDR in PAL mode are no-ops (beep). |
| CONS | Beep (already in palette). |
| LAMBDA | Switch to literal-entry mode directly, carrying the intent to insert a literal at the current position. Palette closes. |
| NIL | Beep. |
| QUOTE | Beep. |
| ATOM | Describe the currently-highlighted palette candidate (its type, arity) in Row 21. Read-only. |
| SYS / INFO | Beep. (Must exit palette with BACK or EVAL first.) |
Mode: literal-entry (LIT)
Section titled “Mode: literal-entry (LIT)”| Key | Behavior |
|---|---|
| Numpad 0–9 | Multi-tap character input into scratch buffer (standard phone-style multi-tap map; see §Literal-entry layout). |
| Numpad . | Insert . into scratch (required for float literals; also serves as string delimiter pair in string mode — see below). |
| Numpad ENT | Confirm. Parse scratch as (in order of preference) number → keyword → identifier → string. Insert as AST leaf. Return to NORM. |
| LAMBDA | Cancel literal-entry. Scratch discarded. Return to NORM. |
| BACK | Backspace in scratch. If empty, cancel and return to NORM. |
| EVAL / CONS / NIL / QUOTE / CAR / CDR | Beep. |
| ATOM | Display current literal mode (LIT: num / LIT: id / LIT: str) in Row 21. The mode is inferred from scratch content; player can force a mode via a long-press on Numpad .. |
| SYS | Abort literal-entry and execute SYS (save + teardown). Scratch is discarded. |
| INFO | Show literal-entry multi-tap map in Row 21 half. |
String mode sub-behavior
Section titled “String mode sub-behavior”If scratch starts with . (single dot), interpret as start-of-string. Subsequent numpad presses insert ASCII CP437 text via multi-tap. Terminate with another . + ENT. The first and last . are dropped; the middle becomes the string content. This avoids needing a dedicated string key.
Mode: grab (GRAB)
Section titled “Mode: grab (GRAB)”| Key | Behavior |
|---|---|
| CAR | Expand grab region downward to include first child. |
| CDR | Expand grab region rightward to include next sibling. |
| BACK | Contract grab region to just the root node (or if already at root node, exit GRAB mode). |
| EVAL | Confirm grab — copy subtree(s) into grab-clipboard, return to NORM. Cursor stays. |
| NIL | Confirm grab AND delete — cut subtree(s) into grab-clipboard, return to NORM. Cursor jumps to parent of deleted region. |
| CONS | Paste grab-clipboard here (insert before current cursor node as new sibling). Empties clipboard? No — clipboard persists until session end, so the player can paste the same subtree multiple times. Return to NORM after paste. |
| QUOTE | Beep (already in grab). |
| LAMBDA / Numpad / SYS / INFO / ATOM | Beep except SYS which still saves and tears down. |
Mode transitions summary
Section titled “Mode transitions summary” CONSNORM ───────► PAL ────[digit/EVAL]──► NORM (inserted) │ │ │ └──BACK──► NORM (cancelled) │ │ LAMBDA └──────────► LIT ────[ENT]──► NORM (inserted) │ │ │ └──LAMBDA/BACK-at-empty──► NORM (cancelled) │ │ QUOTE └──────────► GRAB ───[EVAL|NIL]──► NORM (copied/cut) │ └──BACK─at-root──► NORM (cancelled)Lifecycle
Section titled “Lifecycle”Invocation
Section titled “Invocation”nEmacs is launched in exactly three ways:
- From home screen / SYS menu — empty buffer, no contract. Used for free-form snippet authoring. Calls
nemacs_open_empty(). - From Mission Board when a scripted mission is accepted — buffer seeded from mission template, acceptance contract installed. Calls
nemacs_load_mission(mission_id, buffer_seed, contract_fn, error_scope_fn)— full signature in §Cross-capability hooks. - From snippet library or SYS menu → “Resume” — buffer loaded from deck state, no contract (unless resuming a suspended mission, in which case the mission hook is re-invoked internally). Calls
nemacs_open_snippet(name)ornemacs_resume_suspended().
Buffer load rules
Section titled “Buffer load rules”| Source | Buffer state on entry | Palette seed | Contract | Arena |
|---|---|---|---|---|
| Empty | Single empty root placeholder | Top-level forms filtered for current loaded cart vocab | None | 16 KB fresh |
| Mission | buffer_seed AST (may be empty, a template skeleton, or a partial function signature) | Scoped to mission’s granted FFI tier (per ADR-0007 Tier 1 + granted Tier 2) | contract_fn handle | 16 KB fresh |
| Snippet | Deserialized AST from deck-state blob | Player’s recency ring from last session (if same-cart), else fresh | None | 16 KB fresh |
| Resume | Last-mission resume slot | Rehydrated recency ring if serialized, else fresh | Original mission contract re-hooked from mission id | 16 KB fresh |
Suspension rules (coexistence with active cartridges, REPL, Mission Board)
Section titled “Suspension rules (coexistence with active cartridges, REPL, Mission Board)”The 520 KB SRAM budget has to host: static firmware + framebuffer + cipher state + active cartridge arena (16–32 KB) + REPL arena (24 KB when active) + editor arena (16 KB when active) + phase chain (256 B) + misc. Not all of these can coexist — explicit rules:
| State | Active-cart arena | REPL arena | Editor arena | Rule |
|---|---|---|---|---|
| Home screen | loaded | unused | unused | Baseline. |
| Mission Board browsing | loaded | unused | unused | Cart templates parsed, contracts generated. |
| Active mission (non-scripted) | loaded + working | unused | unused | Phase handler running. |
| REPL open from home | suspended (code pages kept mapped; cart working data paged out) | allocated | unused | Cart cannot run handlers. |
| nEmacs open on empty / snippet | suspended (same as REPL) | unused | allocated | |
| nEmacs open on mission buffer | kept loaded (mission context needed) | unused | allocated | Mission template data and cartridge vocab must be queryable. |
| nEmacs EVAL dispatch | kept loaded | momentarily allocated | allocated | Peak concurrent arenas: cart + REPL + editor = up to 72 KB. See §Cross-capability hooks → dev REPL. |
| Hot Swap request from Mission Board while in nEmacs | — | — | must suspend before swap | See §Scripted-mission flow → cartridge-swap interaction. |
Suspension semantics: suspension serializes the active cart’s working state (not its code) to deck state, frees its working-data pages, keeps its code/templates pages mapped read-only. On resume, the cart is re-initialized with the serialized state. This is the same mechanism nOSh already uses for REPL today (per ADR-0002 §Consequences).
Save / teardown
Section titled “Save / teardown”SYS in any mode triggers nemacs_persist_or_teardown():
- If buffer has a contract (mission): do not save as snippet automatically. Prompt:
SAVE AS SNIPPET? CONS=yes BACK=no EVAL=no. If CONS, prompt for name via literal-entry mode. If BACK/EVAL, discard the buffer but preserve the mission resume slot so the player can come back. - If buffer has no contract (free snippet): prompt for name (default = current buffer name). Write to deck-state snippet blob. Update snippet index.
- Free the 16 KB editor arena. If the mission is still active, resume Mission Board; otherwise return to home screen.
Teardown is idempotent
Section titled “Teardown is idempotent”If a cartridge is removed (detect-pin goes low) while nEmacs is open on a mission buffer, the nOSh runtime forces SYS with discard=true. The mission enters the suspended state (phase_chain preserved per ADR-capability-model). Player sees a toast on Row 21: CART REMOVED — MISSION SUSPENDED. Then nEmacs is torn down.
Palette ranking
Section titled “Palette ranking”nEmacs’s palette is the ADR-0009 v1 static model, with the specific weight wiring clarified below. The brief’s +5 (cartridge domain vocab), +3 (buffer-local identifier), and +2 (recency) numbers are the ADR-0009 constants (DOMAIN_BOOST = 5; LOCAL_BOOST = 3; RECENCY_BOOST = value 0–10 decaying, floor 2 in the common case of “used recently within last 10 tokens”).
Signal pipeline
Section titled “Signal pipeline” Candidate universe ├── BUILTINS (static) ├── CARTRIDGE_VOCAB ← resolved via Cipher domain-query hook (see §Cross-capability hooks) ├── USER_DEFINED_IN_BUFFER ← AST-walk at every palette refresh └── SESSION_HISTORY ← last 20-token recency ring │ ▼ Legal filter (hard constraint, ADR-0009 §Legal-Form Filter) │ ▼ Score pass: +DOMAIN_BOOST (5) if token ∈ currently-loaded-cart vocab +LOCAL_BOOST (3) if token ∈ visible bindings at cursor +RECENCY (0–10) by age in session ring (decays; floor 0) +POPULARITY (0–4) per ADR-0009 table +SEMANTIC_BONUS (1) if context_stack matches context-specific rules │ ▼ Sort (score desc, then alphabetic) │ ▼ top 8 → palette ranks 9–24 → scrollable, accessed with PAD up/down in PAL modePer-signal resolution
Section titled “Per-signal resolution”- Cartridge domain vocabulary (+5): Queried through a Cipher-published API:
cipher_domain_tokens(cart_id) -> const char **. nEmacs consults the currently-loaded cartridge’s id fromnosh_api->deck_state()->cartridge_history. When a mission buffer is active, the mission’s:grantslist (per ADR-0007) narrows this — nEmacs only boosts tokens from carts whose data the mission grants access to. This means a mission that grants:ice-breaker :black-ledgerboosts tokens from BOTH carts’ vocabularies (+5 each), while a mission that grants no cart data only boosts builtins. - Buffer-local identifier boost (+3): nEmacs maintains
local_bindings_indexhash; it is rebuilt lazily on every AST mutation. Scope visibility follows Lisp let/lambda rules — a binding is “local and visible” if the cursor is inside the scope of a(let ((binding ...))or(lambda (binding ...) ...)form. - Recency (+2 typical, 0–10 total): 20-entry FIFO of token ids inserted this session. Recency score is
max(0, 10 - age)where age is the position from the most-recent end. The brief’s “+2” corresponds to a token used ~8 positions back; fresh tokens score higher; tokens aged out of the ring score 0. - Static popularity prior: The fixed table in ADR-0009 (if=+4, let=+4, lambda=+3, etc.). Not player-alterable.
Palette refresh triggers
Section titled “Palette refresh triggers”The palette re-ranks on:
- Cursor move (CAR, CDR, BACK, descent into drilled node)
- AST mutation (insertion via palette, literal-entry, grab paste, NIL deletion)
- Entry into PAL mode (in case cart vocab changed since last visit; rare)
cipher_vocab_changedevent fired by Cipher voice / cartridge loader (e.g., a mission accepted adefdomainexpansion mid-session)
Scripted-mission flow
Section titled “Scripted-mission flow”A scripted mission is a Mission Board contract that requires the player to author a Lisp expression satisfying an acceptance contract (ADR-0007). The flow from Mission Board through nEmacs to acceptance is:
Sequence
Section titled “Sequence”1. Mission Board accepts a scripted contract via EVAL.2. Mission Board → nemacs_load_mission(mission_id, buffer_seed, contract_fn, error_scope_fn, grants[]): - mission_id: opaque mission handle - buffer_seed: initial AST (may be empty, skeleton, or type-hint signature) - contract_fn: predicate lambda (script_output, mission_input) -> (pass | fail(clauses)) - error_scope_fn: optional mapping of failure clause → AST node-id set (for highlighting) - grants: list of :cartridge-data tokens per ADR-00073. nEmacs transitions home → editor mode. Active cart kept loaded (mission context needed).4. Player edits. Palette reflects mission grants (only :granted cart vocab gets +5).5. Player presses EVAL on the top-level form (defn or lambda).6. nEmacs: a. Walks the AST to the enclosing top-level form. b. Allocates or wakes REPL arena (24 KB). Peak concurrent: cart (~16–32) + REPL (24) + editor (16) ≤ 72 KB. Safe on 520 KB. c. Calls repl_eval_form(ast_form, grants) → returns (script_output, err). d. If repl returned an error (syntax, arity, grants-violation, timeout, OOM): - nEmacs highlights the last-touched node (best-effort) via error_scope. - Row 21 shows err message. - REPL arena freed. Buffer unchanged. e. If repl returned a value: - nEmacs calls contract_fn(script_output, mission_input). - contract_fn returns (pass) or (fail clauses). - On pass: nEmacs fires nemacs_emit_mission_solved(mission_id). Mission Board awards credits + rep, plays Cipher debrief (see §Cross-capability hooks → Cipher), tears down nEmacs. - On fail: nEmacs receives failure clauses + (optionally) error-scope node-id sets from error_scope_fn(clauses). Highlights those nodes, writes clause list to Row 21, palette re-filters to legal-AND-potentially-helpful tokens. Player iterates. f. REPL arena freed in all cases except when debugger-hold flag set (post-v1).7. Player eventually presses SYS: - If pass already fired: teardown only. - If not: prompt for "save as snippet" or "suspend mission" or "discard".Acceptance-contract evaluation — error scoping
Section titled “Acceptance-contract evaluation — error scoping”error_scope_fn is an OPTIONAL helper supplied by the mission author. Signature:
error_scope_fn(fail_clauses) -> list of {clause_id, ast_node_id_set}This lets the author tell nEmacs: “clause :no-extras? relates to nodes in the filter predicate subtree, so highlight those.” Without error_scope_fn, nEmacs highlights the entire top-level form and relies on Row 21’s clause list for guidance. Highlighted nodes render with surrounding box glyphs per ADR-0008 Mock 3.
Cartridge-swap interaction
Section titled “Cartridge-swap interaction”A multi-phase mission (per capability model spec) can require a cartridge swap between phases. If the player is mid-edit in nEmacs on Phase 1’s scripted sub-task and completes it, nEmacs fires mission_solved(mission_id, phase=1). Mission Board may then prompt for a Hot Swap:
> PHASE 1 SCRIPT ACCEPTED.> PHASE 2 REQUIRES: BLACK LEDGER.> INSERT MODULE: BLACK LEDGER.nEmacs MUST tear down before the swap (the detect-pin lowering on removal forces it anyway; this is the graceful path). On Phase 2 re-entry, Mission Board may re-invoke nemacs_load_mission with a new buffer (Phase 2’s contract). The Phase 1 buffer is NOT carried over — it was a sub-task artifact. This preserves arena budgets and keeps the phase-chain API clean.
Snippet library
Section titled “Snippet library”Per ADR-0002 v1.0 sign-off, the snippet library ships in v1. This design specifies the storage format and cross-deck portability.
Storage format
Section titled “Storage format”Snippets live in a dedicated flash region within deck state (separate page from operator handle + credits + cartridge_history).
Snippet index entry (32 bytes each, up to 32 entries = 1 KB): char name[20]; /* name, null-terminated, ASCII CP437 */ uint32_t blob_offset; /* byte offset into snippet blob region */ uint16_t blob_size; /* bytes, including trailing 0 */ uint8_t flags; /* bit 0 = authored-here, 1 = received-via-link, 2 = derived-from-mission */ uint8_t reserved[5];
Snippet blob region: Canonical Lisp source text (not bytecode, not serialized AST). UTF-8/CP437 hybrid allowed. Reason: text is portable, parseable by any compatible VM, and debuggable without the originating deck's arena state.The choice of canonical Lisp source over serialized AST matters for link-cable portability and future VM revision: two decks running different nOSh runtime versions can still share a snippet as long as the reader’s VM accepts the source.
Naming
Section titled “Naming”Snippet names are player-chosen, 20 ASCII characters max, unique per deck. The nOSh runtime rejects duplicates at save time. No namespacing at v1; the Relay module (per capability-model spec) could introduce namespacing if snippet distribution becomes a thing post-v1.
Cross-deck portability via link
Section titled “Cross-deck portability via link”Two decks linked over the TRRS link protocol (platform_link_* API, per capability-model spec) can exchange snippets:
- Sending deck:
LINK > SEND SNIPPET > <name>in SYS menu. Snippet blob + name sent to peer. - Receiving deck: sees
LINK RECV: SNIPPET "<name>". PromptedACCEPT? CONS=yes BACK=no. On accept, entry written to receiver’s snippet index withflags.bit 1 = 1.
Cross-deck snippets retain no provenance chain entry of their own (the cartridge provenance chain is per-cart, not per-snippet). Instead, the flags bit distinguishes “authored here” from “received via link” for player awareness.
Snippet retrieval into nEmacs
Section titled “Snippet retrieval into nEmacs”From home screen: SYS > SNIPPETS > <name> opens nemacs_open_snippet(name). The snippet is deserialized into the editor arena. Edits may be saved back (overwriting) or saved as a new snippet.
Cross-capability hooks
Section titled “Cross-capability hooks”This section is the interoperation contract with the other three agents working in the same wave. Each subsection specifies: published API from nEmacs to peer, consumed API from peer to nEmacs, UX flow, and permission boundaries.
← Mission Board (scripted-mission buffer load; acceptance-contract API; save/resume)
Section titled “← Mission Board (scripted-mission buffer load; acceptance-contract API; save/resume)”Events Mission Board FIRES that nEmacs CONSUMES:
/* Fired when player accepts a scripted contract on the Mission Board. */void nemacs_load_mission( mission_id_t mission_id, const ast_node_t *buffer_seed, /* may be NULL for "empty template" */ contract_fn_t contract_fn, /* predicate (script_out, mission_in) -> pass|fail */ error_scope_fn_t error_scope_fn, /* optional; may be NULL */ const cart_id_t *grants, /* null-terminated list of granted cart ids */ mission_input_t *mission_input /* passed by reference to contract_fn on EVAL */);
/* Fired if Mission Board needs nEmacs to abort (e.g., cart ejected). */void nemacs_force_teardown(teardown_reason_t reason);
/* Fired for mid-edit mission updates (rare; e.g., phase clock running out). */void nemacs_mission_tick(mission_id_t id, tick_kind_t kind);Events nEmacs FIRES that Mission Board CONSUMES:
/* Fired on EVAL when contract_fn returns (pass). */void mission_board_on_script_pass(mission_id_t id, const lisp_value_t *output);
/* Fired on EVAL when contract_fn returns (fail clauses) — Mission Board decides whether to count the attempt (e.g., against retry counter). */void mission_board_on_script_fail(mission_id_t id, const fail_clauses_t *clauses);
/* Fired on SYS with "suspend" choice. */void mission_board_on_script_suspended(mission_id_t id, const buffer_snapshot_t *snap);
/* Fired on SYS with "discard" choice after a failed pass. */void mission_board_on_script_abandoned(mission_id_t id);Permission boundary: nEmacs never writes to Mission Board internal state. Mission Board never writes to nEmacs’s AST after nemacs_load_mission has returned. The only shared mutable state is the resume slot in deck-state flash, and nEmacs owns writes to it while Mission Board owns reads.
UX flow (happy path): Mission Board → select contract → EVAL → nemacs_load_mission(...) → editor opens with seed → player edits → EVAL → contract passes → mission_board_on_script_pass(...) → Mission Board triggers Cipher debrief + credit/rep update + nEmacs teardown → returns to Mission Board.
UX flow (suspension): Mission Board → nemacs_load_mission(...) → player edits → SYS → “suspend” → mission_board_on_script_suspended(...) → nEmacs persists resume slot → Mission Board shows the mission in “SUSPENDED” state on the board. Later: Mission Board → SUSPENDED mission → EVAL → nemacs_load_mission(...) with the snapshot as buffer_seed.
← Cipher voice (domain vocabulary propagation; ambient commentary)
Section titled “← Cipher voice (domain vocabulary propagation; ambient commentary)”Events Cipher FIRES that nEmacs CONSUMES:
/* Fired when cartridge load/unload changes domain vocabulary. */void nemacs_on_vocab_changed(const cart_id_t *loaded_carts);
/* Queryable, not fired: nEmacs asks Cipher for the current vocab for a given cart. */const char **cipher_domain_tokens(cart_id_t cart_id);Events nEmacs FIRES that Cipher CONSUMES (opt-in ambient commentary):
/* Low-frequency, debounced. Cipher may or may not say something. */void cipher_on_editor_milestone(editor_milestone_t kind, const char *context);/* kinds: FIRST_LAMBDA_DEFINED, FIRST_RECURSION, LONG_EDIT_NO_EVAL, CONTRACT_FAILED_3_TIMES */
/* Fired on EVAL pass — Cipher owns the actual debrief script. */void cipher_on_mission_script_pass(mission_id_t id, const lisp_value_t *output);Ambient commentary policy: nEmacs does NOT compose Cipher lines. It only fires milestone events. Cipher decides whether to render a line to Row 21 (editor’s local status) or defer. Cipher has veto over cadence — a player who has been editing for 20 minutes should not get chatty commentary every 30 seconds. The capability-model spec’s Cipher voice policy (terse, observational, does not moralize) applies.
Permission boundary: Cipher never mutates the AST or palette. nEmacs never writes to Cipher’s LFSR state. The vocabulary table is owned by Cipher (built from cartridge defdomain forms at load time) and is read-only from nEmacs’s perspective.
UX flow — domain vocab propagation on cart load: Cipher on cartridge insert → parses defdomain forms → builds vocab table → fires nemacs_on_vocab_changed(loaded_carts) → if nEmacs is open, it flushes cached palette candidates and schedules re-rank on next palette refresh trigger. If nEmacs is not open, the event is a no-op; nEmacs will query on next launch.
UX flow — ambient comment on a long edit: Player in nEmacs for 5 minutes without EVAL → nEmacs fires cipher_on_editor_milestone(LONG_EDIT_NO_EVAL, "filter-list.lsp") → Cipher may render on Row 0’s right-half: > STILL WORKING. NO JUDGMENT. (Editorial tone per capability-model spec Cipher style guide.)
← dev REPL (EVAL dispatch; snippet library sharing)
Section titled “← dev REPL (EVAL dispatch; snippet library sharing)”Events nEmacs FIRES that REPL CONSUMES:
/* Dispatch a form for evaluation. Blocks until repl returns. Allocates repl arena on entry if not already allocated. */repl_result_t repl_eval_form( const ast_node_t *form, const cart_id_t *grants, /* NULL = REPL's normal FFI; else mission scope */ uint32_t timeout_ms, /* ADR-0007 default 1000 ms */ uint32_t mem_limit_bytes /* ADR-0007 default 8192 B (mission script) */);
/* Query REPL history without entering REPL (read-only). */const lisp_value_t *repl_history_peek(uint8_t index); /* 0 = most recent */Events REPL FIRES that nEmacs CONSUMES:
/* REPL emits this when the player uses the REPL's "save-as-snippet" action on a history entry. nEmacs writes to its snippet index (shared storage). */void nemacs_snippet_save_from_repl(const char *name, const char *source_text);
/* REPL requesting a browsable snippet list — nEmacs supplies names. */const snippet_index_entry_t *nemacs_snippet_list(uint8_t *count);Shared storage: snippet library is owned by nEmacs but readable and writable by REPL. REPL’s “save this history expression as a snippet” action and REPL’s “load snippet to history” action both go through nEmacs’s snippet index API. This keeps one source of truth and prevents divergent blob formats.
Arena coexistence during EVAL dispatch: the EVAL dispatch is the one moment in the system where cart arena (16–32 KB) + REPL arena (24 KB) + editor arena (16 KB) are all allocated simultaneously. Peak = ~72 KB. Post-dispatch, REPL arena is freed unless the player has explicitly entered REPL mode (e.g., for interactive debugging of a failed script, post-v1.1).
Permission boundary: REPL never mutates the nEmacs AST. nEmacs never reads REPL’s working memory (only its return value). The grants parameter on repl_eval_form enforces ADR-0007 FFI tiering — the REPL arena runs with restricted FFI for mission-scoped dispatches, full FFI for free-play REPL dispatches.
UX flow — EVAL from editor to REPL: Player in nEmacs presses EVAL → nEmacs finds enclosing top-level form → calls repl_eval_form(form, mission_grants, 1000, 8192) → REPL allocates arena, compiles, runs, returns value or error → nEmacs consults contract_fn (if mission context) or displays result on Row 21 (if free-form) → REPL arena freed → editor continues.
UX flow — REPL-authored snippet shared to editor: Player in REPL composes a useful function → presses “save as snippet” → supplies name → REPL calls nemacs_snippet_save_from_repl(name, source) → nEmacs writes to flash → later, from editor home, player opens the snippet via nemacs_open_snippet(name).
Open questions / contradictions
Section titled “Open questions / contradictions”1. CONTRADICTION: palette rows 23–25 (ADR-0008) vs action-bar row 24 (CLAUDE.md)
Section titled “1. CONTRADICTION: palette rows 23–25 (ADR-0008) vs action-bar row 24 (CLAUDE.md)”This is the row-layout reconciliation the brief flagged. ADR-0008’s mockups describe a “bottom 3 rows (rows 23–25)” palette strip. The CLAUDE.md Canonical Hardware Specification (non-negotiable per Spec Hygiene Rule 5) mandates Row 24 as the nOSh runtime action bar and Rows 1–23 as cartridge/editor content. Rows 24 AND 25 cannot belong to the palette.
Resolution adopted in this design:
- Palette occupies Rows 22–23 (2 rows, 8 slots: 4 per row × 16 cols each) — still within the content area.
- The third palette row from ADR-0008 (which duplicated key hints like “CONS: insert | CAR: descend | BACK: parent”) is moved into Row 24 (firmware action bar) via the
nemacs_publish_status_hintAPI. nOSh renders key hints using nEmacs’s current-mode hint struct. - Row 21 becomes local editor status (path-from-root, depth, literal-entry echo, error banner) — a role ADR-0008 placed in “row 22” while also saying “row 22 = status bar”. This design promotes that consistently to Row 21.
Action required on ADR-0008: ADR-0008’s “Accepted (v1.0)” status should be amended with a superseding note pointing at this design, OR ADR-0008’s mockups should be updated to the Rows 1–23 regime (preferred, since ADR-0008 is still “Accepted” per its header but predates 2026-04-14 spec hygiene work). Flag for PM.
Status field discrepancy: ADR-0008 claims “Accepted (v1.0)” for nEmacs, but ADR-0002 v1.0 (2026-04-14) says “nEmacs slips post-launch.” If nEmacs is slipping, its ADR should probably be “Accepted pending scheduling” or explicitly marked as post-v1 scope. This is a documentation hygiene issue for PM review.
2. 31-key assumption: ADR-0008 mentions “PAD 2/4 to scroll palette”
Section titled “2. 31-key assumption: ADR-0008 mentions “PAD 2/4 to scroll palette””CLAUDE.md canonical spec lists 31 physical keys (14 function + 16 numpad + 1 context-sensitive TERM). ADR-0008 references “PAD arrows” and “PAD 2/4” for palette scroll. The numpad is 16 keys, but “PAD 2/4” implies a directional pad. Open question: are PAD-style directional scrolls dedicated keys, or are they alternate functions on numpad 2 / 4 / 6 / 8 when in PAL mode? This design assumes the latter (numpad 2/4/6/8 act as up/left/right/down arrows while in PAL mode, repurposing digit slots 2 and 4 which otherwise select palette slots 2 and 4). This creates an ergonomic conflict: in PAL mode, numpad 2 currently selects palette slot 2 AND scrolls the palette? Resolution proposal:
- In PAL mode: numpad digits select palette slots (1–8). Scrolling uses
QUOTE(unused in PAL) for PAGE-UP andLAMBDA(which currently swaps to LIT mode) for PAGE-DOWN.LAMBDAdual-use is acceptable if it only swaps to LIT when palette is NOT scrolled OR if the PAGE-DOWN affordance is bound to a different key. - Alternative: wait for the TERM key to be finalized and assign one direction to it.
Flag for PM to route to Input System architect.
3. Structural-vs-textual rendering: bracket glyphs
Section titled “3. Structural-vs-textual rendering: bracket glyphs”ADR-0008 says “parens are rendered as box-drawing glyphs” but doesn’t commit to a specific CP437 subset. The font doc (KN-86-Character-Set-and-Font-Architecture.md) is Layer 1 256-glyph code page including CP437 box-drawing. This design proposes a minimal bracket glyph set:
| AST shape | Left glyph | Right glyph |
|---|---|---|
| Top of form (depth 0–1) | ┌─ | ─┐ |
| Continued form (depth > 1) | ├─ | ─┤ |
| Leaf list bracket | └─ | ─┘ |
| Quoted form | ╓─ | ─╖ |
| Grab-region highlight | ▓▓ | ▓▓ (reverse video) |
| Error scope | ╌ ┌─ | ─┘ ╌ |
Open question for PM: should this glyph mapping be promoted to the UI Design System doc, or stay as an editor-local convention? I recommend promoting to UI Design System §6 (new “Structural Lisp rendering”) so that hypothetical other consumers (REPL history replay, mission debrief showing “here’s the solution”) use consistent visuals.
4. Literal-entry multi-tap map
Section titled “4. Literal-entry multi-tap map”The multi-tap scheme is under-specified in ADR-0008 Mock 4. This design assumes phone-keypad classic (2=ABC, 3=DEF, 4=GHI, 5=JKL, 6=MNO, 7=PQRS, 8=TUV, 9=WXYZ, 0=space+symbols, with . switching to string mode and ENT confirming). Open question: does KN-86 have an official multi-tap layout yet? If not, this design’s adoption of the phone-keypad classic is a proposal — flag for gameplay design to ratify or override.
5. EVAL dispatch ownership: does the REPL actually have to be “entered”?
Section titled “5. EVAL dispatch ownership: does the REPL actually have to be “entered”?”ADR-0002 describes REPL as a top-level firmware utility reachable from home screen. This design treats the REPL’s arena + evaluator as a service that nEmacs can invoke without the user “entering” the REPL UI. This is a departure from a strict reading of ADR-0002, where REPL is a user-facing subsystem. Rationale: the Lisp evaluator is a resource, and the REPL UI is one consumer of it. Having nEmacs also consume the evaluator via repl_eval_form is cleaner than duplicating the evaluator inside the editor. Flag for PM to confirm with dev REPL design agent that this API shape is acceptable. If the dev REPL agent designs a REPL where the evaluator is tightly coupled to the REPL UI state, we need to either refactor this to have a shared eval_core subsystem or duplicate the evaluator (the latter is worse for the arena budget — duplication costs code pages even if working arenas are shared).
6. Snippet library arena pressure
Section titled “6. Snippet library arena pressure”The snippet library index is 32 entries × 32 B = 1 KB. Blob region sized arbitrarily. Open question: how much flash is actually allocated to snippets? Deck state is 4 KB per the capability-model spec. Snippet index + blobs need their own flash region separate from the DeckState struct. Flag for embedded-systems review to carve a snippet region (suggest 16 KB → ~100 snippets at average 128 B each, or 8 KB at 32-entry limit).
7. How does a mission SEED a buffer non-trivially?
Section titled “7. How does a mission SEED a buffer non-trivially?”The brief and ADR-0008 both say missions can seed a buffer, but the AST format for buffer_seed isn’t defined. Proposed format: mission templates carry a (:seed-script <lisp-source>) form at authoring time; Mission Board parses this source into an AST at contract-accept time and passes the AST pointer as buffer_seed. Flag for gameplay design review since it affects the mission template authoring UX.
8. Where does “grab across different parts of the tree” end up? (ADR-0008 Open Q §1)
Section titled “8. Where does “grab across different parts of the tree” end up? (ADR-0008 Open Q §1)”ADR-0008’s Open Q §1 asks whether grab can span disjoint tree regions. This design answers: v1 grab is contiguous — grab expands via CAR (first child) and CDR (next sibling), both of which stay within the current subtree root. Disjoint grabs are v1.1. Resolution is simple enough to ratify here, but flagged for formal ratification by PM + gameplay design.
9. Cipher vocabulary narrowing by mission grants
Section titled “9. Cipher vocabulary narrowing by mission grants”When a mission grants :ice-breaker only, should nEmacs boost all loaded carts’ vocab or only the granted cart’s vocab? This design chose: narrow to granted carts. That matches the ADR-0007 intent (scripts shouldn’t accidentally pick up out-of-scope domain terms). But it means a player mid-mission sees fewer cart-specific palette candidates than if they were browsing freely. Flag for gameplay design to validate this tradeoff.
End of design — nEmacs structural editor, post-v0.1 wave. No code. Ready for PM synthesis into the four-capability interaction plan.