Skip to content

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


.kn86 v2.0 is a binary container format for KN-86 cartridges. It holds:

  1. Header: metadata, version, required API version, capability declaration
  2. Bytecode section: compiled Lisp bytecode (via Fe compiler)
  3. Static data section: sprite bitmaps, PSG patterns, strings, mission templates, cart-capabilities block (added 2026-04-24; see §Cart-Capabilities Block)
  4. Optional debug section: source code, line tables, symbol names (strippable)
  5. 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.


┌─ .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 │
│ │
├─ BYTECODE SECTION (variable, aligned to 4-byte boundary) │
│ Raw Fe-compiled bytecode blob │
│ 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 │
│ │
└─────────────────────────────────────────────────────────────┘

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_size ceiling 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 standard read() 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).

Format: Opaque blob of Fe-compiled bytecode.

The Fe compiler (desktop tool) takes .lsp Lisp source and outputs:

  1. Instruction stream: Fe bytecode instructions (see Fe source: fe.c, instruction set ~30–50 opcodes)
  2. Constant table: literals (numbers, symbols, strings) referenced by instructions
  3. Symbol table: (optional, for debugging) maps symbols to indices

Details:

  • No header — raw bytecode
  • Size given by header’s bytecode_size
  • The nOSh runtime loads this blob into memory and hands to Fe evaluator
  • Fe evaluator executes: initialize arena, set up builtins (NoshAPI bindings), evaluate bytecode

Desktop compilation flow:

source.lsp → Fe compiler → bytecode + constants → packager → .kn86

Device loading flow:

.kn86 file → read header → validate version → load bytecode into arena → Fe evaluator

Note: Fe does not require a separate “compile to bytecode” step in the published version. The reference Fe implementation reads source directly. For .kn86 v2.0, we have two options:

  1. Store source in bytecode section: ship .lsp text directly. Advantage: interpreter reads it at load. Disadvantage: larger carts (~5–10 KB per cart for typical size).
  2. 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.


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:

TypeTagPayload formatExample usage
SPRITES1Raw 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_PATTERNS2PSG register dump sequences. Format: [pattern_id (2B), register_sequence…]. Used for pre-canned sounds (horn, alert, etc.).SFX assets
STRINGS3String table: [string_id (2B), length (2B), null-terminated string…]. Cartridge code references strings by ID.Mission templates, UI text
MISSIONS4Mission template data: binary serialization of mission structures (objectives, threat ranges, phase chains, payout formulas). Parsed at mission-board generation time.Mission board generation
CART_CAPABILITIES5Length-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)
END0No 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]

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 budget

Usage:

  • 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.


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 registry
struct 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 dispatch
if (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.


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:

  1. Carve bytes out of the 80-byte header _reserved1 / _reserved2 regions. 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.
  2. 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.

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-escape is not the same token as Cipher-Main-Grid-Escape.
  • Keyword length: 3–31 ASCII bytes inclusive. A len byte outside [3, 31] is a malformed cart and raises :capability-block-malformed at 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.
  • _reserved byte: must be 0x00 in 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 in 0..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.

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:

  1. Read .kn86 header.
  2. Validate magic and version.
  3. Check req_api_version against nOSh runtime version; abort if mismatch. 3a. Parse the CART_CAPABILITIES subsection if present (new, 2026-04-24):
    1. Scan the static-data section (already sized by the header) for a CART_CAPABILITIES subsection. Only one instance is permitted per cart; a second instance is :capability-block-malformed.
    2. If absent, the cart requests no capabilities — proceed to step 4.
    3. 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).
    4. For each declared keyword, look it up in the nOSh runtime’s baked-in allowlist (see §Allowlist below), keyed by the cart’s cart_id from 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.
    5. On success, record the granted capability set in the cart’s per-load runtime state. Per-capability FFI gates (e.g., cipher-emit-main-grid from ADR-0015 §3a) read this set at call time.
  4. Allocate arena (size negotiated: header may hint cart_arena_size, nOSh runtime selects 16–32 KB).
  5. Load bytecode section into arena.
  6. Initialize Fe interpreter in arena.
  7. Load static data section into nOSh runtime memory (separately, not in cart arena).
  8. Call cartridge cart_init Lisp 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.

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:

Cartcart_idGranted keywords
NullTBD at Null packaging time (hash of "NULL" + "CIPHER_ANALYSIS")cipher-main-grid-escape
ICE Breakerfrom packaging(none)
Depthchargefrom packaging(none)
Black Ledgerfrom packaging(none)
Neongridfrom packaging(none)
(all other launch carts)(none)

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:

  1. A new ADR sanctions the exception (matching the ADR-0015 §3a precedent).
  2. A nOSh runtime PR adds a row to kn86_capability_allowlist for the granted cart.
  3. 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).
  4. The cart’s packaging pipeline adds the keyword to its CART_CAPABILITIES subsection.
  5. No .kn86 format change is required. No version bump, no new subsection type, no re-work of existing cartridges. This is the extensibility contract.

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 codeWhen it firesReported detail
:capability-block-malformedThe 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-grantedA declared keyword has no matching allowlist row for this cart’s cart_id.The first un-granted keyword string, verbatim.
:capability-deniedRuntime, 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:

  1. 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.
  2. 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.
  3. 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).
  4. 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.

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 .kn86 format 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”).


At cartridge load (nOSh runtime, once per swap)

Section titled “At cartridge load (nOSh runtime, once per swap)”
  1. Read .kn86 header.
  2. Validate magic and version.
  3. Check req_api_version against nOSh runtime version; abort if mismatch. 3a. Parse and validate the CART_CAPABILITIES static-data subsection if present (added 2026-04-24; see §Cart-Capabilities Block). Reject cart-load with :capability-block-malformed or :capability-not-granted as specified there. On success, record the granted capability set.
  4. Allocate arena (size negotiated: header may hint cart_arena_size, nOSh runtime selects 16–32 KB).
  5. Load bytecode section into arena.
  6. Initialize Fe interpreter in arena.
  7. Load static data section into nOSh runtime memory (separately, not in cart arena).
  8. Call cartridge cart_init Lisp function:
    • Register cell types
    • Create initial cell structures (UI cells, lists, etc.)
    • Seed LFSR
    • Return to nOSh runtime

At mission start (nOSh runtime, per mission)

Section titled “At mission start (nOSh runtime, per mission)”
  1. Clear all user-spawned cells (cells created after cart_init)
  2. Call cartridge on-mission-start handler (if present in Lisp)
  3. Generate mission board via procedural gen
  4. Player enters mission phase

At mission phase boundary (nOSh runtime, per phase)

Section titled “At mission phase boundary (nOSh runtime, per phase)”
  1. Current phase’s on-exit handler fires
  2. Next phase’s on-enter handler fires
  3. 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)”
  1. 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.)
  2. Deallocate cart arena (implicit — no cleanup needed, arena is just a pointer reset)
  3. Unload static data from nOSh runtime memory

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)

Typical cartridge (e.g., ICE Breaker):

ComponentSize
Header80 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.


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
BYTECODE SECTION (8192 bytes):
[Fe-compiled Lisp bytecode for ice_breaker.lsp]
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: 0x12345678

Terminal window
kn86_compiler --source ice_breaker.lsp \
--sprites sprites.png \
--psg-patterns sfx.txt \
--missions missions.txt \
--output ice_breaker.kn86 \
--debug \
--cart-id 0x3F2A1B48

Tool responsibilities:

  1. Parse .lsp, compile to Fe bytecode (or store source)
  2. Load sprite assets (PNG → packed bitmap)
  3. Load PSG patterns (text format → register sequences)
  4. Load mission templates (structured data → binary)
  5. Assemble .kn86 container
  6. Compute checksum
  7. Strip debug section if --no-debug flag
Terminal window
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)

  1. Bytecode format: This spec assumes Fe source is shipped. Phase 2 will define a custom bytecode format (Fe bytecode or a simpler stack VM bytecode). Format change is backward-compatible via version field.

  2. 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). The cart_save / cart_load NoshAPI 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).

  3. 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 via udev events on the USB mass-storage device backing the cart (per ADR-0019) — replacing the prior interim ADR-0013 mechanism (/CS line on the DMG edge connector) — and drives the unload/reload sequence in §Loading Semantics. No format change is required.

  4. 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-.kn86 integrity field — both are still not cryptographic security and remain deferred.

  5. Cart-capabilities serialization (ADR-0015 §3a follow-on): RESOLVED 2026-04-24 by this ADR’s 2026-04-24 amendment. Capabilities live in a new CART_CAPABILITIES static-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-granted at Row 24. See §Cart-Capabilities Block.

  6. Prose-vs-struct header-size discrepancy. RESOLVED 2026-04-24 (editorial pass, GWP-212, commit efb0f87). The ADR-0006 prose diagram, §Header heading, struct Total: comment, §Size Estimates table, and the ICE Breaker example (offsets + kn86_inspect output) now all state 80 bytes, matching the canonical CartridgeHeaderV2 struct in this ADR and the KN86CART_HEADER_SIZE constant in tools/kn86cart/format/kn86cart.h (whose _Static_assert enforces the 80-byte size at compile time). The legacy “64 bytes” appears to be a stale counting error from before the struct grew req_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.


.kn86 v2.0 is a portable, modular, inspectable cartridge format. It holds Lisp bytecode (or source), static assets, and optional debug info. The nOSh runtime loads it once per cartridge swap, initializes the Fe VM in an arena, and dispatches handlers accordingly. The format is designed for extensibility: new asset types can be added without breaking old cartridges.

Ready for packaging tool implementation (Phase 2) and initial cartridge shipping.


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:

  1. Related section: added ADR-0013-cartridge-physical-format.md.
  2. 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.
  3. 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).
  4. 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.
  5. 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).
  • .kn86 header 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:

  1. Related section: added ADR-0015-cipher-line-auxiliary-display.md.
  2. Summary section, bullet #3: static data section now lists cart-capabilities block as one of its contents.
  3. Static-data subsection type table: added CART_CAPABILITIES (type=5) row.
  4. 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.
  5. 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.
  6. §Example (ICE Breaker): clarified as “no capabilities” and a new Null example added showing the CART_CAPABILITIES subsection in situ.
  7. Known Unknowns #5: new entry recording the now-resolved ADR-0015 §3a follow-on.
  8. 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.)
  • .kn86 header layout, bytecode section, checksum format, debug section layout, sprite/PSG/strings/missions subsection tagging, size estimates, and the format version number (still 2) — 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_CAPABILITIES subsection is optional.
  • kn86cart CLI tool — no behavior change required for the v0.1 MVP, which already emits a minimal static-data section. A future --capability flag 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-grid on 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_CAPABILITIES subsection, records zero granted capabilities, and every capability-gated FFI primitive returns :capability-denied at 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 .kn86 now 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 the CART_CAPABILITIES subsection defined here. §14 “Cross-Cutting Concerns” bullet for ADR-0006 (which references a separate proposed CIPHER_GRAMMAR section) 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. The CART_CAPABILITIES = 5 constant 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 --capability flag) 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:

  1. The struct is compiler-verified: tools/kn86cart/format/kn86cart.h includes _Static_assert(sizeof(kn86cart_header_t) == KN86CART_HEADER_SIZE, "...must be exactly 80 bytes"). The tooling cannot build with a wrong size.
  2. Every tool in the repo that reads or writes .kn86 files uses 80 bytes.
  3. 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_offset each paired with a size), and checksum from its implicit sum — a counting error, not a format choice.

Changes in this amendment:

  1. §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).
  2. §Section Details → §Header heading: ### Header (64 bytes, little-endian)### Header (80 bytes, little-endian).
  3. §Header struct comment: /* Total: 64 bytes *//* Total: 80 bytes */.
  4. §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.
  5. §Example: Complete ICE Breaker v2.0 Cartridge: updated bytecode_offset, static_data_offset, and debug_offset from 64 / 8256 / 12352 to 80 / 8272 / 12368 to match the corrected header length. Sizes (bytecode_size, static_data_size, debug_size) are unchanged. The kn86_inspect sample output below it was updated to match.
  6. §Known Unknowns #6: marked RESOLVED with a pointer to this amendment.
  7. Front-matter Amended: line: appended the 2026-04-24 (editorial) entry.

What did not change:

  • Original Status: Accepted (unchanged).
  • CartridgeHeaderV2 struct itself — field list, types, offsets — unchanged. The struct was already correct.
  • .kn86 binary format: no change. Carts on disk are byte-identical.
  • KN86CART_HEADER_SIZE constant: 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.hNOTE ON V2 HEADER SIZE block 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 (beyond header.rs) and kn86-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:

  1. 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)).
  2. Related list: added ADR-0019-cartridge-storage-and-form-factor.md; ADR-0013 marked as superseded by ADR-0019 inline.
  3. 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 standard read() against the mounted SD filesystem rather than MBC5 bank-switching.
  4. 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.)
  5. 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.
  6. Known Unknowns #3 (hot reload): prose updated to point to ADR-0019; the cartridge-presence detection mechanism becomes udev events on the USB mass-storage device backing the cart, replacing the prior interim ADR-0013 /CS mechanism.
  7. 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-.kn86 integrity field) are noted in passing. Still deferred; still not cryptographic security.

What did not change:

  • Original Status: Accepted (unchanged).
  • .kn86 header layout, bytecode section, static data sub-section tagging, debug section layout, checksum format, CART_CAPABILITIES block, size estimates, and the format version number (still 2) — 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_load NoshAPI 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.