Skip to content

Sprint 3 Design Pack — GWP-258 — nOSh event bus pub/sub core

Notion task: GWP-258
Recovered: 2026-04-26 (lost to worktree contention; restored from Notion block children API)


Implement the mission_board_init / mission_board_regenerate generator pipeline per docs/plans/post-v0.1/2026-04-21-mission-board.md. Today GWP-165 is parked at Low priority “deferred” — but the design document is complete and the generator is the unlock for every multi-phase contract type.

The mission board generator is the architectural keystone for the capability model. The post-v0.1 plan is design-complete; the only blocker is engineering time. Decomposing GWP-165 here into a tight, dispatchable story so SM can pick up. Open questions noted in the plan are PM’s domain, not the engineer’s.

References: docs/plans/post-v0.1/2026-04-21-mission-board.md, docs/software/runtime/orchestration.md §“Mission Board”.

  • New module src/mission_board.c and .h with MissionInstance struct matching the design spec L52-69
  • mission_board_init(SystemState*) parses defmission forms from loaded cartridges into a template pool
  • mission_board_regenerate(trigger) produces 4-8 MissionInstances using the LFSR-seeded generator
  • Refresh triggers (:tab_enter, :contract_completed, :contract_abandoned, :cartridge_inserted, :cartridge_ejected, :sys_refresh, :relay_applied) all wired
  • Reputation-tier and balance-tier modifiers honored (low-rep gets threat 1-3, high-rep unlocks 4-6)
  • mission_board_peek snapshot accessor for Cipher engine and REPL (ADR-0005 Tier 3)
  • Event publication via nosh_event_publish for Cipher, REPL log, and future listeners
  • Test test_mission_board covers: empty pool, full pool, reputation tiering, regenerate determinism (same inputs produce same board)
  • The 9 open questions in the plan are PM’s to resolve; engineer files a question doc rather than guessing
  • Owns: src/mission_board.c and .h (new).
  • Touches additively: tests/test_mission_board.c, CMakeLists.txt, types.h (MissionInstance, MissionBoardGen).
  • Must NOT modify: deck.c (DeckState), main.c (event subscribers wire up in a follow-up).
  • TDD; cite the design plan path in commit messages.

GWP-245 (mission-context Tier-2 primitives) — phase handoff depends on phase-advance / mission-complete. Land GWP-245 first.

GWP-165 (existing deferred umbrella) gets retired as duplicate once this lands.

  • Genre template library (post-launch)
  • LAMBDA predicate-macro slot type vs. keystroke macro distinction: per design §‘Open Questions’ #4 — this PR implements (a): introduce a second lambda-slot type. Decisive guidance, not deferred.

Implementation sequencing inside the PR (or two PRs)

Section titled “Implementation sequencing inside the PR (or two PRs)”
    1. Build event bus first (dependency; board uses it constantly). Tests: publish/subscribe, fan-out, payload integrity, no-subscriber publish (no-op).
    1. Define MissionInstance and MissionBoardGen structs in mission_board.h.
    1. Implement mission_board_init — template pool parsing only, no randomness. Test fires mission_board_ready event.
    1. Implement deterministic generator core: board-local LFSR, tier derivation, instance minting. Test deterministic property (AC #11/#12) before going further — gates everything.
    1. Implement mission_board_regenerate(:tab_enter) end-to-end. Wire mission_board_refreshed event.
    1. Implement remaining six refresh triggers one at a time with a test each.
    1. Implement mission_board_peek snapshot.
    1. Implement input handling (mission_board_handle_input) for CAR / CDR / QUOTE / EVAL / BACK at minimum. CONS / LAMBDA / ATOM / NIL / INFO can ship in follow-up if PR is too large.
    1. Implement mission_board_render with row layout from design.
    1. Wire it into main.c (replace stub).
    1. Run full-lifecycle test (AC #13) — should pass.

Sprint 3 Design Pack (PM/BA + Gameplay Designer enrichment, 2026-04-26)

Section titled “Sprint 3 Design Pack (PM/BA + Gameplay Designer enrichment, 2026-04-26)”

Disk source of truth: docs/sprints/2026-04-26-sprint3-gwp-258-design.md. Original ACs above are preserved as audit trail; the items below ENRICH scope without superseding them.

The mission board is the architectural keystone of the capability model. Every claim the platform makes — that cartridges are capability modules, that ICE Breaker is a ‘complete career’ out of the box, that loading Black Ledger turns the deck into a multi-disciplinary intelligence firm — depends on a runtime that generates contracts on demand from cartridge templates seeded by Universal Deck State. 2026-04-21-mission-board.md is design-complete and ratifies a specific data model (MissionInstance, MissionBoardGen), a specific lifecycle (mission_board_init at boot → lazy mission_board_regenerate on first MISSIONS-tab entry → refresh on completion/abandonment/cart-insert/SYS-refresh), a specific event taxonomy (the nine mission_* events that publish onto the firmware topic bus and feed Cipher, the REPL, and nEmacs), and a determinism guarantee. None of that exists yet — g_state.mission_board in main.c is a hand-rolled stub of two hardcoded contracts. This story builds the real generator and the real event bus.

2. Player-Facing Semantics (Gameplay Designer)

Section titled “2. Player-Facing Semantics (Gameplay Designer)”

The mission board is THE gameplay surface the operator returns to between phases. From the operator’s seat: the board feels alive. Insert a new cart and the board visibly refreshes — new contracts appear, threat ceiling rises, multi-phase entries with the cart’s domain tags surface. Complete a contract and the row vanishes; a new one slides in. Reputation climbs and easy contracts thin out — the board pushes the operator upward. None of this is decorative; it is the difference between a deck that generates a career and a deck that holds a list of preset levels.

UI surface: MISSIONS tab. Row 1 = CONTRACTS … BOARD 0xXXXXXXXX / N (board hash on right is top 8 hex of board_seed — doubles as refresh receipt). Rows 2–8 = 4–8 contract list. Rows 14–22 = inspector drill-in. Row 24 = context-sensitive action bar.

Input grammar: CAR=drill into contract (collapses to inspector at rows 14–22; second CAR promotes to full-screen). CDR=next contract (wraps). CONS=pair two bookmarked contracts. QUOTE=bookmark to numpad slot 1–8. EVAL=bid → second EVAL=accept. LAMBDA=record bid-predicate macro. ATOM=test single-phase. NIL=clear in-progress CONS pairing. INFO=short scan; double-tap opens full inspector. BACK=return to HOME tab.

Worked example — one full mission generation cycle from a known seed

Section titled “Worked example — one full mission generation cycle from a known seed”

Operator boots the deck, picks handle ‘HOSHIKO,’ and inserts ICE Breaker for the first time. Deck state:

operator_handle: "HOSHIKO"
credit_balance: 0 (fresh deck)
reputation: 0 (Apprentice tier)
cartridge_history: 0x00000001 (bit 0 = ICE Breaker, just-set)
cipher_seed: 0xDEADBEEF (factory seed)
phase_chain_len: 0 (no suspended mission)

Runtime calls mission_board_init(&g_state) at boot — parses the defmission forms from ICE Breaker’s template region. ICE Breaker contributes ~4 base templates (PENETRATION, EXTRACTION, SABOTAGE, SURVEILLANCE IMPLANT). Init emits mission_board_ready{template_count: 4, domain_tags: [network, digital]}.

Operator switches to MISSIONS tab. First entry triggers mission_board_regenerate(:tab_enter). Generator computes:

reputation_tier = 0 (Apprentice; reputation 0 → tier 0)
balance_tier = 0 (Broke; credit_balance 0 → tier 0)
capability_mask = 0x00000001 (only bit-0 carts present)
relay_overlay_ver = 0 (no Relay)
instance_count = 5 (Apprentice + Broke → generous 5-contract layout)
board_seed = lfsr_advance(0xDEADBEEF, 1) = 0x21D84E00

First instance:

template_handle = 0 (PENETRATION)
narrative_seed = lfsr_advance(board_seed, 0) = 0x21D84E14
threat_level = clamp(template.threat_range, reputation_tier) = 1
phase_count = 1
domain_tags = [network]
payout_credits = 800 × 1.0 × 1.25 (broke bonus) = 1000 ¤
contract_id = 0x1023
flags = 0

Repeat for instances 2–5. Generator advances cipher_seed by instance_count + 1 = 6 LFSR steps, persists, fires mission_board_refreshed{trigger: :tab_enter, instance_count: 5, max_threat_level: 1, has_multi_phase: false, has_scripted: false, relay_overlay_ver: 0}.

Board renders:

Row 0 HOSHIKO ¤0 REP:[ ] T1
Row 1 CONTRACTS BOARD 0x21D84E00 / 5
Row 2 [>] PENETRATE: SHELL CORP RELAY T1 ¤1,000 NET
Row 3 [ ] EXTRACT: STAFF DIRECTORY T1 ¤900 NET
Row 4 [ ] SABOTAGE: TEMP BACKUP HOST T1 ¤1,200 NET
Row 5 [ ] SURVEILLANCE: MAILROOM TERMINAL T1 ¤700 NET · *persist
Row 6 [ ] PENETRATE: WIRELESS PRINT POOL T1 ¤1,100 NET
Row 23 HOME [MISSIONS] REPL LINK STATUS
Row 24 CAR=detail CDR=next QUOTE=bookmark EVAL=bid INFO=scan SYS=menu
[CIPHER-LINE auxiliary OLED]
Row 1 HOSHIKO ¤0 · DEV · 18:03
Row 2 five out, all light.
Row 3 apprentice work. clean if untripped.
Row 4 (blank — no mission selected yet)

All five contracts are threat-1 single-phase — reputation cap doing its job. Broke-bonus payouts inflated. CIPHER-LINE has spoken (Cipher engine consumed mission_board_refreshed and elected to emit two :observe-mode fragments using ICE Breaker’s vocabulary contributions).

Operator EVALs row 2. Board emits mission_bid{contract_id: 0x1023, threat_level: 1, payout_credits: 1000, narrative_seed: 0x21D84E14}. Pattern 5 confirm dialog renders in Zone D (rows 14–22). Operator EVALs again. Board emits mission_accepted{contract_id: 0x1023, threat_level: 1, phase_count: 1, domain_tags: [network], narrative_seed: 0x21D84E14, starts_phase: 1} and hands off to ICE Breaker phase handler. Row 2 vanishes from board immediately; replacement contract slides in only after phase completion (per :contract_completed partial-refresh trigger).

That is one full cycle: known seed → deterministic instances → render → operator selection → bid → accept → board state mutation. Every step deterministic, every step event-published, every step inspectable from the dev REPL.

3. Acceptance Criteria (enrichment — 14 testable items)

Section titled “3. Acceptance Criteria (enrichment — 14 testable items)”
  • Module placement: kn86-emulator/src/mission_board.{c,h}; public surface is six functions: mission_board_init, mission_board_regenerate, mission_board_peek, mission_board_publish_event, mission_board_handle_input, mission_board_render. Internal helpers static.
  • MissionInstance struct exact match with 2026-04-21-mission-board.md L56–72: contract_id, template_handle, source_cartridge_bit, threat_level, phase_count, domain_tags_count, domain_tags[4], payout_credits, reputation_delta_pass, reputation_delta_fail, flags, narrative_seed.
  • MissionBoardGen struct exact match (L41–51): board_seed[4], reputation_tier, balance_tier, capability_mask, relay_overlay_ver, instance_count, next_contract_id. Pure SRAM; not persisted.
  • mission_board_init parses defmission forms for every cartridge whose bit is set in cartridge_history AND is currently inserted. Lambdas held as opaque Fe bytecode handles. Emits one-shot mission_board_ready{template_count, domain_tags[]}.
  • mission_board_regenerate deterministic generation: derive reputation_tier and balance_tier from deck state; compute capability_mask; pick instance_count from tier-product table (4–8). For each instance, advance board’s own LFSR (NOT cipher_seed), pick template by weighted draw, mint narrative_seed, fill all MissionInstance fields. After all instances minted, advance deck’s cipher_seed by exactly instance_count + 1.
  • Refresh trigger taxonomy: implement all seven triggers (:first_tab_enter, :contract_completed, :contract_abandoned, :cartridge_inserted, :cartridge_ejected, :sys_refresh, :relay_applied). Each emits mission_board_refreshed{reason, …} or mission_board_partial_refresh{removed_id, added_id} per design. Cartridge ejection does NOT remove contracts requiring ejected cart — they survive as locked rows with *insert modifier.
  • Event bus: implement nosh_event_publish(topic, payload) (firmware-internal topic bus, fan-out, no per-subscriber filtering). All nine mission_* events publish to it: mission_board_ready, mission_board_refreshed, mission_board_partial_refresh, mission_highlight_changed, mission_bid, mission_accepted, mission_suspended_resume_available, mission_required_cartridge_missing, mission_plan_paired, mission_info_requested, mission_board_empty.
  • mission_board_peek snapshot: read-only stable snapshot of MissionBoardGen plus live MissionInstance[]. Caller (Cipher, REPL) gets a copy valid until next refresh. Does NOT advance any LFSR or mutate deck state.
  • Suspended-mission injection: when phase_chain_len > 0 on regeneration, first row replaced with RESUME: — PHASE N/M; carries persistent *resume modifier glyph. Emits mission_suspended_resume_available with :significant affect tag.
  • Empty-state handling: no carts AND no runtime bounties → Zone B becomes single row ‘(insert a capability module to populate the board)’; emits one-shot mission_board_empty{reason: :no_cartridge}.
  • narrative_seed separation: each MissionInstance.narrative_seed minted from board’s own LFSR, NOT cipher_seed. Test asserts 100-iteration regeneration loop produces identical narrative_seed sequences across reruns from same starting cipher_seed.
  • Test fixture (deterministic regeneration) in kn86-emulator/tests/test_mission_board.c: ‘two boots, identical state’ test — snapshot deck state, run init + regenerate, capture instance list (all fields). Reset, repeat. Assert byte-identical instance arrays.
  • Test fixture (full lifecycle): single test walks init → regenerate → CAR → highlight_changed event → CAR again → drill-to-inspector event → EVAL → bid event → EVAL → accepted event → contract removed from board. All event payloads verified.
  • Build wiring: CMakeLists.txt SOURCES updated; new ctest targets; make + ctest pass clean; scan-build reports zero new findings.
  • Tag interning collision at cart load: two carts declare the same :class symbol with different intended meanings. Per design §‘Tag interning,’ second registration rejected with main-grid error banner and :cart-load-error event. Board MUST NOT attempt to merge.
  • Instance count exceeds MissionInstance array bound: hard cap at 8; clamp with debug log, not a runtime panic.
  • Empty template pool but cart inserted: malformed cart loads cleanly but contributes zero defmission forms. Board renders empty-state row; emits :cart-load-warning instead of mission_board_empty.
  • All eligible templates filtered out by reputation/threat clamp: Apprentice operator inserts cart with all :threat-range 4 6 templates. Board emits mission_board_empty{reason: :no_eligible_templates}; renders empty-state with helper text. Cipher MAY emit a ‘ceiling too low’ fragment via mode selector.
  • Cart ejected mid-bid: operator hits EVAL, confirm dialog opens, then physically ejects required cart before second EVAL. Second EVAL must check cart-presence; on miss, emit mission_required_cartridge_missing and show banner; do NOT advance to mission_accepted.
  • Suspended mission’s required cart no longer in cartridge_history: should be impossible (history append-only) but guard. Emit soft warning, treat suspended row as stale that CDR skips; do NOT crash on phase_chain replay.
  • narrative_seed collision across instances within one board: astronomically unlikely with 32-bit seed, but test fixture sanity-checks uniqueness. Collision indicates LFSR reseed bug.
  • LAMBDA macro mid-regenerate: operator records ‘bid on threat-2 NET contracts’ lambda; new cart insertion triggers full regenerate before lambda’s predicate finds match. Macro persists in lambda_slots; on new board, scan instances and auto-fire mission_macro_invoked if match exists.
  • Relay overlay tag-collision with cart tags: per design §‘Open Questions’ #9, cart wins; firmware emits warning banner; emits :cart-load-warning event. Test precedence with synthetic Relay overlay.
  • docs/plans/post-v0.1/2026-04-21-mission-board.md — primary design.
  • docs/plans/post-v0.1/2026-04-25-mission-composition-grammar.md — defines mission-contributions block schema, verb vocab, affinity tag set.
  • docs/plans/post-v0.1/2026-04-25-mission-genre-templates.md — genre template library (post-launch); not in scope but data model must accommodate.
  • docs/software/runtime/orchestration.md §‘The Mission Board’ — capability-model context.
  • docs/software/runtime/deck-state.md — UDS struct; generator reads operator_handle, credit_balance, reputation, cartridge_history, cipher_seed, phase_chain_len, phase_chain; writes cipher_seed.
  • docs/software/runtime/cartridge-lifecycle.md — :cartridge_inserted and :cartridge_ejected refresh triggers wire into this lifecycle (GWP-259 peer story).
  • docs/software/runtime/cipher-voice.md — Cipher engine consumes mission_* events via cipher-push-event; this story produces but does NOT call cipher-push-event directly.
  • docs/software/cartridges/authoring/campaign-economy.md — 8–10 multi-phase campaign archetypes consume the generator’s multi-phase output path.
  • docs/adr/ADR-0007-scripted-mission-ffi.md — scripted-mission contracts use the same template pool; generator marks scripted templates with λ modifier glyph. Acceptance-contract handles flow through to GWP-248’s runner.
  • docs/adr/ADR-0005-ffi-surface.md — REPL Tier-3 read-only primitives bind to mission_board_peek snapshot.
  • docs/adr/ADR-0015-cipher-line-auxiliary-display.md — CIPHER renders only on OLED, never main grid; board’s main-grid render contract honors this.
  • docs/adr/ADR-0011-device-firmware-update-system.md — Relay overlay flow; :relay_applied refresh trigger wires into this.
  • docs/software/cartridges/modules/ice-breaker.md §‘Mission Contributions’ — DIGITAL+NETWORK anchor; baseline calibration.
  • docs/software/cartridges/modules/black-ledger.md §‘Mission Contributions’ — multi-phase campaign partner; cross-program integration test target.
  • docs/software/cartridges/modules/depthcharge.md §‘Mission Contributions’ — required for MARITIME INTELLIGENCE 3-phase archetype.
  • docs/software/cartridges/modules/null.md §‘Mission Contributions’ — diagnostic cart; meta-mission template exercises inspector path.
  • docs/software/cartridges/modules/neongrid-blackledger-campaign.md — explicit cross-cart campaign spec.
  • kn86-emulator/src/mission_board.c (new, ~800–1100 LOC C)
  • kn86-emulator/src/mission_board.h (new, public surface — six functions plus two structs)
  • kn86-emulator/src/nosh_event_bus.c (new or factored from main.c, ~150–250 LOC — shared infrastructure)
  • kn86-emulator/src/nosh_event_bus.h (new — nosh_event_publish, nosh_event_subscribe, nosh_event_init)
  • kn86-emulator/tests/test_mission_board.c (new — full-lifecycle + determinism + edge-case tests)
  • kn86-emulator/tests/test_nosh_event_bus.c (new — bus mechanics tests)
  • kn86-emulator/CMakeLists.txt (update SOURCES, register two new test targets)
  • kn86-emulator/src/main.c (modify: replace hand-rolled stub board with calls to mission_board_init / regenerate / render / handle_input)

~1500–2000 LOC including tests. LARGE PR — at upper edge of reviewable in one pass. Consider stack: PR #1 ships event bus only, PR #2 ships mission board on top. SM’s call. If single-PR, review must be structured (engineer narrates with section guide).

TDD per test-driven-development skill. Full-lifecycle test (AC #13) is the ONE test that should be written first as the integration target — every other test supports it. Determinism (AC #11/#12) is a class of properties verified across many tests, not a single test.

  • nEmacs scripted-mission editing mode (consumer of mission_accepted{scripted: true}; separate story)
  • Full Cipher engine (this story only PUBLISHES events; Cipher consumption is GWP-260 territory the peer agent is handling)
  • REPL mission-* primitives (consume mission_board_peek; separate story; can land in parallel)
  • Tutorial board for first-boot (separate story)
  • Mission Composition Grammar’s algorithmic multi-phase synthesis (post-launch; generator must accept mission-contributions blocks but does not yet COMPOSE missions from them)

The design plan itself surfaces 9 open questions. This story implements with provisional answers; please confirm or override before SM dispatches:

  • cartridge_history width (uint16_t vs uint32_t): canonical Capability Model spec and docs/software/runtime/deck-state.md L22 say uint32_t. Provisional answer: 32-bit. Bare-deck-spec uint16_t reference is superseded. Confirm and we close it permanently with a doc-sweep follow-up. PM action if confirmed: open Sprint 4 task to grep for stale uint16_t cartridge_history references.
  • Mission Board as tab vs. as post-boot screen: provisional answer is MISSIONS tab inside four-tab Bare Deck Terminal hub per current canonical docs/software/runtime/bare-deck-terminal.md. The Capability Model spec line ‘primary interface after boot’ is older and superseded; please ratify so engineering brief is unambiguous. Both questions answerable in one Slack message; neither is a blocker if engineer ships per provisional answers. Other design OQs (Cipher latency, LAMBDA macro type, ejected-cart Cipher behavior, SYS-refresh cost, REPL simulate-accept gating, Relay tag collision) are either resolved by ADR-0015 or out of scope for this PR.
  • Blocks: GWP-248 does NOT strictly block this — runs in parallel. Interface contract: this story exposes acceptance_contract_handle as opaque Fe bytecode pointer on MissionInstance-derived ScriptedMissionContext; GWP-248 consumes that handle in its _run request struct. No version coupling beyond ‘Fe bytecode handle, opaque.’
  • Blocked by: Nothing in Sprint 3. Design plan is design-complete. Peer agent’s Sprint 3 work (GWP-259, GWP-260) consume this story’s events but do NOT block its implementation.
  • Unblocks: every multi-phase contract type in campaign-economy.md; Mission Composition Grammar’s algorithmic synthesis (post-launch); REPL mission-* primitives; Cipher engine subscriptions to mission_* events (GWP-260); cart-lifecycle FSM event hooks (GWP-259).
  • Suggested wave: Sprint 3, Wave 1 — alongside GWP-248. Both foundational. Peer agent’s GWP-259 should also ship Wave 1 (produces cart-lifecycle events this story subscribes to); GWP-260 in Wave 2 (consumes events this story produces). Order: bus + board + lifecycle in Wave 1 → Cipher engine + REPL bindings in Wave 2.