Changes
diff --git a/designs/design-0-mcp.md b/designs/design-0-mcp.md
new file mode 100644
index 0000000..fcb81db
--- /dev/null
+++ b/designs/design-0-mcp.md
@@ -0,0 +1,614 @@
+# NetHack MCP server and spectator UI
+
+Status: proposed design; implementation has not started.
+
+## 1. Purpose and decisions
+
+Build a program through which an LLM agent can play NetHack using MCP,
+while a person watches the same game in a read-only UI. The application
+owns the game session, but does not call an LLM itself. An external MCP
+client supplies the agent and decides when to invoke tools.
+
+Confirmed requirements:
+
+- All new server and engine integration code uses C++23. NetHack remains
+ C. Use libmw where it provides suitable utilities.
+- The UI is read-only. It has no gameplay inputs, reset button, or manual
+ takeover mode.
+- ASCII graphics are sufficient.
+- Fetch NetHack, libmw, and other project dependencies through CMake
+ FetchContent with pinned revisions. Common libraries such as curl and
+ OpenSSL may come from the system; system Lua is also permitted.
+- This document specifies the design only; it does not authorize
+ implementation.
+
+Proposed defaults:
+
+- A browser spectator page is the first UI. A terminal spectator can be
+ added later using the same observation endpoint.
+- One active game per MCP server process; multiple spectators may watch.
+- MCP uses stdio. A separate loopback HTTP listener serves the viewer.
+- A single executable has a server mode and an internal engine-worker
+ mode. These run as separate processes.
+- Tools expose individual keys and structured prompt/menu responses.
+ Semantic commands such as `move(north)` are deferred.
+- Each game uses a private working directory. Normal NetHack rules apply;
+ hidden engine state is not exposed to the agent.
+
+Non-goals for the first version include multiple simultaneous games in
+one server, remote hosting, an autonomous agent loop, tiles, replay
+playback, arbitrary NetHack configuration, and high-level navigation.
+
+## 2. Evidence and integration constraints
+
+The inspected reference checkout is `NetHack/`, branch `NetHack-5.0`,
+commit `c94fd5225beef48143244bfb7bc42682aad58741`. It was used for
+research only. The application build must fetch NetHack through
+FetchContent and must not require this local directory.
+
+The relevant local source files are:
+
+| Source | What it establishes |
+| --- | --- |
+| [libnh README](../NetHack/sys/libnh/README.md) | Library entry point and shim callback registration |
+| [shim implementation](../NetHack/win/shim/winshim.c) | Actual callback names, arguments, and return types |
+| [window types](../NetHack/include/wintype.h) | Glyph and menu identifier layouts |
+| [window interface](../NetHack/include/winprocs.h) | Typed window-port functions |
+| [window specification](../NetHack/doc/window.txt) | Semantics of drawing, menus, prompts, and status |
+| [role selection](../NetHack/src/role.c) | Generic character selection through window callbacks |
+| [termination](../NetHack/include/extern.h) | Unix `nethack_exit` maps to process `exit` |
+| [library startup](../NetHack/sys/libnh/libnhmain.c) | Startup, working-directory changes, and game loop |
+
+The shim is a UI interface, not a turn-based engine API. NetHack calls
+into the port to draw and request input. Input callbacks may occur during
+character creation, normal commands, direction selection, menus, or
+end-of-game disclosure. A request for a key does not necessarily mean a
+new game turn has started.
+
+Some input is already structured: text prompts supply a buffer, menus
+return selected identifiers, and extended commands return an index into
+the command table. We should preserve these distinctions.
+
+The native library build has not yet succeeded locally. The initial
+attempt generated ignored build artifacts but failed to find its expected
+Lua dependency. System Lua 5.4.8 exists, but the upstream distribution
+detection does not recognize this Gentoo installation. The library target
+also names a bundled Lua archive explicitly. Therefore system-Lua support
+must be verified as part of implementation, not assumed from its presence.
+
+The [official upstream library documentation](https://github.com/NetHack/NetHack/blob/NetHack-5.0/sys/libnh/README.md)
+is a useful overview. Exact ABI details must come from the pinned fetched
+headers: for example, the current glyph layout nests color information
+inside `glyph_info.gm`, rather than matching older examples verbatim.
+
+## 3. Process architecture
+
+```mermaid
+flowchart LR
+ Agent[External LLM / MCP client] <-->|MCP stdio| Server[C++ server]
+ Server <-->|Private framed JSON channel| Worker[C++ engine worker]
+ Worker <-->|Window callbacks| NH[NetHack static library]
+ Browser[Read-only browser] -->|GET snapshots| Server
+```
+
+The parent owns MCP, HTTP, worker supervision, and immutable observations.
+The worker owns every call into NetHack and all NetHack global state.
+This separation is necessary because normal NetHack termination calls
+`exit()`. Running the game on a parent-process thread would still exit
+the entire server when the game finishes.
+
+Launch the same executable with an internal `--engine` argument using
+`posix_spawn` and descriptor actions. Do not fork and then run C++ game
+code inside an already multithreaded parent. Pass a private socket or pipe
+pair explicitly. The worker must never inherit the MCP stdout descriptor
+as its ordinary stdout. Redirect ordinary worker output to a diagnostic
+pipe and send only protocol frames over the dedicated IPC descriptor.
+
+The parent continues serving the final observation after worker exit.
+Starting another game launches a fresh worker, avoiding assumptions about
+whether NetHack globals can be reinitialized safely.
+
+### Responsibilities
+
+| Component | Responsibility |
+| --- | --- |
+| `McpServer` | Protocol handshake, tools, validation, response serialization |
+| `GameSession` | Session identity, operation state, input eligibility |
+| `EngineProcess` | Spawn, framed IPC, exit status, termination and reaping |
+| `WindowAdapter` | Translate actual NetHack callbacks into owned values |
+| `ObservationStore` | Publish consistent snapshots and bounded history |
+| `SpectatorServer` | Serve static assets and GET-only observations |
+
+Use one coordinator to serialize state mutations. The MCP reader must
+remain responsive while an engine action is pending. HTTP handlers read
+published snapshots; they never invoke NetHack. No snapshot lock may be
+held during engine I/O or while waiting for user input.
+
+## 4. Window adapter and C boundary
+
+Prefer the shipped shim as the initial integration point. Register one
+callback, dispatch by its name, and immediately copy pointer arguments
+before returning. Read variadic arguments using their actual C types and
+default promotions, not merely the shim's format string. Some format
+strings are insufficient to reconstruct the native type accurately.
+
+Keep NetHack headers confined to an adapter translation unit. Include C
+declarations with C linkage, and compile against the same configuration
+as the static library. If the headers cannot compile as C++, use a small
+C ABI adapter for those declarations; this is an integration fallback,
+not a second server implementation. Never reproduce NetHack structs by
+hand to avoid including headers.
+
+No C++ exception may cross a C callback frame. Catch at the callback
+boundary, emit a fatal diagnostic when possible, and terminate the worker
+with an error status.
+
+### Callback coverage
+
+| Callback family | Required behavior |
+| --- | --- |
+| Initialize / create / destroy | Allocate window IDs, track types and lifetimes, set required initialization state |
+| Map glyph / clear / cursor | Update cells and cursor using supplied glyph data |
+| String / mixed string | Retain messages or window text; decode glyph escapes through NetHack helpers |
+| Menu start / add / end | Build menu entries, copying opaque identifiers inside the worker |
+| Menu select | Publish a menu request and wait for structured selections |
+| Character / position input | Publish a key request; return exactly one validated key |
+| Yes/no | Publish question, allowed choices, and default; implement documented escape/default behavior |
+| Line input | Copy bounded text into the caller's buffer; cancellation returns NetHack's escape sentinel |
+| Extended command | Present available command names and resolve a response to its actual table index |
+| Player selection | Use the generic NetHack selection path through the same menus and prompts |
+| File display | Read through NetHack's data-library facilities, then expose text and acknowledgement |
+| Status | Track field activation, values, condition bits, flush, and reset |
+| History | Maintain bounded message history and honor restore/history callbacks |
+| Inventory update | Mark cached inventory stale unless a verified refresh path updates it |
+| Exit | Publish final available UI state before normal worker shutdown |
+
+Callbacks intentionally implemented as no-ops, such as an optional bell,
+must be listed explicitly. Unknown input callbacks are fatal integration
+errors rather than silently returning zero.
+
+The shipped shim uses some generic status handlers instead of forwarding
+every field-enable callback. During implementation, verify this wiring
+and use a narrow adapter override where necessary. Do not advertise a
+status or permanent-inventory capability that the adapter cannot honor.
+
+Menu identifiers can contain pointers. They remain private to the worker.
+An external entry ID is an integer assigned for that menu instance; the
+worker maps it back to the copied `anything` value. Allocate returned
+`menu_item` arrays using allocation compatible with NetHack's `free()`.
+
+## 5. Input synchronization
+
+Every blocking input callback creates an input boundary. The worker first
+flushes pending display updates, then publishes a snapshot containing a
+new `input_id`, and blocks on the IPC channel. IDs increase throughout a
+game and never repeat within that game.
+
+Every gameplay response carries `game_id` and `input_id`. The parent
+rejects responses for an old game or an old prompt before forwarding
+anything. This protects against delayed responses and duplicate calls.
+
+The first version accepts one key per `press` call. It does not keep a
+general key queue. For example, opening a door is:
+
+1. Agent presses `o` at input boundary 40.
+2. NetHack requests a direction at boundary 41.
+3. Agent presses `h` at boundary 41.
+4. NetHack resolves the action and reaches boundary 42.
+
+The next boundary may be another prompt, not normal gameplay. Return it
+as observed. This also supports command prefixes and count entry without
+guessing when a sequence is safe to consume.
+
+`nh_poskey` and `nhgetch` identify the callback but cannot reliably prove
+that a request is a normal movement command. Use pending kind `key` with
+a `source` field. Do not invent a reliable `normal_turn` classification.
+
+Text, yes/no, menus, extended commands, and acknowledgements have distinct
+pending kinds. `press` is rejected for those kinds; `respond` or
+`select_menu` is used instead. Informational menus and blocking text
+windows require explicit acknowledgement. Merely viewing them in the
+browser must never acknowledge them.
+
+### Operation completion and waiting
+
+An accepted input is applied once. The operation completes when the next
+input boundary arrives or the worker exits. A 10-second response deadline
+limits how long an MCP call waits; it is not a game-turn limit.
+
+If the deadline expires, return `operation_state: running` with an
+`operation_id` and the latest snapshot. The agent must use `observe` to
+check completion. Reject further gameplay actions as `BUSY` until the
+operation completes. Never resend input because a wait timed out.
+
+Cancellation stops waiting for an MCP result; it cannot undo consumed
+game input. Preserve the operation and its eventual result in session
+state. A canceled request must not trigger a replacement key.
+
+## 6. Observations and fairness
+
+Observations describe what a conventional UI receives. Do not expose
+unseen monsters, true identities of unidentified objects, hidden map
+terrain, future RNG values, or raw engine memory.
+
+The canonical snapshot has the following shape. Values here are
+illustrative; `map.rows` in a real snapshot contains 21 complete rows.
+
+```json
+{
+ "schema_version": 1,
+ "game_id": "g_opaque_unique_id",
+ "revision": 17,
+ "lifecycle": "waiting",
+ "operation": null,
+ "map": {
+ "width": 79,
+ "height": 21,
+ "origin": {"x": 1, "y": 0},
+ "rows": [],
+ "cursor": {"x": 38, "y": 10}
+ },
+ "status": {
+ "hp": {"text": "12", "value": 12},
+ "max_hp": {"text": "12", "value": 12}
+ },
+ "messages": [{"id": 8, "text": "Hello Agent!"}],
+ "messages_truncated": false,
+ "inventory": {"known": false, "stale": true, "entries": []},
+ "pending": {"input_id": 5, "kind": "key", "source": "nh_poskey"},
+ "viewer_url": "http://127.0.0.1:8765/"
+}
+```
+
+NetHack's display columns are 1 through 79; column 0 is internal.
+Rows are 0 through 20. Preserve those coordinates in tool responses and
+show coordinate rulers in the UI. Cursor position is not necessarily
+player position: inspection and targeting may move it.
+
+Maintain glyph IDs and colors internally from rendered callbacks. Use
+ASCII rows for compact tool output; offer visible cell details through
+`observe(detail: full)`. Do not derive object identities by consulting
+hidden structures behind a glyph. Clear the cached map on the port's map
+clear callback so old levels cannot bleed into new displays.
+
+Preserve formatted status text even when a numeric interpretation exists.
+Normalize only fields with verified types; hunger and conditions require
+their own representation. A missing or disabled field is absent, not
+zero. Turn count, if exposed, comes from the displayed time field; a
+snapshot revision is never treated as a game turn.
+
+Store up to 500 recent messages with monotonically increasing message
+IDs. `observe(after_message_id)` can return only newer messages and must
+report truncation if the cursor predates the retained buffer. Ordinary
+tool results include a bounded recent tail, so repeated observations do
+not grow indefinitely.
+
+Inventory is not guaranteed to arrive as a complete independent stream.
+Initially, expose inventory windows when the agent requests them through
+the normal `i` command. Cache entries only when the adapter can identify
+a full inventory response reliably; otherwise leave `known: false` and
+show the menu itself. Mark any cached inventory stale after gameplay
+input. `observe` never secretly executes an inventory command.
+
+Preserve final menus and text in the published snapshot until a subsequent
+input boundary supersedes them. Destruction of a NetHack window should
+not erase information before an agent or spectator can observe it.
+
+## 7. MCP contract
+
+Use the published MCP `2025-11-25` protocol as the initial supported
+version. During initialization, negotiate that supported version rather
+than blindly echoing the client's requested version. Advertise only the
+tools capability. Implement `initialize`, `notifications/initialized`,
+`ping`, `tools/list`, `tools/call`, and cancellation handling. Follow the
+[MCP lifecycle specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle).
+
+Use newline-delimited UTF-8 JSON-RPC on stdio. Only protocol messages go
+to stdout; diagnostics go to stderr. Serialize writes to prevent threads
+from interleaving responses. Enforce a 1 MiB incoming message limit.
+See the [stdio transport specification](https://modelcontextprotocol.io/specification/2025-11-25/basic/transports).
+
+Every tool has a JSON Schema with `additionalProperties: false`, explicit
+required fields, and bounded values. Return an object as
+`structuredContent` and a text content block containing its JSON encoding.
+Tool execution failures use `isError: true`; malformed protocol envelopes
+and unknown methods use JSON-RPC errors. See the
+[MCP tools specification](https://modelcontextprotocol.io/specification/2025-11-25/server/tools).
+
+### Tool definitions
+
+| Tool | Arguments | Behavior |
+| --- | --- | --- |
+| `new_game` | Optional `name`, `role`, `race`, `gender`, `alignment` | Start a fresh worker and return its first boundary |
+| `observe` | Optional `game_id`, `detail`, `after_message_id`, `wait_ms` | Read current state; optionally wait for a revision or operation completion |
+| `press` | Required `game_id`, `input_id`, `key` | Answer one pending key request |
+| `select_menu` | Required `game_id`, `input_id`, `selections`; optional `cancel` | Return selected menu entries or cancellation |
+| `respond` | Required `game_id`, `input_id`; response fields depend on pending kind | Answer text, choice, command, or acknowledgement |
+| `quit_game` | Required `game_id` | Stop the current worker and retain its last state |
+
+`new_game` defaults to name `Agent` and random unspecified character
+attributes. Validate values against NetHack's supported choices. Reject
+new-game requests while a game is active; require explicit termination
+first. Names are display names, never directory paths. Character
+selection uses NetHack's own compatibility checks and can surface menus
+if needed. Do not silently claim that an incompatible requested character
+was created exactly as specified.
+
+`observe.detail` is `compact` by default or `full`. `wait_ms` ranges from
+0 to 10000; default 0. Observing before a game starts returns `idle` and
+the viewer URL. An explicitly wrong game ID returns `STALE_GAME`.
+
+`press.key` accepts a single printable ASCII character, or a named key
+`ENTER`, `ESC`, `SPACE`, `TAB`, `BACKSPACE`, `CTRL_A` through `CTRL_Z`, or
+`META_x` for a single printable ASCII suffix. Specify and test the byte
+mapping centrally. Do not use terminal escape sequences to encode arrows;
+the agent can use NetHack movement keys. Zero bytes are forbidden.
+
+`select_menu.selections` is an array of `{entry_id, count}` objects.
+`count` is omitted for all items in that entry or is a positive integer
+within NetHack's supported range. Reject duplicate IDs, nonselectable
+entries, and multiple selections for `PICK_ONE`. `PICK_NONE` requires an
+empty selection list and acknowledges the menu. An empty list for a
+selectable menu means select nothing; `cancel: true` means cancellation.
+Expose preselected flags in observations; submitted selections are the
+complete desired result, so the agent need not emulate toggle keys.
+
+`respond` accepts exactly one response variant:
+
+- `text`: line content without newline, bounded to the pending capacity.
+- `choice`: a permitted single character for a choice prompt.
+- `command`: an exact offered extended-command name.
+- `acknowledge: true`: continue past blocking informational text.
+- `cancel: true`: use the pending kind's documented cancellation behavior.
+
+The pending object reports accepted variants, choices/default, text byte
+capacity, or menu mode as appropriate. Invalid responses leave the worker
+blocked at the same input ID. Text limits are byte limits, not Unicode
+character counts. Reject unsupported encoding explicitly.
+
+`quit_game` is administrative termination, not a claim that NetHack's
+in-game quit dialogue completed. It terminates the worker, reaps it, and
+labels the session `aborted`. An agent wanting normal in-game quitting
+can use the extended `quit` command and answer its prompts. This
+distinction permits recovery from a stuck game without manufacturing a
+normal game result. Repeated administrative termination of the same
+terminal session is harmless.
+
+Useful application error codes are `NO_GAME`, `GAME_ACTIVE`, `STALE_GAME`,
+`STALE_INPUT`, `WRONG_INPUT_KIND`, `INVALID_SELECTION`, `INVALID_RESPONSE`,
+`BUSY`, `ENGINE_FAILURE`, and `LIMIT_EXCEEDED`. Errors include current
+game/input IDs when available so the agent can recover by observing.
+
+## 8. Worker protocol and supervision
+
+IPC uses a four-byte unsigned big-endian payload length followed by that
+many UTF-8 JSON bytes. Set a 4 MiB frame limit in both directions. Read and
+write loops handle partial transfers and interrupted system calls.
+Reject malformed, oversized, out-of-order, or unsupported frames.
+
+The worker first sends `hello` with an IPC version and NetHack build
+identity. Parent and worker must agree on IPC version 1. Each frame also
+carries the game ID; worker events have an increasing sequence number.
+
+Parent messages are `start` and `input`. `start` contains only validated
+character settings and the private run path. `input` carries the pending
+input ID and the already validated typed response. The worker validates
+again because it owns the authoritative pending callback.
+
+Worker events are `snapshot`, `diagnostic`, and `exiting`. A snapshot
+contains the complete visible state at an input boundary. Optional
+intermediate snapshots can be coalesced to at most 10 per second during
+long actions; boundaries and final snapshots must not be dropped.
+
+Parent supervision treats process exit as authoritative. Drain remaining
+IPC frames before publishing the terminal state. A clean exit after
+normal shutdown is `ended`, an intentional parent stop is `aborted`, and
+an unexpected nonzero exit or signal is `failed`. Do not infer victory
+from exit code zero. Preserve rendered end-of-game text; a structured
+victory/death classification can be added only after a reliable source
+has been verified.
+
+Shutdown sequence on MCP stdin EOF or parent termination:
+
+1. Stop accepting gameplay calls and close the IPC input channel.
+2. If the worker remains alive, send SIGTERM and wait up to two seconds.
+3. Send SIGKILL if necessary and reap it.
+4. Stop HTTP and join parent threads.
+
+Worker IPC EOF must end the worker rather than returning endless escape
+keys to NetHack. Enforce a bounded diagnostic buffer, and keep draining it
+so excessive output cannot deadlock the engine.
+
+## 9. Browser spectator
+
+Serve a small bundled HTML/CSS/JavaScript page with libmw's HTTP facilities.
+No frontend framework, Bootstrap, external font, CDN, or tileset is needed.
+The C++ executable remains the only server runtime; browser JavaScript is
+limited to fetching and rendering observations.
+
+Layout:
+
+- Game name, lifecycle, and last-update indicator.
+- Monospace map with coordinate rulers; optional cell colors.
+- Status fields, including HP, hunger, conditions, and displayed turn.
+- Current pending question, text window, or menu.
+- Recent messages and clearly labeled cached inventory, if known.
+
+The page has no gameplay event handlers. Display preferences such as font
+size may remain local to the browser. Loading, reloading, disconnecting,
+or opening additional tabs has no effect on the game.
+
+HTTP routes:
+
+| Route | Result |
+| --- | --- |
+| `GET /` | Spectator page |
+| `GET /viewer.js`, `GET /viewer.css` | Bundled static assets |
+| `GET /api/state` | Latest complete published snapshot |
+| `GET /health` | Server readiness and worker lifecycle |
+
+Poll state every 250 ms while visible, with one outstanding request per
+tab. Back off when hidden or disconnected. Use revision-based ETags and
+`If-None-Match`; unchanged state returns 304. Snapshot responses require
+revalidation. Never implement gameplay POST routes or an HTTP MCP endpoint
+in this version.
+
+Bind to `127.0.0.1`, with a configurable port; default 8765. If occupied,
+fail promptly with a useful error or allow an explicitly requested port
+0 to choose a free port. Report the bound URL on stderr and in tool
+results. Do not automatically launch a browser.
+
+Render game text using DOM text nodes or `textContent`, never `innerHTML`.
+Do not enable cross-origin access; validate the Host header against the
+bound loopback address. These measures keep game strings as text and the
+local endpoint scoped to the intended viewer.
+
+The inspected libmw `HTTPServer::start()` spins until its listener is
+running, which may hang on bind failure. During implementation, use an
+appropriate supported binding path or make a separately reviewed libmw
+fix. A preflight port check alone does not eliminate the bind race.
+
+## 10. Build, files, and persistence
+
+Proposed application layout:
+
+```text
+nethack/
+ CMakeLists.txt
+ README.md
+ designs/design-0-mcp.md
+ src/
+ main.cpp
+ mcp_server.cpp
+ game_session.cpp
+ engine_process.cpp
+ window_adapter.cpp
+ observation_store.cpp
+ spectator_server.cpp
+ include/
+ ... corresponding public headers ...
+ web/
+ index.html
+ viewer.js
+ viewer.css
+ tests/
+ build/ generated; includes fetched dependency sources
+```
+
+Use CMake for application code and FetchContent for external project
+sources, including NetHack and libmw. Fetch from their upstream
+repositories using full commit IDs, or versioned archives with verified
+hashes. Do not use moving branches as dependency pins or require sibling
+development checkouts. The inspected NetHack commit above is the initial
+candidate pin.
+
+FetchContent obtains NetHack's sources; it does not replace NetHack's
+build system. Connect the populated source directory to CMake custom
+build targets that run upstream setup and make, then expose the resulting
+static library as an imported target. Build an isolated copy under the
+application build directory because upstream setup writes generated
+makefiles and headers into its source tree. Build all compilation steps
+with `-j24`. Track generated headers and runtime data as build outputs,
+and record the upstream commit, compiler, configuration, and Lua version.
+
+Fetch libmw through FetchContent as well, with its build outputs inside
+this application's build tree. Apply the same policy to transitive
+project dependencies such as cpp-httplib, spdlog, nlohmann/json, and any
+test libraries. Declare their pinned FetchContent details before loading
+libmw so its moving-branch declarations do not determine the versions.
+Do not depend on artifacts from another project's `build/` directory.
+
+Common platform libraries, including curl, OpenSSL, and threads, may be
+discovered through CMake package discovery or pkg-config. Prefer system
+Lua 5.4 for this build, requiring both headers and a linkable library.
+Pass the discovered include and library paths into the NetHack build
+adapter explicitly; do not rely on upstream distribution detection or
+hard-code Gentoo-specific paths. Fail configuration with a clear missing
+dependency message if compatible system Lua is unavailable. A future
+bundled Lua option must obtain its sources through FetchContent too.
+
+First validate a minimal worker linked to the actual generated `libnh.a`
+and system Lua. Resolve the system-Lua/archive mismatch in a reproducible
+build adapter or a small patch stored with the application and applied
+only to the isolated build copy. A passing header compilation
+alone is not evidence that the library can start and run a game.
+
+Use a private application data root, configurable on the command line.
+Each game gets a unique directory and a `save/` subdirectory with owner-only
+permissions. Populate the runtime data required by the pinned NetHack
+build, including its data archive, system configuration, and writable
+record/lock files. Resolve all asset paths before NetHack changes its
+working directory. Do not inherit the user's `.nethackrc` or arbitrary
+`NETHACKOPTIONS`; supply controlled defaults with ASCII symbols, visible
+turn count, and predictable key bindings.
+
+Retain run directories after termination for diagnostics and possible
+recovery. There is no automatic recursive cleanup. Normal save commands
+may produce NetHack save files, but resuming them through MCP is deferred
+until compatibility and lock recovery are explicitly designed. Do not
+promise that stopping the MCP server saves the game.
+
+## 11. Verification and acceptance criteria
+
+Tests must exercise real input transitions, not just JSON formatting.
+
+### Adapter and session tests
+
+- Map origin, width, row count, clear behavior, and cursor distinction.
+- Glyph/color extraction against the pinned headers.
+- Menu IDs containing both integer and pointer representations; selectable
+ headers, preselection, counts, empty choice, and cancellation.
+- Line limits, invalid choice, default/escape mapping, and extended names.
+- Status flush/reset and disabled fields; inventory freshness labels.
+- Stale game/input IDs and duplicate requests cannot apply a second input.
+- Timeout/cancellation after accepted input cannot cause a replay.
+- Malformed frames, partial reads, worker EOF, crash, and oversized output.
+
+### End-to-end tests
+
+1. Complete an MCP initialization and enumerate tools using an independent
+ MCP client implementation.
+2. Start a normal game and receive a nonempty ASCII map and pending input.
+3. Make a move or wait action and verify a subsequent boundary arrives.
+4. Open inventory, select or acknowledge as appropriate, and return to play.
+5. Exercise a direction request and a text prompt; verify neither consumes
+ unsolicited queued input.
+6. End a game normally and verify MCP and HTTP remain alive.
+7. Start a second game and verify it has a new worker, directory, and IDs.
+8. Kill a test worker and verify a failure snapshot remains inspectable.
+9. Close MCP stdin and verify no worker is orphaned.
+10. Fetch the viewer from two tabs; verify GET requests do not advance
+ input IDs, turn count, or game state.
+11. Occupy the HTTP port and verify startup fails promptly.
+12. Render game strings containing HTML markup and verify they remain text.
+
+Use temporary private run directories for tests. Deterministic callback
+fixtures test precise branches; real-game tests must tolerate random map
+generation and character outcomes. Test-only wizard scenarios, if used,
+must remain isolated from normal game defaults.
+
+Completion means an external MCP client can start, play, handle prompts
+and menus, and end a real game while a browser observes the same state.
+It also means a normal game exit cannot terminate the MCP server, and no
+browser route can change game state.
+
+## 12. Implementation sequence and remaining decisions
+
+After design approval, implement in this order:
+
+1. Reproducible NetHack library/data build and minimal worker startup.
+2. Callback adapter, observations, and typed IPC input boundaries.
+3. Parent supervision, timeout behavior, and terminal-state retention.
+4. MCP lifecycle and tools with independent-client verification.
+5. Read-only browser and end-to-end validation.
+6. Build/run documentation and MCP client configuration example.
+
+The browser choice, stdio transport, one-key input granularity, and deferred
+save/resume support are proposed scope choices rather than explicit user
+requirements. They can be revised before implementation. Read-only UI and
+C++ application code are established requirements.
+
+Build feasibility, adapter header compatibility, libmw bind-failure
+handling, and exact status/inventory callback behavior remain technical
+validation items. They should be resolved in the first integration steps
+without weakening the input-boundary or read-only guarantees.