ADR-0006: Cartridge Format v2.0 Specification
Supersedes spike: former spikes/ADR-0001-cart-format-v2.md
Supersedes format: Implicit v1.1 format (native ARM object code, never shipped)
Related: ADR-0001-embedded-lisp-scripting-layer.md, ADR-0004-vm-selection.md, ADR-0005-ffi-surface.md, ADR-0013-cartridge-physical-format.md (superseded by ADR-0019), ADR-0015-cipher-line-auxiliary-display.md, ADR-0019-cartridge-storage-and-form-factor.md, ADR-0035-trackpoint-cart-ffi.md (defines the :pointer-hidden manifest flag; §F2 points the schema home here)
Summary
Section titled “Summary”.kn86 v2.0 is a binary container format for KN-86 cartridges. It holds:
- Header: metadata, version, required API version, capability declaration
- Code section: Lisp source, parsed and tree-walked on load. (Originally framed as “compiled Lisp bytecode (via Fe compiler).” Fe is a tree-walking interpreter — no bytecode is produced or shipped today. The section name is retained as the forward-compatible reservation for deferred AOT bytecode. See the 2026-06-14 amendment.)
- Static data section: sprite bitmaps, PSG patterns, strings, mission templates, cart-capabilities block (added 2026-04-24; see §Cart-Capabilities Block)
- Optional debug section: source code, line tables, symbol names (strippable)
- Checksum/signature: integrity verification (modest, not cryptographic)
The format is portable, inspectable, and moddable. A cartridge is self-describing: a tool can read the header, determine what NoshAPI version is required, extract debug info, or strip it for production.
File Structure
Section titled “File Structure”┌─ .kn86 Cartridge Image (little-endian, all multi-byte values) ─┐│ │├─ HEADER (80 bytes, fixed) ││ Magic: "KN86" (4 bytes) ││ Version: 2 (uint16) + _reserved1 (uint16) ││ Cart ID: unique identifier (uint32) ││ Capability type: e.g., "NETWORK_INTRUSION" (32 bytes) ││ Declared req_api_version: e.g., 0x0201 (uint16) ││ Declared req_vm_version: e.g., 0x0100 (uint16) ││ Bytecode offset + size: (2 × uint32) ││ Static data offset + size: (2 × uint32) ││ Debug section offset + size: (2 × uint32) — 0s if none ││ Checksum: CRC-32 (uint32) ││ Reserved (4 bytes): padding for future use ││ │├─ CODE SECTION (variable, aligned to 4-byte boundary) ││ Raw .lsp Lisp source text (tree-walked; AOT bytecode ││ deferred). Field names keep the "bytecode_" prefix (ABI). ││ Size: bytecode_size (from header) ││ │├─ STATIC DATA SECTION (variable, aligned to 4-byte boundary) ││ Sprites, PSG patterns, strings, mission templates ││ Organized as sub-sections with type tags ││ Size: static_data_size (from header) ││ │├─ DEBUG SECTION (optional, aligned to 4-byte boundary) ││ Source code, line tables, symbol names ││ Only present if debug_section_size > 0 ││ │├─ CHECKSUM (4 bytes, optional) ││ CRC-32 of [HEADER..DEBUG], for integrity check ││ Stored at end or in reserved area ││ │└─────────────────────────────────────────────────────────────┘Section Details
Section titled “Section Details”Header (80 bytes, little-endian)
Section titled “Header (80 bytes, little-endian)”struct CartridgeHeaderV2 { uint8_t magic[4]; /* "KN86" (0x4B 0x4E 0x38 0x36) */ uint16_t version; /* 2 (for v2.0) */ uint16_t _reserved1; /* Alignment padding */
uint32_t cart_id; /* Unique ID: CRC of (program_name + module_class) or author-assigned */
char capability_type[32]; /* e.g., "NETWORK_INTRUSION\0" */ /* Describes the cartridge's domain */
uint16_t req_api_version; /* Minimum NoshAPI version required (e.g., 0x0201 = v2.1) */ uint16_t req_vm_version; /* Minimum Fe VM version (e.g., 0x0100 = v1.0) */
uint32_t bytecode_offset; /* Byte offset from start of file to bytecode section */ uint32_t bytecode_size; /* Size in bytes of bytecode blob */
uint32_t static_data_offset; /* Byte offset to static data section */ uint32_t static_data_size; /* Size in bytes of static data */
uint32_t debug_offset; /* Byte offset to debug section (0 if none) */ uint32_t debug_size; /* Size in bytes (0 if no debug section) */
uint32_t checksum; /* CRC-32 of header + all sections (0 if not checked) */
uint32_t _reserved2; /* Future use */
/* Total: 80 bytes */};Interpretation:
- magic: Always “KN86” (0x4B 0x4E 0x38 0x36 in hex). Identifies file type.
- version: Format version. 2 for this spec. Future versions may add sections/fields.
- cart_id: Unique cartridge identifier. Can be auto-generated (e.g., CRC of name) or author-assigned. Used for save-game isolation and mod tracking.
- capability_type: Domain description (string, null-terminated, max 31 chars). e.g., “NETWORK_INTRUSION”, “SONAR_DIVING”, “FORENSICS”, “CRYPTO”. Primarily for UI (player sees “Network Intrusion” in mission board).
- req_api_version: Semver-style uint16: high byte = major, low byte = minor. e.g., 0x0201 = v2.1. The nOSh runtime checks: if (nosh_api_version < req_api_version) reject cartridge.
- req_vm_version: Fe VM version required. e.g., 0x0100. Allows VM improvements without breaking old carts.
- bytecode_offset, bytecode_size: Define the bytecode blob location and size.
- static_data_offset, static_data_size: Define static data location and size. (Amended 2026-04-22 per ADR-0013; re-amended 2026-04-24 per ADR-0019, which supersedes ADR-0013.) The
static_data_sizeceiling is bounded by the physical cartridge’s storage capacity (the SD card inside the ADR-0019 sled, gigabyte-scale), not by nOSh runtime memory. Earlier size estimates assumed the cart image had to fit in nOSh runtime RAM; that constraint no longer holds. The nOSh runtime reads static data on demand from the cartridge’s mounted SD filesystem (under ADR-0019), via standardread()calls; the previous interim resolution under ADR-0013 was MBC5 bank-switching from cartridge ROM, now obsolete. - debug_offset, debug_size: Define debug section (optional). If debug_size == 0, no debug section is present.
- checksum: CRC-32 of all data [header..debug]. Set to 0 if not computed. On load, the nOSh runtime can optionally verify (cost: ~100 µs on Pico 2).
Code Section (the “bytecode section”) — detail
Section titled “Code Section (the “bytecode section”) — detail”Format (current): Raw Fe-readable .lsp Lisp source text. No bytecode — Fe is a tree-walking interpreter (ADR-0004 2026-06-14 amendment) and the loader evaluates the source directly.
Deferred (AOT bytecode, parked): Were a future Fe AOT compiler ever built (design at kec-lisp/docs/bytecode-vm.md, gated on the ADR-0004 revisit triggers), the desktop tool would take .lsp Lisp source and output:
- Instruction stream: Fe bytecode instructions
- Constant table: literals (numbers, symbols, strings) referenced by instructions
- Symbol table: (optional, for debugging) maps symbols to indices
…plus a bytecode verifier (bytecode would then cross the cart trust boundary).
Details (current):
- No header — raw
.lspsource text - Size given by header’s
bytecode_size(field name kept for ABI stability) - The nOSh runtime reads this section and
fe_read+ evaluates each top-level form - Fe evaluator: initialize arena, set up builtins (NoshAPI bindings), evaluate the source on the tree-walker
Build flow (current):
source.lsp → kec build (inline loads, parse-check) → raw source in code section → .kn86Build flow (deferred AOT):
source.lsp → Fe AOT compiler → bytecode + constants → packager → .kn86Device loading flow:
.kn86 file → read header → validate version → read code section into arena → fe_read + eval (tree-walk)Note: Fe does not require a separate “compile to bytecode” step — the reference Fe implementation reads source directly, and that is what ships. For .kn86 v2.0, the two historical options were:
- Store source in bytecode section: ship
.lsptext directly. Advantage: interpreter reads it at load. Disadvantage: larger carts (~5–10 KB per cart for typical size). - Custom bytecode format: define a bytecode instruction set, modify Fe to consume it. Advantage: ~30% smaller carts. Disadvantage: more implementation work.
Recommendation: Option 1 for MVP (ship source). Option 2 (bytecode compiler) as Phase 2 optimization. Both are compatible with the format.
Static Data Section
Section titled “Static Data Section”Organized as tagged sub-sections. Each sub-section has a type tag, size, and payload.
Static Data Section Layout:
[Type:SPRITES] [Size:4096] [Bitmap Data...][Type:PSG_PATTERNS] [Size:512] [Pattern Data...][Type:STRINGS] [Size:1024] [String Table...][Type:MISSIONS] [Size:2048] [Mission Template Data...][Type:END] [Size:0]Sub-section header (8 bytes per subsection):
struct StaticDataSubsection { uint32_t type; /* Tag: SPRITES=1, PSG_PATTERNS=2, STRINGS=3, MISSIONS=4, CART_CAPABILITIES=5, END=0 */ uint32_t size; /* Bytes of payload (not including this header) */ /* Payload (size bytes) follows immediately */};Sub-section types:
| Type | Tag | Payload format | Example usage |
|---|---|---|---|
| SPRITES | 1 | Raw bitmap data. Sprites are packed 1 bpp, row-major. Each sprite has a metadata entry in the section header (width, height, offset into bitmap blob). | Cell display, UI graphics |
| PSG_PATTERNS | 2 | PSG register dump sequences. Format: [pattern_id (2B), register_sequence…]. Used for pre-canned sounds (horn, alert, etc.). | SFX assets |
| STRINGS | 3 | String table: [string_id (2B), length (2B), null-terminated string…]. Cartridge code references strings by ID. | Mission templates, UI text |
| MISSIONS | 4 | Contract schemas (defcontract-schema forms). Each sub-section payload is one or more Fe-compiled defcontract-schema declarations: required-capabilities, threat-range, phase-count, noun-generators, condition-generators, payout-formula symbol, ttl-range, opportunity-weight. Note (2026-05-03 per ADR-0028): the tag name “MISSIONS” is preserved for backward compatibility, but the payload semantics shifted from “pre-baked mission templates the runtime selects” to “schemas Mission Control runs at generation time.” See software/runtime/mission-control.md §3.2 and the ADR-0006 amendment dated 2026-05-03. | Mission Control contract generation |
| CART_CAPABILITIES | 5 | Length-prefixed list of ASCII capability keywords the cart requests (see §Cart-Capabilities Block for full serialization). Omit the subsection when no capabilities are requested — this is the v0.1 default for every launch cart except Null. | Capability-flag system (ADR-0015 §3a) |
| END | 0 | No payload (size=0). Marks end of static data section. | (terminator) |
Design rationale: Tagged sections allow the nOSh runtime to skip unknown types (for forward compatibility) and cartridges to include only what they need (no bloat from unused asset types).
Example (ICE Breaker cartridge, no capabilities):
[Type:SPRITES] [Size:2048] [network_diagram, threat_meter, grid_bg, ...][Type:PSG_PATTERNS] [Size:256] [alarm_pattern, hack_complete_sting, ...][Type:STRINGS] [Size:512] ["CONTRACT_EXTRACT", "NETWORK_INTRUSION", ...][Type:MISSIONS] [Size:1024] [mission_meridian_extract, mission_cascade_vault, ...][Type:END] [Size:0]Example (Null cartridge, one capability):
[Type:SPRITES] [Size:1024] [cipher_debug_glyphs, ...][Type:STRINGS] [Size:512] ["CIPHER_ANALYSIS", ...][Type:MISSIONS] [Size:512] [mission_cipher_analysis, ...][Type:CART_CAPABILITIES] [Size:26] [count:1, _reserved:0, "cipher-main-grid-escape"][Type:END] [Size:0]Debug Section (Optional)
Section titled “Debug Section (Optional)”Present only if debug_size > 0 in header. Omitted in production carts to save space.
Debug Section Layout:
[Header] Type: "DEBUG_v1\0" (8 bytes) Line table size (uint32) Symbol table size (uint32) Source size (uint32)
[Line Table] Maps bytecode instruction offset → source line number Format: [instr_offset (uint32), line_number (uint32)]... (repeating until end)
[Symbol Table] Maps symbols to their internal indices Format: [symbol_hash (uint32), name_length (uint16), name (variable)]... (repeating)
[Source Code] Original .lsp source (uncompressed text) May be truncated if exceeds budgetUsage:
- Debugger (future): uses line table to map bytecode PC to source location
- Stack traces: unwind Fe stack, use symbol table to name variables/functions
- Source inspection: developer can re-read original source from cart
Typical debug size: 10–20 KB for a 50-line cartridge (source + tables). Strippable for production.
Dispatch Contract: Handler References
Section titled “Dispatch Contract: Handler References”How does the runtime know whether a cell handler is C code or Lisp code?
Option A: Type-based (simpler)
Cell type is registered with a flag: is_lisp_handler. At dispatch time, check the flag.
// In cell registrystruct CellTypeInfo { uint16_t type_id; size_t cell_size; void *handler_ptr; // C function pointer bool is_lisp_handler; // If true, handler_ptr is actually a Lisp lambda ref};
// At dispatchif (type_info->is_lisp_handler) { invoke_lisp_handler(handler_ptr, args...); // Fe evaluator} else { invoke_c_handler(handler_ptr, args...); // Direct call}Option B: Tagged union (more flexible)
Handler is a union type that explicitly indicates its variant.
typedef union { void (*c_fn)(void *); // C handler uint32_t lisp_lambda_ref; // Lisp handler (opaque ref into Fe arena)} Handler;
struct CellTypeInfo { uint16_t type_id; size_t cell_size; Handler handler; HandlerType handler_type; // HANDLER_C or HANDLER_LISP};Recommendation: Option A (simpler). At cartridge load time, the cart_init function registers cell types:
;; In cartridge .lsp(register-cell-type 'contract :fields [...] :handlers {:on-car my-contract-handler, ...} :is-lisp #t)The cartridge Lisp explicitly declares which handlers are Lisp and provides lambda references. The nOSh runtime stores this in the registry and dispatches accordingly.
Cart-Capabilities Block
Section titled “Cart-Capabilities Block”Added by the 2026-04-24 amendment following ADR-0015 §3a. Prior to this amendment, no cart could declare privileged capabilities; the only cart that needs any today is Null, which requires cipher-main-grid-escape for its designed gameplay.
Why a static-data subsection, not a header field
Section titled “Why a static-data subsection, not a header field”ADR-0015 §3a commits to a data-not-code mechanism: adding a new capability keyword must not require a nOSh runtime code change, and must not require a .kn86 format version bump. Two candidate placements were considered:
- Carve bytes out of the 80-byte header
_reserved1/_reserved2regions. Rejected — there are only 6 reserved bytes total (2 at 0x06, 4 at 0x4C), insufficient for a variable-length keyword list, and burning them closes a door we may need for future format extensions. - Add a new tagged static-data subsection (
CART_CAPABILITIES = 5). Accepted — the static-data section already uses the tagged-subsection pattern (SPRITES, PSG_PATTERNS, STRINGS, MISSIONS), the nOSh runtime loader already skips unknown tags (per the original §Static Data Section “forward compatibility” note), and adding a subsection is additive rather than space-competitive.
Using a tagged subsection also means the capability block is visible to the existing kn86_inspect tool without requiring a parser upgrade — unknown tags render as [CART_CAPABILITIES] (size N bytes) today and parse structurally once the tool learns the layout. Backward compatibility is free.
Serialization
Section titled “Serialization”CART_CAPABILITIES is a length-prefixed list of capability keywords. Each keyword is an ASCII kebab-case string, 3–31 characters, matching [a-z][a-z0-9-]*. The payload is:
Offset Size Field------ ---- -----0x00 1 count (uint8, 0–15 capability keywords)0x01 1 _reserved (uint8, 0x00; future flag byte)0x02 N₁ keyword[0] (length-prefixed: uint8 len, then len ASCII bytes)... Nᵢ keyword[i] (same length-prefix form)Each keyword entry is:
Offset Size Field------ ---- -----0x00 1 len (uint8, 3–31)0x01 len text (ASCII, no null terminator, no padding)Concrete example — Null’s capability block (one keyword: cipher-main-grid-escape, 23 chars):
[StaticDataSubsection header] type: 0x00000005 (CART_CAPABILITIES) size: 0x0000001A (26 bytes payload: 2-byte header + 1-byte len + 23-byte text)
[Payload] 0x00: count = 0x01 0x01: _reserved = 0x00 0x02: len = 0x17 (23) 0x03..0x19: text = "cipher-main-grid-escape"A cart with no capability requests omits the CART_CAPABILITIES subsection entirely. This is the v0.1 default for every launch cart except Null. It is not an error for the subsection to be present with count = 0; that form is equivalent to omission and exists to let the packager include a deterministic capabilities section even when the list is empty (useful for diff-stable CI artifacts).
Encoding constraints:
- Endianness: N/A — all fields are byte-wide or ASCII.
- Keyword alphabet: lowercase ASCII letters, digits, and hyphen. Case-insensitive matching is not supported;
cipher-main-grid-escapeis not the same token asCipher-Main-Grid-Escape. - Keyword length: 3–31 ASCII bytes inclusive. A
lenbyte outside[3, 31]is a malformed cart and raises:capability-block-malformedat load. - Count ceiling:
count ≤ 15. Above 15, the cart is rejected with:capability-block-malformed. This ceiling is deliberate — the nOSh runtime allowlist is a runtime-baked table, not a runtime-growable list, and 15 slots is comfortably above any projected need. _reservedbyte: must be0x00in v0.1. Reserved for future flag use (e.g., a “required” vs. “optional” bit). Non-zero values are rejected with:capability-block-malformed.- Subsection size: must equal
2 + Σ(1 + len[i])for all i in0..count. Any mismatch is:capability-block-malformed.
The choice of ASCII kebab-case (rather than binary keyword IDs) is deliberate: capability keywords are sparse, debuggable, diffable in text review, and their string cost is trivial against a 12–16 KB typical cart. A binary ID table would couple the nOSh runtime and the cart packager on an ID schedule that has to be maintained in lockstep forever; the ASCII path has zero maintenance cost.
nOSh Runtime Loader Parsing Behavior
Section titled “nOSh Runtime Loader Parsing Behavior”Cart-load adds a new step between existing steps §Loading Semantics step 3 (API-version check) and step 4 (arena allocation). The full sequence, with the new step inlined:
- Read
.kn86header. - Validate magic and version.
- Check
req_api_versionagainst nOSh runtime version; abort if mismatch. 3a. Parse theCART_CAPABILITIESsubsection if present (new, 2026-04-24):- Scan the static-data section (already sized by the header) for a
CART_CAPABILITIESsubsection. Only one instance is permitted per cart; a second instance is:capability-block-malformed. - If absent, the cart requests no capabilities — proceed to step 4.
- If present, validate the payload against the encoding constraints above. On any constraint failure, abort cart-load with
:capability-block-malformed(the reporting path is specified below). - For each declared keyword, look it up in the nOSh runtime’s baked-in allowlist (see §Allowlist below), keyed by the cart’s
cart_idfrom the header. If any declared keyword is not present in the cart’s allowlist entry, abort cart-load with:capability-not-granted(reporting path below). The error reports the first offending keyword by name. - On success, record the granted capability set in the cart’s per-load runtime state. Per-capability FFI gates (e.g.,
cipher-emit-main-gridfrom ADR-0015 §3a) read this set at call time.
- Scan the static-data section (already sized by the header) for a
- Allocate arena (size negotiated: header may hint cart_arena_size, nOSh runtime selects 16–32 KB).
- Load the code section (Lisp source) into the arena.
- Initialize Fe interpreter in arena.
- Load static data section into nOSh runtime memory (separately, not in cart arena).
- Call cartridge
cart_initLisp function:- Register cell types
- Create initial cell structures (UI cells, lists, etc.)
- Seed LFSR
- Return to nOSh runtime
The capability check runs before arena allocation and Fe init specifically so that a rejected cart has zero runtime footprint. No Lisp has executed; no memory has been claimed; the nOSh runtime has done no work beyond parsing structure.
Allowlist
Section titled “Allowlist”The allowlist is a nOSh-runtime-baked table, compiled into the nOSh runtime binary at build time. It is not operator-configurable, not runtime-mutable, and not loaded from disk. Tampering to grant unauthorized capabilities requires reflashing the nOSh runtime.
Source of truth: nosh/capabilities/allowlist.c (filename is proposed; the canonical path is owned by the C Engineer agent’s nOSh runtime implementation PR). The table is a static const array of { cart_id, capability_keyword } pairs.
Data structure (nOSh runtime side):
struct CartCapabilityGrant { uint32_t cart_id; /* matches .kn86 header cart_id */ const char *capability_keyword; /* ASCII kebab-case */};
static const struct CartCapabilityGrant kn86_capability_allowlist[] = { { 0x?????????, "cipher-main-grid-escape" }, /* Null — cart_id TBD at packaging */ /* Additional grants land here as future ADRs sanction exceptions. */};
static const size_t kn86_capability_allowlist_count = sizeof(kn86_capability_allowlist) / sizeof(kn86_capability_allowlist[0]);A cart identified by cart_id is granted a capability iff a row exists for that (cart_id, keyword) tuple. A cart with no row in the allowlist is granted zero capabilities — identical to a cart that declared no capabilities.
v0.1 launch allowlist contents:
| Cart | cart_id | Granted keywords |
|---|---|---|
| Null | TBD at Null packaging time (hash of "NULL" + "CIPHER_ANALYSIS") | cipher-main-grid-escape |
| ICE Breaker | from packaging | supersedes-terminal |
| Depthcharge | from packaging | supersedes-sonar |
| Black Ledger | from packaging | supersedes-audit |
| Neongrid | from packaging | supersedes-grid |
| (all other launch carts) | — | (none) |
The four supersedes-* keywords were added per ADR-0030. The keyword is a runtime allowlist token; the cart’s register-mission-contributions form encodes the higher-level :supersedes :baseline-name (and :threat-cap N) in its Lisp surface. The allowlist gate ensures that only the four launch carts can declare a supersession, preventing future carts (or unsanctioned mod cartridges) from declaring :supersedes for a baseline they should not be allowed to dominate. Per the no-precedent rule documented in mission-control.md §6 and ADR-0030, no future cart will ever be granted a supersedes-* keyword.
Cart ID pinning. The exact cart_id values are assigned by kn86cart build at packaging time (CRC of program_name + module_class, per §Header Interpretation). The nOSh runtime allowlist entries must be updated to match once the launch carts are finalized — that pinning step is a sub-task of the nOSh runtime allowlist implementation PR and lives in the C Engineer agent’s scope. This ADR commits to the mechanism and the v0.1 grant; the exact uint32 values follow once packaging is deterministic.
Adding a new capability keyword in the future:
- A new ADR sanctions the exception (matching the ADR-0015 §3a precedent).
- A nOSh runtime PR adds a row to
kn86_capability_allowlistfor the granted cart. - A nOSh runtime PR adds the corresponding FFI gate at every primitive that reads the capability set (the gate code pattern lives alongside ADR-0015 §3a’s
cipher-emit-main-grid). - The cart’s packaging pipeline adds the keyword to its
CART_CAPABILITIESsubsection. - No
.kn86format change is required. No version bump, no new subsection type, no re-work of existing cartridges. This is the extensibility contract.
Error-Reporting Path
Section titled “Error-Reporting Path”Capability-related errors are raised at cart-load, before any cart Lisp executes. They are hard errors, never silent drops — the operator must learn that the cart was rejected, and the cart’s intent (the keyword it requested) must be visible in the error message.
Three error codes are defined:
| Error code | When it fires | Reported detail |
|---|---|---|
:capability-block-malformed | The CART_CAPABILITIES subsection fails an encoding constraint (bad len, bad _reserved, size mismatch, duplicate subsection, count > 15). | Offset into the subsection where the parse failed, plus the failing field name. |
:capability-not-granted | A declared keyword has no matching allowlist row for this cart’s cart_id. | The first un-granted keyword string, verbatim. |
:capability-denied | Runtime, not cart-load: a cart calls a privileged FFI primitive without having declared the gating capability. Defined by ADR-0015 §3a for cipher-emit-main-grid; this ADR commits to the error-code name. | The primitive name that was called. |
:capability-block-malformed and :capability-not-granted both abort cart-load before arena allocation. :capability-denied aborts the specific FFI call at runtime but does not unload the cart (same convention as other runtime FFI errors in ADR-0005).
Reporting surface. All three errors flow through the standard nOSh runtime error pipeline. The user-facing path is:
- Row 24 firmware action bar (per CLAUDE.md Canonical Hardware Specification row layout — Row 24 is the nOSh runtime-owned action bar) renders a single-line error tag in the form
CART REJECTED: :capability-not-granted cipher-main-grid-escape. The message is deliberately short to fit the 80-column row. - CIPHER-LINE (ADR-0015) does not surface these errors — the auxiliary display is reserved for the Cipher voice + utility surfaces, and cart-load errors are a runtime-state event, not a Cipher-voice event.
- nOSh runtime log (the SYS diagnostic log, viewable via the nOSh SYS tab of the Bare Deck terminal) records the full error with offset detail for post-mortem. This log is persisted to EEPROM (bounded ring, per the nOSh runtime log discipline in
KN-86-Capability-Model-Spec.md). - Emulator build additionally writes the error to stderr via the existing cart-load logging path. Not a user-facing surface, but load-bearing for CI and golden-file regression tests.
Row 24 is confirmed as the operator-facing channel in this amendment. A cart that is rejected leaves the prior cart loaded (or boots the nOSh runtime into the no-cart Bare Deck state if no prior cart was present); the action-bar error sits visible until the operator dismisses it with TERM or inserts a new cart.
Extensibility
Section titled “Extensibility”Adding a new capability keyword in the future requires only an allowlist row and, if it gates a new FFI primitive, a matching FFI gate. It does not require:
- a
.kn86format version bump, - a new static-data subsection tag,
- a change to the subsection’s binary layout,
- a compiler-level schema migration,
- a re-pack of existing cartridges.
The ASCII-keyword design specifically buys this property. A capability keyword is a new string in the allowlist and a new check at the FFI boundary — both small, local changes reserved for the nOSh runtime. This matches the ADR-0015 §3a design intent (“Capability flags are data, not code”).
Cart Manifest Schema
Section titled “Cart Manifest Schema”Added by the 2026-06-07 amendment per ADR-0035 §F2. ADR-0035 introduces the trackpoint cursor cart-FFI and designates ADR-0006 as the home for the :pointer-hidden manifest flag (ADR-0035 §2, §F2).
Beyond the capability keyword list, a cart’s static contract carries a small set of manifest flags — boolean (or scalar) declarations that set a cart-load default the runtime applies before any cart Lisp executes. Manifest flags are distinct from capability keywords: a keyword requests a privilege gated by the baked-in allowlist, whereas a manifest flag sets a default behavior that requires no grant and is always honored. The flag set is intentionally small and additive; a cart that declares no flags gets the v0.1 defaults below.
:pointer-hidden (boolean, default false)
Section titled “:pointer-hidden (boolean, default false)”| Flag | Type | Default | Semantics |
|---|---|---|---|
:pointer-hidden | boolean | false | When true, the trackpoint cursor is hidden by default the moment the cart loads, before any cart Lisp runs. The operator still has a logical cursor position (it keeps updating from trackpoint motion); only the visual rendering is suppressed. When false (or absent), the cursor is visible at cart-load — the runtime default. The cart may still override the manifest default at runtime via (cursor-visible! #t) / (cursor-visible! #f) per ADR-0035 §2; the FFI override applies for the lifetime of that cart load only, and cart unload restores the runtime default (visible) for the next cart. |
Authoring surface. The flag is declared in the cart’s manifest, e.g.:
:pointer-hidden trueWhen to set it. Use :pointer-hidden true when the cart’s UI is keyboard-only and a visible cursor would be visually noisy — e.g. a phase-screen cart that owns the full main grid for ASCII art. Carts whose cursor visibility varies per phase declare the cart-load default here and toggle at runtime with (cursor-visible! ...); manifest-only is the declarative default, the FFI is the imperative override (see ADR-0035 §2 “Why both, not one”).
Cart-load ordering. The :pointer-hidden flag is captured at manifest parse time — in the same cart-load window as CART_CAPABILITIES parsing (§Loading Semantics step 3a), before arena allocation and before the cart’s first Lisp form runs. This guarantees the cursor is in the manifest-declared state for the cart’s very first rendered frame; there is no “frame of wrong cursor” at cart load.
Scope. This amendment commits to the flag, its type, its default, and its semantics. The exact on-disk byte placement (a field within the CART_CAPABILITIES block versus a new CART_MANIFEST_FLAGS static-data subsection) and the cart-bundle tooling + runtime-loader changes are the engineering follow-on owned by the C Engineer + Platform Engineering agents per ADR-0035 §F2; that work is tracked outside this ADR (the emulator-side cartridge.c parsing is a separate kinoshita-repo task). No .kn86 format version bump is required — manifest flags are additive, and an older runtime that does not recognize the flag falls back to the runtime default (cursor visible), which is safe.
Loading Semantics
Section titled “Loading Semantics”At cartridge load (nOSh runtime, once per swap)
Section titled “At cartridge load (nOSh runtime, once per swap)”- Read
.kn86header. - Validate magic and version.
- Check
req_api_versionagainst nOSh runtime version; abort if mismatch. 3a. Parse and validate theCART_CAPABILITIESstatic-data subsection if present (added 2026-04-24; see §Cart-Capabilities Block). Reject cart-load with:capability-block-malformedor:capability-not-grantedas specified there. On success, record the granted capability set. - Allocate arena (size negotiated: header may hint cart_arena_size, nOSh runtime selects 16–32 KB).
- Load the code section (Lisp source) into the arena.
- Initialize Fe interpreter in arena.
- Load static data section into nOSh runtime memory (separately, not in cart arena).
- Call cartridge
cart_initLisp function:- Register cell types
- Create initial cell structures (UI cells, lists, etc.)
- Seed LFSR
- Return to nOSh runtime
- After
cart_initreturns, restore save state from the cartridge’s own SD filesystem at<save_root>/<cart_id_hex>.savper ADR-0019 CART-05 (added 2026-04-25 / GWP-249). The runtime callscart_load(buf, save_size)against the slot the cartridge declared via static data; on miss (file absent or size mismatch) the cart starts from itscart_initdefaults. Symmetric with the cartridge-unload step that writes the same path.
At mission start (nOSh runtime, per mission)
Section titled “At mission start (nOSh runtime, per mission)”- Clear all user-spawned cells (cells created after
cart_init) - Call cartridge
on-mission-starthandler (if present in Lisp) - Generate mission board via procedural gen
- Player enters mission phase
At mission phase boundary (nOSh runtime, per phase)
Section titled “At mission phase boundary (nOSh runtime, per phase)”- Current phase’s
on-exithandler fires - Next phase’s
on-enterhandler fires - If no next phase, trigger
mission-complete→ payout
At cartridge unload (nOSh runtime, on cartridge swap)
Section titled “At cartridge unload (nOSh runtime, on cartridge swap)”- Save cartridge-specific save data (if any) to the cartridge’s own SD filesystem (e.g.,
/save/<cart_id>.sav) per ADR-0019. (Previously specified as on-cart MBC5 SRAM under ADR-0013; ADR-0013 has been superseded by ADR-0019.) - Deallocate cart arena (implicit — no cleanup needed, arena is just a pointer reset)
- Unload static data from nOSh runtime memory
Migration Story
Section titled “Migration Story”v1.1 → v2.0:
- v1.1 never shipped (design was C macro-based, proof-of-concept only)
- No live cartridges to migrate
- v2.0 is the first production format
v2.0 future (to v3.0):
- Add new optional section types (e.g., SHADERS for future graphical enhancements)
- New header fields are added at the end; old nOSh runtime skips them
- Bytecode format unchanged (Fe version compatibility)
- Old v2.0 carts load on new nOSh runtime; new carts may not load on old nOSh runtime (version check in header)
Size Estimates
Section titled “Size Estimates”Typical cartridge (e.g., ICE Breaker):
| Component | Size |
|---|---|
| Header | 80 B |
| Bytecode (Fe source) | ~8 KB (for 200 lines of Lisp) |
| Static data (sprites, strings, missions) | ~4 KB |
| Debug section | ~12 KB (includes source) |
| Total (with debug) | ~24 KB |
| Total (no debug) | ~12 KB |
For production deployment (debug stripped): 12–16 KB per cartridge. Well within any distribution budget.
Checksum & Integrity
Section titled “Checksum & Integrity”The checksum field in the header is a CRC-32 of the entire file (header + all sections).
Rationale:
- Modest: detects bit-flip errors, not cryptographic attacks
- Appropriate for a device that ships cartridges via USB/SD card or local network
- Not for security (player could modify cartridge); for accident detection
Calculation (pseudocode):
uint32_t compute_checksum(const uint8_t *file, size_t size) { // CRC-32 (same polynomial as ZIP, PNG, Ethernet) uint32_t crc = 0xFFFFFFFF; for (size_t i = 0; i < size; i++) { crc = (crc >> 8) ^ crc32_table[(crc ^ file[i]) & 0xFF]; } return crc ^ 0xFFFFFFFF;}nOSh runtime behavior:
- On load, if
checksum != 0, compute actual checksum and compare - If mismatch: log warning, may abort (configurable at build time)
- If
checksum == 0: skip check (development builds, rapid iteration)
Example: Complete ICE Breaker v2.0 Cartridge
Section titled “Example: Complete ICE Breaker v2.0 Cartridge”File: ice_breaker.kn86 (14 KB)
HEADER: magic: "KN86" version: 2 cart_id: 0x3F2A1B48 (hash of "ICE_BREAKER" + "NETWORK_INTRUSION") capability_type: "NETWORK_INTRUSION" req_api_version: 0x0201 req_vm_version: 0x0100 bytecode_offset: 80 bytecode_size: 8192 static_data_offset: 8272 static_data_size: 4096 debug_offset: 12368 debug_size: 2048 checksum: 0x12345678
CODE SECTION (8192 bytes): [Lisp source for ice_breaker.lsp — parsed and tree-walked on load; no bytecode today, AOT deferred]
STATIC DATA SECTION (4096 bytes): [Type:SPRITES] [Size:2048] [sprite data] [Type:STRINGS] [Size:1024] [string table] [Type:MISSIONS] [Size:1024] [mission templates] [Type:END] [Size:0]
DEBUG SECTION (2048 bytes): [Line table: bytecode PC → source line] [Symbol table: function/var names] [Original ice_breaker.lsp source code]
CRC-32 CHECKSUM: 0x12345678Tooling
Section titled “Tooling”Compiler (desktop, one-time investment)
Section titled “Compiler (desktop, one-time investment)”kn86_compiler --source ice_breaker.lsp \ --sprites sprites.png \ --psg-patterns sfx.txt \ --missions missions.txt \ --output ice_breaker.kn86 \ --debug \ --cart-id 0x3F2A1B48Tool responsibilities:
- Bundle
.lspsource: inline(load …)s and parse-check (thekec buildmodel — a source bundler, not a compiler). AOT bytecode compilation is deferred (see 2026-06-14 amendment). - Load sprite assets (PNG → packed bitmap)
- Load PSG patterns (text format → register sequences)
- Load mission templates (structured data → binary)
- Assemble
.kn86container - Compute checksum
- Strip debug section if
--no-debugflag
Cartridge inspector (any platform)
Section titled “Cartridge inspector (any platform)”kn86_inspect ice_breaker.kn86
Output: Magic: KN86 Version: 2.0 Cart ID: 0x3F2A1B48 Capability: NETWORK_INTRUSION API Version: 2.1 VM Version: 1.0 Bytecode: 8192 bytes (offset 80) Static Data: 4096 bytes (offset 8272) Debug Info: 2048 bytes (offset 12368) Checksum: 0x12345678 [OK] Sprites: 4 (total 2048 bytes) Strings: 8 (total 512 bytes) Missions: 2 (total 1024 bytes)Known Unknowns / Follow-ups
Section titled “Known Unknowns / Follow-ups”-
Bytecode format (DEFERRED 2026-06-14): This spec assumes Fe source is shipped — which is what shipped, and remains current behavior. The Fe runtime is a tree-walking interpreter (ADR-0004 2026-06-14 amendment); there is no bytecode today. A custom/AOT bytecode format is deferred, not abandoned — the implementation-ready design is parked at
kec-lisp/docs/bytecode-vm.mdwith explicit revisit triggers, and the change would be backward-compatible via the version field (and would add a bytecode verifier, since bytecode would then cross the cart trust boundary). See the 2026-06-14 amendment. -
Save data for cartridges:RESOLVED 2026-04-22 per ADR-0013; RE-RESOLVED 2026-04-24 per ADR-0019, which supersedes ADR-0013. Per-cartridge save state lives as a file on the cartridge’s own SD card filesystem (e.g.,/save/<cart_id>.sav). The nOSh runtime treats it as opaque cartridge-owned storage on the mounted SD; the SD’s wear-leveling controller handles the underlying flash. Cross-cartridge fields — operator handle, credits, reputation, cartridge history bitfield, and the variable-length phase chain — remain in device storage as Universal Deck State (on the Pi’s microSD per ADR-0011). Thecart_save/cart_loadNoshAPI signatures are unchanged at the FFI surface; only the storage backend differs from the prior interim resolution under ADR-0013 (which had specified MBC5 battery-backed SRAM on the cartridge). -
Hot reload:RESOLVED 2026-04-22 per ADR-0013; RE-RESOLVED 2026-04-24 per ADR-0019, which supersedes ADR-0013. Hot reload while a mission is in progress remains not-a-meaningful-question under the SD-sled physical-cartridge model. Cartridge state is, by construction, the bytecode + static data + save file on the physical cart’s SD — removing the cart removes its state, inserting a cart brings fresh state. The state transition is well-defined at the hardware boundary: cartridge-swap is a full unload → reload with no in-flight state to migrate. The nOSh runtime detects cartridge-presence viaudevevents on the USB mass-storage device backing the cart (per ADR-0019) — replacing the prior interim ADR-0013 mechanism (/CSline on the DMG edge connector) — and drives the unload/reload sequence in §Loading Semantics. No format change is required. -
Encryption / DRM: Should cartridges be encrypted? Current: no. If wanted: encrypted bytecode section + key in nOSh runtime (not suitable for open-source hardware; deferred). The prior ADR-0013 note about MBC5 cartridge-ID registers as a possible integrity-beacon carrier is obsolete under ADR-0019 (no MBC5); the SD-sled equivalent if wanted would be the SD card’s own CID/CSD registers or an in-
.kn86integrity field — both are still not cryptographic security and remain deferred. -
Cart-capabilities serialization (ADR-0015 §3a follow-on):RESOLVED 2026-04-24 by this ADR’s 2026-04-24 amendment. Capabilities live in a newCART_CAPABILITIESstatic-data subsection (type=5), with a length-prefixed ASCII-keyword payload. The nOSh runtime validates the block against a baked-in allowlist at cart-load and rejects unauthorized declarations with:capability-not-grantedat Row 24. See §Cart-Capabilities Block. -
Prose-vs-struct header-size discrepancy.RESOLVED 2026-04-24 (editorial pass, GWP-212, commitefb0f87). The ADR-0006 prose diagram, §Header heading, structTotal:comment, §Size Estimates table, and the ICE Breaker example (offsets +kn86_inspectoutput) now all state 80 bytes, matching the canonicalCartridgeHeaderV2struct in this ADR and theKN86CART_HEADER_SIZEconstant intools/kn86cart/format/kn86cart.h(whose_Static_assertenforces the 80-byte size at compile time). The legacy “64 bytes” appears to be a stale counting error from before the struct grewreq_vm_version, four offset fields, and the checksum field. No binary-format change was required — the on-disk header has always been 80 bytes; only the prose was stale. See §Amendment Log 2026-04-24 (editorial) for details.
Summary
Section titled “Summary”.kn86 v2.0 is a portable, modular, inspectable cartridge format. It holds Lisp source (AOT bytecode deferred — see 2026-06-14 amendment), static assets, and optional debug info. The nOSh runtime loads it once per cartridge swap, parses the source into a fresh Fe context (tree-walking interpreter) backed by an arena, and dispatches handlers accordingly. The format is designed for extensibility: new asset types can be added without breaking old cartridges, and the version field reserves a clean path to AOT bytecode if a revisit trigger ever fires.
Ready for packaging tool implementation (Phase 2) and initial cartridge shipping.
Amendment Log
Section titled “Amendment Log”2026-06-14 — Code section ships Lisp source; AOT bytecode deferred (clarification)
Section titled “2026-06-14 — Code section ships Lisp source; AOT bytecode deferred (clarification)”Status effect: Accepted (unchanged). No byte-level format change; no format version bump. This amendment corrects framing and records a deferral; it does not change the on-disk layout. Companion to the ADR-0004 2026-06-14 amendment and the ADR-0001 2026-06-14 amendment.
What was conflated. The Summary’s “Bytecode section: compiled Lisp bytecode (via Fe compiler),” the ICE Breaker example’s “Fe-compiled Lisp bytecode,” and the tooling section’s “compile to Fe bytecode” describe a desktop compiler and an on-cart bytecode artifact that were never built. The selected runtime, Fe, is a tree-walking interpreter — it reads source into a cons-cell AST and evaluates it directly. There is no Fe bytecode format, no instruction set, and no compile step.
What ships (and was always the recommendation). The body’s own §“Loading Semantics” note already recorded two options and recommended Option 1 — ship .lsp source for MVP. That is what shipped and what remains current: the .kn86 code section holds Lisp source; the device parses it and tree-walks it on load. The standalone language tool that produces a self-contained source bundle is kec build (github.com/Kinoshita-Electronics-Consortium/kec-lisp), which inlines (load …)s and parse-checks — a source bundler, explicitly not a compiler. A .kn86 cart wraps such a source bundle plus the static-data/debug sections this ADR specifies.
AOT bytecode: deferred, not abandoned (Option A — Josh, 2026-06-14). The “Bytecode section” structural name and the version field are retained as the forward-compatible reservation for a future AOT bytecode artifact (the body’s Option 2 / Known Unknown #1). That path is deferred, gated on the revisit triggers in the ADR-0004 amendment; the implementation-ready design (in-memory VM → AOT) is parked at kec-lisp/docs/bytecode-vm.md. If it ever lands, the code section would carry bytecode, a bytecode verifier would be added (bytecode would then cross the cart trust boundary), and the change would be backward-compatible via the version field. Nothing in the on-disk layout needs to change to keep that door open.
Edits in this amendment: Summary item 2 (Bytecode → Code section, source), final Summary paragraph, the ICE Breaker example code section, the Tooling step-1 line, and Known Unknown #1 — all reframed to “source today, AOT bytecode deferred.” The 80-byte header, the offset/size fields (named bytecode_offset/bytecode_size in the struct — kept for ABI stability; they address the code section regardless of its contents), the static-data sub-section tagging, debug section, checksum, and CART_CAPABILITIES block are all unchanged. Format version remains 2.
Amendment 2026-06-07 — Adds the :pointer-hidden cart manifest flag (per ADR-0035 §F2)
Section titled “Amendment 2026-06-07 — Adds the :pointer-hidden cart manifest flag (per ADR-0035 §F2)”Status effect: Accepted (unchanged). Adds one manifest flag to the cart’s static contract. No byte-level format change; no format version bump.
Rationale. ADR-0035 (Trackpoint cart-FFI) introduces the merged trackpoint cursor and its NoshAPI surface (cursor-position, on-trackpoint-move, on-trackpoint-click, cursor-visible!). The cursor is visible by default; carts hide it two ways — a runtime FFI ((cursor-visible! bool), owned by ADR-0035 / ADR-0005) and a declarative cart-load default expressed as a manifest flag. ADR-0035 §2 and §F2 designate ADR-0006 as the home for that manifest flag (:pointer-hidden). This amendment adds the flag to ADR-0006’s manifest schema so the cart format is the single source of truth for it.
Changes in this amendment:
- Front-matter
Amended:line: appended the 2026-06-07 entry. - Related list: added
ADR-0035-trackpoint-cart-ffi.md. - New top-level section: §Cart Manifest Schema. Introduces the manifest-flag concept (distinct from capability keywords) and defines
:pointer-hidden(boolean, defaultfalse): whentrue, the trackpoint cursor is hidden at cart-load before any Lisp runs; the cart may still toggle at runtime via(cursor-visible! #t)per ADR-0035 §2. Documents authoring surface, cart-load parse ordering, and scope (exact byte placement + tooling/loader work is the C Engineer + Platform Engineering follow-on per ADR-0035 §F2).
What did not change:
- Original Status: Accepted (unchanged).
.kn86byte-level layout — header, bytecode section, static-data sub-section tagging, debug section layout, checksum format, CART_CAPABILITIES block — all unchanged. Format version remains2.- CART_CAPABILITIES allowlist machinery, error symbols (
:capability-block-malformed,:capability-not-granted,:capability-denied) — unchanged. - Loading-semantics step ordering — unchanged. The
:pointer-hiddenflag is captured in the same cart-load parse window as CART_CAPABILITIES (step 3a), before arena allocation. - Existing carts on disk — no migration required. A cart that declares no manifest flags gets the v0.1 default (cursor visible). An older runtime that does not recognize the flag falls back to the runtime default (visible), which is safe.
Authority trail. ADR-0035 §2 (“Visibility default — VISIBLE, with hide via manifest and runtime FFI”) and §F2 (“Cart manifest :pointer-hidden flag — extend ADR-0006 cart format with the manifest key”). Engineering follow-up (out of this doc’s scope): exact on-disk placement of the flag, cart-bundle tooling acceptance, and runtime-loader honoring — owned by C Engineer + Platform Engineering. The emulator-side cartridge.c parsing is a separate kinoshita-repo task.
Amendment 2026-05-05 — CART_CAPABILITIES allowlist gains supersedes-* keywords for the four launch carts (per ADR-0030)
Section titled “Amendment 2026-05-05 — CART_CAPABILITIES allowlist gains supersedes-* keywords for the four launch carts (per ADR-0030)”Status effect: Accepted (unchanged). Adds four allowlist keywords. No byte-level format change.
Rationale. ADR-0030 introduces System-tier baseline modules (TERMINAL / GRID / AUDIT / SONAR) and a :supersedes mechanic on register-mission-contributions. The four launch carts each declare :supersedes :<baseline> and :threat-cap N in their Lisp form. To prevent future carts (or mod cartridges) from declaring supersession for baselines they should not be allowed to dominate, the cart-side :supersedes declaration must be gated by the existing CART_CAPABILITIES allowlist machinery (per the original 2026-04-24 amendment). The four launch carts’ allowlist entries gain a supersedes-* keyword.
Changes in this amendment:
- §Allowlist v0.1 launch contents table: ICE Breaker gains
supersedes-terminal; Depthcharge gainssupersedes-sonar; Black Ledger gainssupersedes-audit; Neongrid gainssupersedes-grid. The Null cart’scipher-main-grid-escapekeyword is unchanged. - No-precedent reinforcement: The amendment preserves the existing “future carts must add a new ADR + new allowlist row” extensibility contract. Per
mission-control.md§6 and ADR-0030, no future cart will ever be granted asupersedes-*keyword — the allowlist mechanism is the enforcement point.
What did not change:
- Original Status: Accepted (unchanged).
.kn86byte-level layout — unchanged.- CART_CAPABILITIES sub-section serialization, type tag, length-prefix format — unchanged.
- Allowlist machinery — unchanged. This is purely a v0.1 contents update.
- Loading semantics, error symbols (
:capability-block-malformed,:capability-not-granted) — unchanged.
Authority trail. ADR-0030 §Decision item 3 (:supersedes field on register-mission-contributions); software/cartridges/design-bibles/launch-titles-capability.md (per-cart updated (register-capabilities ...) blocks). Engineering follow-up: the runtime’s allowlist build step must include the four supersedes-* rows once the launch-cart cart_id values are pinned.
Amendment 2026-05-03 — MISSIONS sub-section payload clarified; CART_CAPABILITIES reconciled with the Capability Registry (per ADR-0028)
Section titled “Amendment 2026-05-03 — MISSIONS sub-section payload clarified; CART_CAPABILITIES reconciled with the Capability Registry (per ADR-0028)”Rationale. ADR-0028 promotes Mission Control as a named runtime subsystem with schema-driven contract generation. The cart-side container shape is unchanged at the byte level, but the meaning of the existing MISSIONS sub-section payload (type=4) changed: it is now a sequence of defcontract-schema forms (Fe-compiled), not pre-baked mission template structs. Mission Control evaluates these schemas at generation time against UDS + LFSR seed; the runtime no longer “selects a template and produces a contract.” The byte-level payload format, sub-section type tag, and loader behavior are unchanged.
Separately, the CART_CAPABILITIES sub-section (type=5) added 2026-04-24 needed a vocabulary reconciliation. ADR-0028 introduces three capability metadata lists per module: :provides (capability symbols other carts/schemas require), :affinities (composition tags), and :seeds (named generator function symbols). The CART_CAPABILITIES block is the on-disk home for these; the keyword allowlist documented in §Cart-Capabilities Block is extended to include the :provides / :affinities / :seeds triple. The cart’s (register-capabilities ...) Lisp form is the authoring surface; the CART_CAPABILITIES sub-section is the on-disk binary that surface compiles to.
Changes in this amendment:
- Related section: added
ADR-0028-mission-control-capability-registry.mdandADR-0029-mission-runner.md. - Summary section, bullet #3: unchanged textually, but the meaning of “mission templates” is now “contract schemas” — see the table row update.
- Static-data subsection type table: MISSIONS (type=4) row payload description updated from “binary serialization of mission structures” to “Contract schemas (
defcontract-schemaforms)” with a backward-compatibility note. CART_CAPABILITIES (type=5) row is unchanged at the byte level; semantic reconciliation lives in §Cart-Capabilities Block (follow-on editorial pass). - §Cart-Capabilities Block, allowlist: the v0.1 allowlist gains the registry vocabulary triple (
:provides,:affinities,:seeds) as keyword categories. Keyword tokens already declared (cipher-main-grid-escapefor Null, etc.) remain valid; the addition is additive. - Loading Semantics, step 3a: capability validation now records the granted capability set into the Mission Control–visible Capability Registry, not just an internal flag table. The runtime-internal data structure is engineering-implementation detail; the contract is unchanged.
- Known Unknowns: none added. None resolved by this amendment.
What did not change:
- Original Status: Accepted (unchanged).
.kn86byte-level layout — header, bytecode section, static data sub-section tagging, debug section layout, checksum format, all unchanged.- Sub-section type tag values — MISSIONS=4, CART_CAPABILITIES=5 are stable identifiers.
- Loading-semantics step ordering and error symbols —
:capability-block-malformed,:capability-not-grantedcontinue to apply. - Existing carts on disk — no migration is required. A v0.1 cart compiled before this amendment, whose MISSIONS payload happens to be a pre-baked template struct, still loads; a runtime that predates ADR-0028 sees the same bytes and selects from them as before. New carts compile to
defcontract-schemaforms; runtimes implementing ADR-0028 evaluate them. The transition is per-cart, not per-runtime.
Scope discipline: this is an amendment that aligns the cart format with Mission Control’s contract-generation architecture. It does not redefine the format. For the runtime architecture rationale, see ADR-0028.
Amendment 2026-04-22 — Closes Known Unknowns #2 and #3
Section titled “Amendment 2026-04-22 — Closes Known Unknowns #2 and #3”Following the acceptance of ADR-0013 (Cartridge Physical Format) on 2026-04-22, two Known Unknowns in this ADR are resolved and one header-field ceiling is updated. The .kn86 binary container format is unchanged; no downstream container changes are required.
Changes in this amendment:
- Related section: added
ADR-0013-cartridge-physical-format.md. - Header Interpretation —
static_data_size: noted the ceiling is now bounded by MBC5 ROM capacity (up to 8 MB per cartridge), not nOSh runtime memory. The nOSh runtime reads static data on demand via MBC5 bank-switching. - Known Unknowns #2 (per-cartridge save data): resolved. Per-cartridge save state lives on cartridge-side MBC5 battery-backed SRAM (up to 128 KB). Universal Deck State in device storage retains only the cross-cartridge fields (handle, credits, reputation, cart history bitfield, phase chain).
- Known Unknowns #3 (hot reload): resolved. MBC5 + physical cartridge swap defines a clean state transition at the hardware boundary. Hot reload mid-mission is no longer a meaningful question under the physical-cartridge model.
- Known Unknowns #4 (encryption / DRM): cross-reference to ADR-0013’s note on MBC5 cartridge-ID registers as a possible integrity-beacon carrier. Still deferred; still not cryptographic security.
What did not change:
- Original Status: Accepted (unchanged).
.kn86header layout, bytecode section, static data sub-section tagging, debug section layout, checksum format, loading semantics — all unchanged.- Size estimates table — unchanged; estimates remain valid for the first-party cartridge class.
- Known Unknowns #1 (bytecode format) — still open.
Scope discipline: this is an amendment, not a rewrite. For the physical-format rationale and trade-off analysis, see ADR-0013.
Amendment 2026-04-24 — Adds §Cart-Capabilities Block following ADR-0015 §3a
Section titled “Amendment 2026-04-24 — Adds §Cart-Capabilities Block following ADR-0015 §3a”Rationale for in-place amendment rather than companion ADR. ADR-0015 §3a commits to the capability-flag mechanism; the serialization of that mechanism inside .kn86 is cart-format territory, and ADR-0006 is the single source of truth for cart format. Fragmenting the serialization into a companion ADR would split the cart-format spec across two documents and force the kn86cart.h header file (which already declares itself the SINGLE SOURCE OF TRUTH for the on-disk format) to reference both ADRs as co-authorities. The 2026-04-22 amendment set the precedent: additive changes to .kn86 belong in an Amendment Log on this ADR. The original “Accepted” status is preserved; the amendment log is signed and dated.
Josh authorized the in-place path implicitly by approving ADR-0015 §3a’s explicit direction (“The cart-format change (adding cart-capabilities to the .kn86 header) is a follow-on amendment to ADR-0006”). No companion ADR was proposed.
Changes in this amendment:
- Related section: added
ADR-0015-cipher-line-auxiliary-display.md. - Summary section, bullet #3: static data section now lists cart-capabilities block as one of its contents.
- Static-data subsection type table: added
CART_CAPABILITIES(type=5) row. - New top-level section: §Cart-Capabilities Block. Spells out the binary serialization, nOSh runtime loader parsing behavior, allowlist format and v0.1 contents, error-reporting path (Row 24 firmware action bar), and the extensibility contract.
- Loading Semantics, step 3a: new step inserted for capability parsing and allowlist validation. Old steps 4–8 are unchanged; they are merely renumbered in prose for clarity.
- §Example (ICE Breaker): clarified as “no capabilities” and a new Null example added showing the
CART_CAPABILITIESsubsection in situ. - Known Unknowns #5: new entry recording the now-resolved ADR-0015 §3a follow-on.
- Known Unknowns #6: new entry tracking the prose-vs-struct header-size discrepancy (80-byte canonical in tooling; prose still says 64-byte). Deferred to an editorial pass; non-blocking. (Resolved 2026-04-24 by the subsequent editorial amendment; see below.)
What did not change:
- Original Status: Accepted (unchanged).
- 64-byte / 80-byte prose-vs-struct discrepancy is logged but not fixed in this amendment — an editorial-only pass is the right mechanism, not a capability-block PR. (Resolved 2026-04-24 by the subsequent editorial amendment; see below.)
.kn86header layout, bytecode section, checksum format, debug section layout, sprite/PSG/strings/missions subsection tagging, size estimates, and the format version number (still2) — all unchanged.- Known Unknowns #1 (bytecode format) — still open.
- Existing carts (ICE Breaker, Depthcharge, Black Ledger, Neongrid) require zero repackaging to continue working; the
CART_CAPABILITIESsubsection is optional. kn86cartCLI tool — no behavior change required for the v0.1 MVP, which already emits a minimal static-data section. A future--capabilityflag is the right extension point, but is out of scope for this amendment; the nOSh runtime loader can parse hand-built capability blocks in the meantime. The packager extension is queued for the C Engineer agent’s nOSh runtime allowlist implementation PR.
Backward / forward compatibility.
- Carts without
CART_CAPABILITIES: loader treats them as zero-capability carts. This is every launch cart except Null. No repack needed. - Old nOSh runtime meeting new carts (future scenario): a nOSh runtime build that predates this amendment will encounter an unknown static-data subsection type (
CART_CAPABILITIES = 5) and must skip it, matching the original “forward compatibility” design note in §Static Data Section. An old nOSh runtime will therefore accept a cart that requests a capability the old nOSh runtime cannot honor — this is safe because every capability-gated FFI primitive is itself new and does not exist in old nOSh runtime; the cart’s call to, e.g.,cipher-emit-main-gridon old nOSh runtime would resolve to “unknown primitive” and fail cleanly at the FFI layer. No privilege escalation is possible across nOSh runtime versions. - New nOSh runtime meeting old carts: new nOSh runtime sees no
CART_CAPABILITIESsubsection, records zero granted capabilities, and every capability-gated FFI primitive returns:capability-deniedat call time. No existing cart calls these primitives, so no existing cart is affected.
Documentation Updates (per Spec Hygiene Rule 3)
Section titled “Documentation Updates (per Spec Hygiene Rule 3)”Files changed in the PR that lands this 2026-04-24 amendment:
-
docs/architecture/adr/ADR-0006-cart-format-v2.md— this file. -
docs/architecture/adr/ADR-0015-cipher-line-auxiliary-display.md— Known Unknowns #5 updated to cross-reference this amendment as the resolving change; §3a “Cross-reference” sentence updated accordingly.
Files touched only to fix cross-references or stale claims (no behavior change):
-
docs/KN-86-Definitive-Guide.md— Part 6 “Cartridge format v2.0 (ADR-0006)” cross-reference updated to note the cart-capabilities subsection. -
docs/KN-86-Platform-Design-Master-Index.md— ADR-0006 row already cites the spec by URL; no row change needed, but the platform-component summary line mentioning.kn86now references the capability-flag mechanism. -
docs/architecture/KN-86-CIPHER-LINE-Grammar-Framework.md— §2 “Sanctioned Exception — Null’s Main-Grid Cipher Escape” clarified that the(cart-capabilities ...)Lisp form is the packager’s authoring form and that the on-disk serialization is theCART_CAPABILITIESsubsection defined here. §14 “Cross-Cutting Concerns” bullet for ADR-0006 (which references a separate proposedCIPHER_GRAMMARsection) is compatible with the capability block — both would coexist as distinct static-data subsections. -
docs/gameplay-specs/KN-86-Null-Gameplay-Spec.md— §“Main-Grid Cipher Escape (Sanctioned Exception)” clarified that the shown(cart-capabilities ...)form is packager authoring input, not the on-disk shape, and points to ADR-0006 §Cart-Capabilities Block for the binary. Added Row 24 reporting path reference to match ADR-0006.
Files deliberately NOT touched in this PR:
CLAUDE.md— canonical hardware spec; ADR-0015 already added Spec Hygiene Rule 6 to it. Row 24 action-bar remains firmware-owned per the existing Row layout rule (term-of-art retained); no new CLAUDE.md row is required for this amendment.tools/kn86cart/format/kn86cart.h— the canonical on-disk header; no header-layout change in this amendment. TheCART_CAPABILITIES = 5constant is a static-data subsection tag, and subsection tags are additive and stable. Adding the constant to this header file is a legitimate follow-up PR (C Engineer scope) but not a spec change.tools/kn86cart/src/*— packager behavior change (new--capabilityflag) is queued as a C Engineer follow-up, not a spec amendment.kn86-emulator/src/cartridge.c— loader change implementing the §Loading Semantics step 3a and the allowlist table is C Engineer follow-up scope.
Spec Hygiene Rule 3 grep sweep confirmed no doc contradicts the amendment. The one known-stale note — the 64-byte prose-vs-80-byte-struct discrepancy in §File Structure — was called out explicitly as Known Unknown #6 rather than silently corrected, because reconciling the prose diagram is an editorial pass best done in a focused PR. (That focused editorial PR is the 2026-04-24 editorial amendment that immediately follows this section; Known Unknown #6 is now closed.)
Amendment 2026-04-24 (editorial) — Closes Known Unknown #6 (prose-vs-struct header size, GWP-212)
Section titled “Amendment 2026-04-24 (editorial) — Closes Known Unknown #6 (prose-vs-struct header size, GWP-212)”Rationale. Known Unknown #6 tracked an editorial inconsistency introduced before the 2026-04-14 acceptance: the §File Structure prose diagram, the §Header heading, the struct Total: comment, the §Size Estimates table, and the ICE Breaker example all labelled the header “64 bytes,” while the field-by-field CartridgeHeaderV2 struct in the same ADR sums to 80 bytes. Downstream tooling (tools/kn86cart/format/kn86cart.h with its _Static_assert, tools/kn86cart/src/header.rs, kn86-emulator/src/types.h, kn86-emulator/tests/test_cartridge_v2_loader.c) has always followed the 80-byte struct — the on-disk format has been 80 bytes since day one. Only the prose was stale.
This amendment reconciles the prose to the struct. No binary-format change; no field change; no loader change. A cart produced by the 2026-04-14 tooling is identical byte-for-byte to a cart produced after this amendment.
Canonical choice: 80 bytes. The struct is authoritative because:
- The struct is compiler-verified:
tools/kn86cart/format/kn86cart.hincludes_Static_assert(sizeof(kn86cart_header_t) == KN86CART_HEADER_SIZE, "...must be exactly 80 bytes"). The tooling cannot build with a wrong size. - Every tool in the repo that reads or writes
.kn86files uses 80 bytes. - The 64-byte prose omits
req_vm_version(added between the initial draft and acceptance), the four offset fields (bytecode_offset,static_data_offset,debug_offseteach paired with a size), andchecksumfrom its implicit sum — a counting error, not a format choice.
Changes in this amendment:
- §File Structure prose diagram:
HEADER (64 bytes, fixed)→HEADER (80 bytes, fixed). Inner bullet list expanded to match the actual struct field set (adds_reserved1,req_vm_version, the offset fields, and the checksum line; drops the vague “8 bytes reserved” line in favor of the accurate 4-byte_reserved2). - §Section Details → §Header heading:
### Header (64 bytes, little-endian)→### Header (80 bytes, little-endian). - §Header struct comment:
/* Total: 64 bytes */→/* Total: 80 bytes */. - §Size Estimates row:
| Header | 64 B |→| Header | 80 B |. The total-with-debug / total-no-debug rows are unaffected because every cart built by the packager has always emitted 80 bytes for the header; the row was the only stale cell. - §Example: Complete ICE Breaker v2.0 Cartridge: updated
bytecode_offset,static_data_offset, anddebug_offsetfrom64 / 8256 / 12352to80 / 8272 / 12368to match the corrected header length. Sizes (bytecode_size,static_data_size,debug_size) are unchanged. Thekn86_inspectsample output below it was updated to match. - §Known Unknowns #6: marked RESOLVED with a pointer to this amendment.
- Front-matter
Amended:line: appended the 2026-04-24 (editorial) entry.
What did not change:
- Original Status: Accepted (unchanged).
CartridgeHeaderV2struct itself — field list, types, offsets — unchanged. The struct was already correct..kn86binary format: no change. Carts on disk are byte-identical.KN86CART_HEADER_SIZEconstant: was already 80, still 80.- Loading semantics (§Loading Semantics), capability block (§Cart-Capabilities Block), static-data subsection layout, checksum format, debug section layout — all unchanged.
- Other Known Unknowns (#1 bytecode format, #4 encryption) — still open as they were.
Backward / forward compatibility. There is no compatibility impact. The prose change does not produce or consume different bytes. Every existing cartridge image, every existing tool build, every existing nOSh runtime build continues to work without rebuild or repack.
Documentation Updates (per Spec Hygiene Rule 3)
Section titled “Documentation Updates (per Spec Hygiene Rule 3)”Files changed in the PR that lands this editorial amendment (branch feat/GWP-212-adr-0006-header-size):
-
docs/architecture/adr/ADR-0006-cart-format-v2.md— this file. All prose, heading, struct comment, size-table, example-offset, and Known-Unknown fixes above. -
docs/KN-86-Definitive-Guide.md— Part 6 “Cartridge format v2.0 (ADR-0006)” prose updated from “64-byte header” to “80-byte header”; ADR roll-up table row updated from “64-B header” to “80-B header”; §Known Unknowns item on the cart header format migration path updated from “64-byte v2.0 header” to “80-byte v2.0 header”.
Files touched to retire obsolete discrepancy callouts (the discrepancy no longer exists, so the “ADR prose says 64 but struct is 80” comments should be removed to avoid future reader confusion):
-
tools/kn86cart/format/kn86cart.h— header-comment note about the prose-vs-struct discrepancy updated to reflect the 2026-04-24 editorial reconciliation. -
tools/kn86cart/src/header.rs— module-level doc comment and inline test comment updated; the discrepancy is resolved, the notes now simply cite the canonical 80-byte size. -
tools/kn86cart/README.md— “Header size discrepancy with ADR-0006 prose” section replaced with a short “Header size” note pointing at ADR-0006 as the authoritative source (no remaining discrepancy to describe). -
kn86-emulator/src/types.h—NOTE ON V2 HEADER SIZEblock updated to state that ADR-0006 now agrees with the struct (80 bytes) after the 2026-04-24 editorial pass. -
kn86-emulator/tests/test_cartridge_v2_loader.c— banner comment’s “ADR’s ‘64 bytes’ annotation is a counting error” line updated; ADR-0006 is now self-consistent.
Files deliberately NOT touched in this PR:
docs/_archive/**— archived outlines reference the historical 64-byte claim. Archive is history; touching it would change history. Out of scope for an editorial reconciliation of active docs.CLAUDE.md— does not restate the header byte count (per Spec Hygiene Rule 1). No update needed.tools/kn86cart/src/*.rs(beyondheader.rs) andkn86-emulator/src/cartridge.c— no behavior change; these were already computing 80 bytes. No change needed.- Gameplay specs, UI specs, and hardware specs referencing “64 bytes” in unrelated contexts (Universal Deck State, title buffers, audit log records, etc.) — these are different
64s; not in scope for this editorial pass.
Spec Hygiene Rule 3 grep sweep completed: rg -i "64[- ]byte|64 B|64-byte header" across docs/, tools/kn86cart/, and kn86-emulator/. Matches on the 64-byte Universal Deck State, 64-byte title buffer, 64-byte audit log records, the 3.12” OLED footprint, and archived docs were confirmed to be different subjects and left untouched. Every remaining reference to a 64-byte cartridge header has been fixed or is in docs/_archive/**.
Amendment 2026-04-24 (post-ADR-0019) — Re-resolves Known Unknowns #2 and #3 following ADR-0019 supersession of ADR-0013
Section titled “Amendment 2026-04-24 (post-ADR-0019) — Re-resolves Known Unknowns #2 and #3 following ADR-0019 supersession of ADR-0013”Rationale. ADR-0013 (Cartridge Physical Format — DMG 32-pin pinout + MBC5 mapper + CR2032-backed on-cart SRAM) was accepted on 2026-04-22 and superseded two days later by ADR-0019 (Cartridge Storage and Physical Form Factor — full-size SD card in a custom two-piece clamshell sled, read via USB mass storage). The 2026-04-22 amendment to this ADR closed Known Unknowns #2 (per-cart save) and #3 (hot reload) by appealing to ADR-0013’s MBC5 SRAM and /CS mechanisms. Those mechanisms no longer exist on the committed hardware path. This amendment re-resolves the same Known Unknowns by appealing to ADR-0019’s SD-filesystem and udev-event mechanisms, leaving the underlying contract identical at the FFI and capability-model surfaces.
Changes in this amendment:
- Front-matter
Amended:line: appended a fourth entry (2026-04-24 (re-resolves Known Unknowns #2 and #3 following ADR-0019, which supersedes ADR-0013)). - Related list: added
ADR-0019-cartridge-storage-and-form-factor.md; ADR-0013 marked as superseded by ADR-0019 inline. - Header Interpretation —
static_data_size: replaced the “MBC5 ROM capacity (up to 8 MB)” ceiling description with the “SD card capacity (gigabyte-scale)” framing; the on-demand read path is now standardread()against the mounted SD filesystem rather than MBC5 bank-switching. - Loading Semantics — At cartridge unload, step 1: “save cartridge-specific save data (if any) to EEPROM” → “save cartridge-specific save data (if any) to the cartridge’s own SD filesystem (e.g.,
/save/<cart_id>.sav) per ADR-0019.” (The original “EEPROM” wording predates both ADR-0013 and ADR-0019; it was an artifact from before the per-cart save question was answered. The 2026-04-22 amendment clarified the semantic in §Known Unknowns but did not edit this step’s prose. Editorial corrected here as part of the re-resolution.) - Known Unknowns #2 (per-cartridge save data): prose updated to point to ADR-0019; the resolution mechanism becomes “file on the cartridge’s own SD card filesystem.” Cross-cartridge fields and Universal Deck State location are unchanged.
- Known Unknowns #3 (hot reload): prose updated to point to ADR-0019; the cartridge-presence detection mechanism becomes
udevevents on the USB mass-storage device backing the cart, replacing the prior interim ADR-0013/CSmechanism. - Known Unknowns #4 (encryption / DRM): the prior ADR-0013 cross-reference (MBC5 cartridge-ID register as integrity-beacon carrier) is marked obsolete; the SD-sled equivalents (SD CID/CSD or in-
.kn86integrity field) are noted in passing. Still deferred; still not cryptographic security.
What did not change:
- Original Status: Accepted (unchanged).
.kn86header layout, bytecode section, static data sub-section tagging, debug section layout, checksum format,CART_CAPABILITIESblock, size estimates, and the format version number (still2) — all unchanged.- Known Unknowns #1 (bytecode format) — still open.
- Known Unknown #5 (cart-capabilities serialization) — still resolved by the 2026-04-24 §Cart-Capabilities Block amendment; ADR-0019 does not touch it.
- Known Unknown #6 (prose-vs-struct header size) — still resolved by the 2026-04-24 editorial amendment; ADR-0019 does not touch it.
- Existing carts (ICE Breaker, Depthcharge, Black Ledger, Neongrid) require zero repackaging; the cart container format is byte-identical pre- and post-ADR-0019.
cart_save/cart_loadNoshAPI signatures (per ADR-0005) — unchanged at the FFI surface; only the storage backend implementation moves from “MBC5 SRAM” to “SD filesystem path.”- The capability model, Universal Deck State, mission board, phase chain, Cipher voice, and Row 0 / Row 24 layout are all out of scope for ADR-0019 and untouched here.
Backward / forward compatibility. Same as for the 2026-04-22 amendment: the on-disk .kn86 format is unchanged, so every existing cart and every existing nOSh runtime build continues to work without rebuild or repack. The semantic difference between the two amendments is where cart_save writes — the FFI signature is stable, so cart Lisp code is unaffected.
Documentation Updates (per Spec Hygiene Rule 3)
Section titled “Documentation Updates (per Spec Hygiene Rule 3)”Files changed in the PR that lands this 2026-04-24 (post-ADR-0019) amendment (branch docs/GWP-223-cartridge-storage-form-factor):
-
docs/architecture/adr/ADR-0006-cart-format-v2.md— this file (front-matter, Header Interpretation, Loading Semantics, Known Unknowns #2/#3/#4, Amendment Log). -
docs/architecture/adr/ADR-0019-cartridge-storage-and-form-factor.md— new ADR; cross-references this amendment in its §Decision item 6 and §“What does NOT change” list. -
docs/architecture/adr/ADR-0013-cartridge-physical-format.md— superseded; banner added pointing to ADR-0019.
The grep sweep for the broader supersession lives in the ADR-0019 PR description; the items affected by this amendment specifically are the four edits listed above. No other live spec doc references the prior MBC5-SRAM / /CS resolution mechanisms.
2026-06-20 — defcontract-schema gains the :objectives clause (ADR-0043)
Section titled “2026-06-20 — defcontract-schema gains the :objectives clause (ADR-0043)”Status effect: Accepted (unchanged). Additive amendment to the schema-generator surface; no byte-level format change — format version stays 2, and schemas already ship inside the cart static-data block (per the 2026-04-24 §Cart-Capabilities Block amendment). Existing carts require zero repackaging.
What changed. ADR-0043 makes the mission objective first-class. The defcontract-schema form (the schema-generator vocabulary this ADR governs at the static-data layer) gains one :objectives clause with two halves:
spine— hand-authored goal cells that every generated instance receives, including the key:branch(mutually-exclusive choice).limbs— a procedural pool the generator draws from: anoptional-pool(:draw0..N per instance) andescalationhooks (latent goals wired to runtime predicates, e.g.(on (>= trace 75) …)).
Goal-cell grammar (the four facets role / reveal / reward / link, reward cells (kind amount when), and :hold constraints) is specified in the companion spec mission-objectives.md §2/§6.
What did NOT change. The .kn86 header layout, static-data section tagging, CART_CAPABILITIES block, checksum, and format version (2) are unchanged. The defcontract-schema clauses already enumerated (:required-capabilities, :threat-range, :phase-count, :noun-generators, :condition-generators, :payout-formula, :ttl-range, :opportunity-weight) are unchanged; :objectives is purely additive, and a schema without it behaves exactly as before.
Authority trail. ADR-0043 §Decision-3 + §“Documentation Updates”; mission-control.md §3.2; companion spec mission-objectives.md §6.