# MCP Interface Contract **The interface contract.** MCP-first (D-op1): every interface in this engine is designed on this surface first; the engine API (`api.md`) is derived from it. This file is the spec — implementations must match it. ## Principles 1. **One gateway, many namespaces** (D9): a central MCP gateway exposes every subsystem under its own namespace (`item.*`, `crop.*`, `save.*`). "Every feature has MCP" is enforced by the registration layer: a subsystem cannot boot without registering a namespace. 2. **Creation-first, debug-second** (D3): tools that build game content (write `content/*.yaml`) are the primary surface. Runtime tools are for **debugging only** — they introspect state and trigger events, they never serve players. 3. **JSON only** (D18): every tool argument and resource is normalized JSON. YAML is the authoring surface, never the MCP surface. 4. **No Mod↔MCP bridge** (D10): `mod.*` is introspection-only. MCP never invokes Mod-registered behavior; Mods never register MCP tools. 5. **Namespace registration contract** — each subsystem implements: ```ts interface McpNamespace { name: string; // e.g. "item" tools: McpTool[]; // { name, description, inputSchema, outputSchema?, handler } resources?: McpResource[]; // { uri, description, read(params) } prompts?: McpPrompt[]; // { name, description, arguments?, build() } enabled: boolean; // per-subsystem switch (D9) } ``` > **Implementation note (walking skeleton, 2026-08-12):** resources use template URIs in the `uri` field (`map://{mapId}`) resolved by `read(params)` — not the `uriTemplate` field. Parameterized resources surface as templates in `listResources` with their braces intact. ## Transport - **stdio** — dev toolchains (`apps/mcp-server`): agent connects, creates content. - **streamable HTTP** — runtime debug: connects into a *running* headless core process; live introspection, event triggering, snapshot/restore. - Both speak the same namespace surface; nothing is transport-specific. ### `apps/mcp-server` entry options The stdio entry serves any content/mods pair (not just the monorepo's own). Roots resolve CLI > env > default: | Source | Flag / var | Default | |---|---|---| | CLI | `--content-dir ` / `--mods-dir ` / `--ws-port ` | — | | Env | `NENGINE_CONTENT_DIR` / `NENGINE_MODS_DIR` / `NENGINE_WS_PORT` | — | | Default | — | `content/`, `mods/` at the monorepo root; WS channel off | Relative CLI/env paths resolve against `process.cwd()`. Zero-arg invocation is unchanged. Example: ```sh node apps/mcp-server/dist/index.js \ --content-dir examples/sample-game/content \ --mods-dir examples/sample-game/mods ``` The wiring is `apps/mcp-server/src/index.ts`: exported `resolveDirs(argv, env, cwd)` → `createEngine` → `createNengineMcpServer`; `main` auto-runs on import only under direct execution (`import.meta.url === process.argv[1]`), so tests import it without booting stdio. ### WebSocket world-sync channel (render viewports) **Not an MCP namespace** — a separate broadcast channel for render viewports (D8/D24: headless core authoritative, browsers are WebSocket viewports). It adds no tools/resources/prompts to the MCP surface. Started in the same process as the stdio entry via `--ws-port ` (or the `NENGINE_WS_PORT` env var), resolved in the same CLI > env > default chain as content/mods roots (there is no default — absent means the channel is off, zero-flag behavior unchanged): ```sh node apps/mcp-server/dist/index.js \ --content-dir examples/sample-game/content \ --mods-dir examples/sample-game/mods \ --ws-port 8787 ``` **Frames** (JSON text, one per WebSocket message): | Direction | Frame | Meaning | |---|---|---| | server → client | `{"type":"snapshot","map":MapDef,"world":Entity[],"time":{day,season,tickInDay},"sprites":SpriteDef[],"sounds":SoundDef[],"shops":ShopDef[],"assetsIndex":Record}` | Sent on connect and in reply to a sync request: the full current state — the map def (clients need it to build a viewport, since world entities alone lack it), every world entity, the current `TimeSystem` state (so viewport HUDs can show day/season without waiting for an event frame), the registered `sprite`/`sound` def catalogs (small bounded sets from `registry.list` at snapshot time — the render layer needs the draw/audio instructions to present the world; D26), the `shop` def catalog (D35 — the dev UI's shop panel reads buy/sell rows from it; additive), and the **metadata-only `assetsIndex` (D37)** — asset id → `{kind, mime, size}` (byte size). **Bytes are NOT shipped per snapshot**: clients request them on demand via `assets-request` (below) | | server → client | `{"type":"event","event":string,"payload":unknown}` | Broadcast to ALL connected clients for every `WorldEventBus` emission (`day-start`, `time-tick`, `crop-grow`, `npc-move`, `player-intent`, `input`, `sound-play`, ...) | | server → client | `{"type":"assets","assets":Record}` | D37: the on-demand reply to an `assets-request` — the requested ids' full base64 entries (unknown ids ignored; the request is capped at 64 ids server-side, so larger requests are truncated and the remainder must be re-requested). Sent to the requesting client only (not broadcast) | | client → server | `{"type":"sync"}` | Optional: request a fresh snapshot (viewports re-sync on each event frame) | | client → server | `{"type":"assets-request","ids":string[]}` | D37: request the base64 bytes for the given asset ids (bounded — the coordinator batches at most 64 per request). The server replies with a `{type:"assets"}` frame; unknown ids are ignored gracefully. This replaced the D31 `assets` field on the snapshot — **versioned protocol change (D37): old clients reading `snapshot.assets` break; the new client (`connectWs`) reads `assetsIndex`, requests what it needs, and feeds `{type:"assets"}` responses to the viewport/sound** | | client → server | `{"type":"intent","action":"move"\|"interact"\|"buy"\|"sell"\|"talk"\|"choose"\|"select-seed","args":{...}}` | Player input: parse → `IntentSystem.handle(action, args)` → the resulting `player-intent` event is broadcast as an event frame (IntentSystem emits it on the bus). Invalid input never crashes the server — a synthesized `player-intent` error frame is broadcast instead | | client → server | `{"type":"input","input":string}` | Raw input string (D26): `"key:ArrowUp"` / `"key:Space"` / `"pointer:click::"`. The server emits an **`input`** bus event `{input}` broadcast as an event frame (behavior-language-matchable via `on: "input"`). **Built-in mapping**: `key:ArrowUp`/`key:ArrowDown`/`key:ArrowLeft`/`key:ArrowRight` additionally call `IntentSystem.handle("move", {tile})` for the tile one step from the player's **current** position in that direction (player-centered). Unknown keys/prefixes are broadcast-only (no mapping). A non-string/empty `input` never crashes — a synthesized `input` error frame `{input, ok:false, error:"input must be a non-empty string"}` is broadcast instead | Event payloads are JSON-serialized defensively: a non-serializable payload (circular refs, functions) is skipped with a log line — the server never crashes on a bad payload. No ping/pong heartbeat in this slice. Browser clients use the native `WebSocket`; `apps/dev` connects via `connectWs` (`packages/render/src/sync/ws.ts`, same handle shape as `LocalSyncHandle`). ## Gateway-Level Namespace (`server.*`) | Kind | Name | Purpose | |---|---|---| | tool | `server.info` | Engine version, subsystems and their `enabled` state, loaded content counts | | tool | `server.enable_subsystem` / `server.disable_subsystem` | Toggle a namespace at runtime (D9) — `{name}` → `{name, enabled}` or an error result | | resource | `server://status` | Same as `server.info`, as a resource | | prompt | `server.orient` | Onboard an agent: engine capabilities, content layout, authoring workflow | **Runtime semantics (D9, implemented):** `server.enable_subsystem {name}` / `server.disable_subsystem {name}` flip a namespace's `enabled` switch on the gateway — the same per-gateway **in-memory** state `server.info`/`server.status` report (D9: subsystems can be enabled/disabled independently; state is not persisted and does not touch engine systems). A disabled namespace's tools/resources/prompts vanish from `listTools`/`listResources`/`listPrompts` and its tools reject calls with `namespace "" is disabled`. Both tools return `{name, enabled}` (`enabled` = the new state). Errors: non-string/empty `name` → `server.enable_subsystem: name must be a non-empty string` / `server.disable_subsystem: name must be a non-empty string`; unknown namespace → `server.enable_subsystem: unknown subsystem: ` / `server.disable_subsystem: unknown subsystem: ` (the gateway's throw on a missing namespace is converted). **Guard rail:** disabling the `server` namespace itself is rejected (`server.disable_subsystem: cannot disable the server subsystem`) — that namespace hosts the re-enable tool. --- ## `item.*` — Items & Tools Creation tools (primary): | Kind | Name | Description | |---|---|---| | tool | `item.validate` | Validate an item definition (JSON) against the item schema; returns normalized JSON + errors | | tool | `item.create` | Create an item definition — writes `content/items/.yaml` (via YAML serialization) and hot-reloads | | tool | `item.update` | Patch an existing item definition (merge + validate + write) | | tool | `item.list` | List registered item ids with one-line summaries | | tool | `item.get` | Fetch one item definition as normalized JSON | Debug tools (runtime only): | Kind | Name | Description | |---|---|---| | tool | `item.query_inventory` | Read current inventory state (entity-level) | | tool | `item.give` | Give an item to the player inventory (debug) | Resources: | URI | Description | |---|---| | `item://catalog` | Full item catalog (normalized JSON) | | `item://{id}` | Single item definition | Prompts: | Name | Purpose | |---|---| | `item.create-prompt` | Guided template: agent builds a well-formed item definition | --- ## `crop.*` — Planting & Growth | Kind | Name | Description | |---|---|---| | tool | `crop.validate` / `crop.create` / `crop.update` / `crop.list` / `crop.get` | Definition lifecycle, as `item.*` (`content/crops/`) | | tool | `crop.till` | Debug: till a plot (`{x?, y?, plotId?}` → `{plotId, x, y, tilled}`); idempotent — creates the plot entity if absent | | tool | `crop.sow` | Debug: sow a seed on a tilled plot (`{x?, y?, plotId?, seedItemId}` → `{plotId, x, y, cropItemId, seedItemId}`); consumes 1 seed from the player inventory (emits `item-use`) | | tool | `crop.harvest` | Debug: harvest a mature plot (`{x?, y?, plotId?}` → `{plotId, x, y, cropItemId, drops: [{itemId, qty}]}`); rolls drops into the player inventory (emits `item-acquire` per drop + `crop-harvest`), clears the crop | | tool | `crop.stage` | Debug: force an entity into a growth stage (fires growth hooks) | | tool | `crop.water` | Debug: water a tilled plot — sets `watered: true` (`{x?, y?, plotId?}` → `{plotId, x, y, watered}`); wraps `PlantingSystem.water` | | resource | `crop://catalog`, `crop://{id}` | Crop definitions | | prompt | `crop.create-prompt` | Guided crop creation (stages, water needs, seasons) | The **full farming loop (till → sow → water → harvest) is drivable through MCP** via `crop.till`/`crop.sow`/`crop.water`/`crop.harvest` — each wraps the matching `PlantingSystem` mechanism (debug, D3). `crop.water` is a **set** (not a toggle): it calls `PlantingSystem.water(x,y)`, so `watered` is always `true` after a successful call — watering an already-watered plot keeps it watered, and calling it repeatedly is safe (a `waterRequired` stage never flips back to unwatered mid-loop). Growth *advance* (stage progression over days) itself is simulation (TimeSystem + GrowthSystem), not an MCP tool — MCP only *forces* stages for debugging (D3). `crop.sow` resolves `cropItemId` from the seed exactly as `PlantingSystem.sow` does (the crop def whose `seedItem` matches, not the seed id itself); `crop.harvest` requires the final stage (`planting.harvest` semantics). All three accept `{x, y}` or `{plotId}` (identical coordinate/plotId validation to `crop.stage`/`crop.water`); `crop.sow` additionally requires a non-empty `seedItemId`. --- ## `map.*` — Maps & Terrain | Kind | Name | Description | |---|---|---| | tool | `map.list` | List registered map ids with one-line summaries (`{maps: [{id, width, height, tilesetRef}]}`) | | tool | `map.get` | Fetch one map definition as normalized JSON, including the player spawn entity (defaults to the first registered map when `mapId` omitted) | | tool | `map.validate` | Validate a map definition (JSON) against the map schema; returns the normalized def | | tool | `map.create` | Create a map definition — writes `content/maps/.yaml` and registers it live (invalid defs are rejected without writing) | | tool | `map.update` | Patch an existing map definition (merge + validate + rewrite `content/maps/.yaml` + re-register) | | resource | `map://{mapId}` | Map definition (tile layers as normalized JSON) plus the player spawn entity | | prompt | `map.create-prompt` | Guided map creation (grid dimensions, layers, spawn, walkability) | The map lifecycle tools (`map.validate`/`map.create`/`map.update`, D40) mirror `item.*` exactly and share the engine's single **authoring layer** (`@nengine/mcp` `authoring.ts`) with every other authoring namespace — validate against `mapSchema` (no write on failure), write `content/maps/.yaml` (YAML `stringify`), register live (`create` registers/overwrites; `update` removes then re-registers). Map authoring touches only the registry and the content file — the world map ENTITY is not re-seeded (same as all other kinds; the playtest instance reads fresh files). Exact shapes: `map.validate`/`map.create`/`map.update` → normalized `MapDef` JSON or error result (`map.update: map not found: `); `map.create` of a duplicate id overwrites. The shared layer is the D40 foundation the GUI editor will call for every kind (AI via MCP and human via GUI produce byte-identical content). --- ## `time.*` — Day & Season | Kind | Name | Description | |---|---|---| | tool | `time.get` | Current day, season, time-of-day tick | | tool | `time.advance` | Debug: advance the clock by ticks/days/seasons; fires `onDayStart`/`onSeasonChange` hooks as it crosses boundaries — **clock movement only**, no systems step, no `time-tick` | | tool | `time.tick` | Debug: run a full simulation step — advances the clock AND drives every system with a `step` (GrowthSystem grows crops/animals) and emits `time-tick` for intra-day scheduling (D23). `{ticks?}` defaults to `1` | | tool | `time.set` | Debug: jump to a specific day/season | | resource | `time://state` | Current time state as resource | | prompt | `time.configure-prompt` | Guide: authoring `content/time.yaml` (day length, season order) | Time *configuration* (day length, seasons) is content data (`content/time.yaml`); MCP reads it via `time://config` if needed. **The tick loop (D23)**: the headless core has no render loop, so simulation is driven explicitly by `engine.tick(dt)` (dt in ticks; 1 tick = 1 engine time unit, `dayLengthTicks` from the `time` def). `time.tick` is the MCP surface for that driver. Each `tick` advances `TimeSystem` (firing `day-start`/`season-change` boundary events as it crosses them), then runs every system's `step(dt)` in registration order (TimeSystem first — it emits the **`time-tick`** bus event `{day, season, tickInDay}` after advancing — then GrowthSystem, which accumulates day-fractions against `dayLengthTicks` and advances crops/animals on day boundaries), then the loop ends. `time.advance` deliberately does NOT do the systems step or emit `time-tick` — it is the raw clock primitive the tests and boundary-event tooling use. `engine.tick` vs `time.advance` are therefore semantically different: the first runs simulation, the second only moves time. An **optional auto-tick** (`createEngine({ tickIntervalMs, tickDeltaTicks? })`) drives `tick` from a `node:timers` interval (default `tickDeltaTicks` = 1 tick per interval; `unref`'d so it never holds the process open, cleared on `engine.stop()`); it is OFF by default so tests and headless MCP usage stay deterministic. --- ## `shop.*` — Economy | Kind | Name | Description | |---|---|---| | tool | `shop.validate` / `shop.create` / `shop.update` / `shop.list` / `shop.get` | Shop definitions (`content/shops/`): stock, prices, buy/sell rules | | tool | `shop.buy` | Debug: execute a purchase (fires `onTransaction`) | | tool | `shop.sell` | Debug: execute a sale | | resource | `shop://{shopId}` | Shop definition | | prompt | `shop.create-prompt` | Guided shop creation | --- ## `save.*` — Persistence (mechanism only, D14) | Kind | Name | Description | |---|---|---| | tool | `save.snapshot` | Serialize the full world state to JSON (the engine's save format) | | tool | `save.restore` | Restore from a snapshot (validate, migrate via schema-version hooks, inject) | | tool | `save.migrate` | Dry-run / apply a schema-version migration to a snapshot | | tool | `save.validate` | Validate a snapshot against the current world schema | | resource | `save://state` | Latest snapshot as resource | | prompt | `save.authoring-prompt` | Explain save format, versioning, migration authoring | Save *policy* — when to save, slots, autosave, UI — is creator content/Mods (D14), so it has **no** MCP tools. --- ## `behavior.*` — Declarative Behavior Language (v1 core: conditions + triggers, D15) | Kind | Name | Description | |---|---|---| | tool | `behavior.trigger.validate` / `behavior.trigger.create` / `behavior.trigger.update` / `behavior.trigger.list` / `behavior.trigger.get` | Trigger definitions: `when → fire ` (`content/behaviors/`) | | tool | `behavior.fire` | Debug: manually fire a trigger by id | | tool | `behavior.condition.validate` | Validate a condition expression alone | | resource | `behavior://triggers` | All trigger definitions (normalized JSON) | | prompt | `behavior.trigger-prompt` | Guided trigger authoring (condition grammar, action set) | Condition grammar (v1): comparisons (`entity.state == "sprout"`, `time.day >= 5`) combined with `and`/`or`/`not`. Action set (v1): `set-state`, `emit-event`, `give-item`, `play-sound`, `dialogue`. ### Behavior language v2: state machines + time triggers (D15, Tier 2 lead item) v2 = v1 triggers + **state machine syntax** + **time-based triggering primitives** (`onSchedule`). State machines are the mechanism underneath NPC schedules, quests, and events (boundaries.md "temporal state → behavior language v2"). They are a **separate content kind `statemachine`** (`content/statemachines/.yaml`) so the existing `behavior.trigger.*` tools stay intact; their MCP tools live under the same `behavior.*` namespace. | Kind | Name | Description | |---|---|---| | tool | `behavior.statemachine.validate` / `behavior.statemachine.create` / `behavior.statemachine.update` / `behavior.statemachine.list` / `behavior.statemachine.get` | State machine definition lifecycle (`content/statemachines/`), mirroring `behavior.trigger.*` | | tool | `behavior.statemachine.status` | Debug: current state of a running machine instance (entity-backed; reports `started: false` before the machine has ever transitioned) | | tool | `behavior.statemachine.transition` | Debug: force a machine instance into a state (fires onEnter/onExit, emits `state-change` regardless of declared transitions) | | resource | `behavior://statemachines` | All state machine definitions (normalized JSON) | | prompt | `behavior.statemachine-prompt` | Guided state machine authoring (states, transitions, schedule entries) | **`StateMachineDef` shape** (content kind `statemachine`): ```json { "id": "quest-id", "initial": "inactive", "states": [ { "id": "inactive" }, { "id": "active", "onEnter": [{ "action": "give-item", "itemId": "parsnip-seed", "qty": 1 }] }, { "id": "complete", "onExit": [{ "action": "emit-event", "event": "quest:done" }] } ], "transitions": [ { "from": "inactive", "to": "active", "on": "day-start", "when": { "op": "gte", "path": "time.day", "value": 3 } }, { "from": "active", "to": "complete", "when": { "op": "gte", "path": "player.inventory.parsnip", "value": 5 } } ], "onSchedule": [ { "at": "day-start", "to": "working" }, { "at": { "day": 1, "season": "summer" }, "to": "resting", "when": { "op": "eq", "path": "payload.season", "value": "summer" } } ] } ``` - `states[]` — at least one state; each state has an optional `onEnter`/`onExit` action list (same 5-kind action set as triggers). `onEnter` runs when entering the state; `onExit` when leaving. State ids are unique. - `transitions[]` — `from`/`to` must reference declared states. A transition fires when `on` matches the current bus event name AND `when` (a v1 `ConditionExpr` over the trigger ctx) evaluates true; at least one of `on`/`when` is required. A transition with no `on` is a pure condition transition evaluated on every bus event (trigger-like). First matching transition in declaration order wins; at most one transition per machine per event. - `onSchedule[]` — the **time primitive** (v1 of v2; no tick loop yet — documented limitation). Entries fire on `TimeSystem` boundary events: `at: "day-start"` / `at: "season-change"` match those events; `at: { day?, season? }` (at least one present) matches the `day-start`/`season-change` payload. `to` must reference a declared state; optional `when` is an extra condition evaluated against the trigger ctx (including `payload.day`/`payload.season`). Schedule entries apply from **any** state and are evaluated **after** `transitions` (so explicit event/condition transitions take priority). A transition to the machine's current state is a no-op (no onEnter/onExit, no `state-change`). **Runtime semantics (v2)**: the behavior engine (TriggerSystem, id `"trigger"`) drives both triggers and machines in the same `match` pass under the **same re-entrancy guard** — trigger actions and machine transitions run triggers-first, machines-second, and events emitted during the pass are never re-matched. Machine state persists on a **world entity** `{ id: "sm:", type: "statemachine", machineId, state }` — snapshot-able by `save.*`, inspectable by MCP. Instances are created lazily on first transition (never at boot, so existing worlds/snapshots are unaffected). Each machine transition emits a `state-change` bus event `{machineId, from, to}` mapped by the engine to the **`onStateChange`** hook (machinery for quest-tracking Mods; `onTriggerFire` stays for triggers). `behavior.statemachine.status` returns `{machineId, state, started}` where `state` is the declared `initial` and `started: false` before the instance exists. Quest progress convention (boundaries.md): `player.questProgress.` set via `set-state` actions. **Design decisions (v2, D20)** — (1) separate content kind `statemachine` + `behavior.statemachine.*` tools rather than a `behavior`-kind discriminated union: the existing trigger tool names stay truthful, content dirs stay 1:1 per kind, and the `behavior://triggers` resource keeps a clean meaning; (2) `onSchedule` over a tick loop: the engine has no auto-driven tick (GrowthSystem.step has no driver) — schedules hook the existing `day-start`/`season-change` bus events, so no new game loop; fine-grained intra-day timing awaits a tick loop; (3) TriggerSystem extended (id kept `"trigger"`) rather than a sibling system so triggers and machines share one match loop and one re-entrancy guard; (4) machine state on a world entity (not an in-memory map) so `save.snapshot`/`save.restore` persist it for free. --- ## `dialogue.*` — Dialogue & Story (content-document system, D19) Dialogue is an **engine system** (boundaries.md item 2): authored content documents with nodes, choices, and conditions — a content document, NOT a state machine (D19). AI-authored dialogue is a core creation scenario (creation-first, D3). | Kind | Name | Description | |---|---|---| | tool | `dialogue.validate` / `dialogue.create` / `dialogue.update` / `dialogue.list` / `dialogue.get` | Dialogue definition lifecycle (`content/dialogues/.yaml`), mirroring `item.*` exactly | | tool | `dialogue.start` | Debug: start a dialogue; returns the current node (text + available choices) | | tool | `dialogue.choose` | Debug: pick a choice by index (into the **available** choices); returns the next node | | resource | `dialogue://catalog` | All dialogue definitions (normalized JSON) | | resource | `dialogue://{id}` | Single dialogue definition | | prompt | `dialogue.create-prompt` | Guided dialogue authoring (nodes, choices, conditions, actions) | **`DialogueDef` shape** (content kind `dialogue`): ```json { "id": "welcome", "startNode": "hello", "nodes": [ { "id": "hello", "text": "Welcome to your farm!", "choices": [ { "text": "Thanks! What can I plant?", "to": "seed-tip", "when": { "op": "gte", "path": "time.day", "value": 1 }, "actions": [{ "action": "give-item", "itemId": "parsnip-seed", "qty": 1 }] } ] }, { "id": "seed-tip", "text": "Parsnip seeds grow well in spring.", "to": "farewell" }, { "id": "farewell", "text": "See you around." } ] } ``` - `nodes[]` — at least one node; each node has a unique `id`, a `text` string, and **exactly one** of `choices` or `to` (or neither — a terminal node). A `choices` node branches; a `to` node is linear; a terminal node ends the dialogue when entered. - `choices[]` — each choice has `text`, `to` (a declared node), optional `when` (a v1 `ConditionExpr` evaluated against the dialogue ctx), and optional `actions` (the same 5-kind action set as triggers — `set-state`/`emit-event`/`give-item`/`play-sound`/`dialogue`; runs when the choice is picked, before moving to `to`). - `actions` on a node run when the node is **entered**. - `startNode` must be a declared node id. **Runtime semantics**: one active dialogue at a time; state persists on a world entity `{ id: "dialogue:active", type: "dialogue", dialogueId, nodeId, payload? }` — snapshot-able by `save.*`. `dialogue.start` creates it (lazily) and returns the start node; `dialogue.choose` picks from the **available** choices (those whose `when` evaluates true against the dialogue ctx — `time.*`, `player.*`, `world..*`, `payload.*`, the same path-resolution convention as triggers), runs the picked choice's actions, moves to the target node, and runs that node's actions. Entering a node with no available outbound path (a terminal node, or a `choices` node whose conditions all fail) **ends** the dialogue: the node's text is still delivered in the result, `dialogue-end` is emitted, and the active entity is removed. `dialogue-start` `{dialogueId, nodeId}` and `dialogue-end` `{dialogueId}` bus events map to hooks `onDialogueStart`/`onDialogueEnd`. The `dialogue` action kind inside dialogue nodes is a reserved no-op in this slice (nested dialogue arrives later). **Design decisions (D21)**: (1) **state on a world entity** (not a player field / in-memory map) so `save.snapshot`/`save.restore` persist it for free and MCP can inspect it like any entity — mirrors the statemachine decision (D20.4); (2) **linear `to` nodes surface a synthesized `Continue` choice** so the headless driver has one uniform advance action (`choose(index)`) — a `to` node's text is still presented before advancing; (3) **available choices are filtered by `when` at read time** and `choose` indexes into that filtered list (a gated-out choice is hidden, not erroring); (4) **terminal / all-gated nodes auto-end** the dialogue — otherwise a headless dialogue with no outbound path would soft-lock (there is no `dialogue.end` tool); (5) `dialogue.start`/`dialogue.choose` are **debug** tools (D3) — dialogue is authored content; the runtime drive surface is for headless testing. --- ## `weather.*` — Weather (lightweight probability-roll system, D19) Weather is a **lightweight engine system** (boundaries.md item 3): a probability table in `content/weather.yaml` (a **single-file** content kind, like `time.yaml`) keyed by season; the engine rolls a weighted pick on day-start. "Roll from a probability table" is a mechanism; the behavior language has no randomness (D19). **No creation tools** — the weather table is one file, so creation tools (which write per-id files) would be meaningless; the table is authored directly in YAML. | Kind | Name | Description | |---|---|---| | tool | `weather.get` | Debug: current weather (id, optional display name, current season) | | tool | `weather.set` | Debug: force a weather id (mechanism; emits `weather-change` on change) | | resource | `weather://state` | Current weather state | **`WeatherDef` shape** (content kind `weather`): ```json { "id": "default", "defaults": { "sunny": { "name": "Sunny" }, "rain": { "name": "Rain" } }, "seasons": [ { "season": "spring", "weathers": [{ "id": "sunny", "weight": 50 }, { "id": "rain", "weight": 30 }] }, { "season": "summer", "weathers": [{ "id": "sunny", "weight": 70 }, { "id": "rain", "weight": 15 }] } ] } ``` - `seasons[]` — at least one season entry; each has a `season` name (matching `time.yaml` season names) and `weathers[]` (1+) of `{ id, weight }` where `weight` is a positive integer (relative probability). - `defaults` — optional per-weather display metadata (`{ name }`); keys must be declared weather ids (schema-enforced). **Runtime semantics**: weather state lives on a world entity `{ id: "weather", type: "weather", weatherId }` created **at boot** with the current season's first table entry (deterministic — no RNG at boot). The first `day-start` rolls a weighted pick from the current season's table (the RNG is injectable for deterministic tests; default `Math.random`). When the current season has no table entry, the first season's table is used (fallback). `weather-change` `{from, to}` is emitted **only when the weather actually changes**, mapped to hook `onWeatherChange`. `weather.get` → `{weatherId, name?, season}`; `weather.set` → `{weatherId, name?, season, from?}` (`from` present when it changed). Unknown weather id → error result. **Design decisions (D21)**: (1) **state on a world entity** (snapshot-able, save.*-covered) created at boot so `weather.get` is always defined; (2) **injectable RNG** (constructor arg, default `Math.random`) for deterministic roll tests; (3) **initial weather = first entry of the current season's table** (no boot-time randomness); (4) **no creation tools and no prompt** — the single-file content kind makes per-id creation meaningless; (5) season is read from `TimeSystem.get().season` at roll time (the time def owns season names). --- ## `mod.*` — Introspection Only (D10) | Kind | Name | Description | |---|---|---| | tool | `mod.list` | Loaded Mods: id, manifest, status, registered hooks | | tool | `mod.status` | One Mod's detail: hook points bound, content overrides applied | | tool | `mod.reload` | Restart the headless core with re-loaded Mods (D13: code Mods restart, they don't hot-reload) | | resource | `mod://{id}` | Mod manifest + hook binding detail | No tools exist to invoke Mod behavior through MCP. > **Implemented (2026-08-12):** `mod.*` namespace is live (3 tools + 1 resource, no prompts). See the Change Log for exact result shapes and the `mod.reload` semantics decision. --- ## `placement.*` — Grid Placement (geometry/spatial primitive, D19/D22) Grid placement is an **engine primitive** (boundaries.md item 4): grid footprint + overlap validation + buildability-read-from-map-data is geometric mechanism the behavior language can't express ("geometry/spatial → engine primitive"). Building **definitions** — footprint, buildability rule, movement blocking — are **data** (`content/buildings/.yaml`, content kind `building`). Buildings in the world are **entities** (`type: "building"`), so `save.*` snapshot/restore covers them for free. | Kind | Name | Description | |---|---|---| | tool | `placement.building.validate` | Validate a building definition (JSON) against the building schema; returns the normalized def | | tool | `placement.building.create` | Create a building definition: validates, writes `content/buildings/.yaml`, registers live | | tool | `placement.building.update` | Patch an existing building definition (merge + validate + rewrite + re-register) | | tool | `placement.building.list` | List registered building ids with one-line summaries | | tool | `placement.building.get` | Fetch one building definition as normalized JSON | | tool | `placement.place` | Debug: place a building entity at a grid origin (validates def, map bounds, buildability, overlap) → placed entity or error result | | tool | `placement.remove` | Debug: remove the building occupying a grid cell → removed entity or error result | | tool | `placement.list` | Debug: list all placed building entities | | tool | `placement.occupies` | Debug: the building occupying a grid cell, or `null` | | resource | `placement://buildings` | Building definition catalog (normalized JSON) | | resource | `placement://{id}` | Single building definition (missing id → MCP error `building not found: `) | | resource | `placement://placed` | Live placed building entities | | prompt | `placement.create-prompt` | Guided building definition authoring (footprint, buildable tiles, blocksMovement) | **`BuildingDef` shape** (content kind `building`): ```json { "id": "shed", "name": "Shed", "footprint": { "w": 2, "h": 2 }, "buildable": "map", "blocksMovement": true, "description": "Stores tools and harvests." } ``` - `footprint` — the building's grid size in cells; `w`/`h` are positive integers. Placement uses the footprint as an axis-aligned rectangle anchored at the placement origin `(x, y)`. - `buildable` — which tiles allow placement (tile ids come from the map's layers; the terrain layer is the map layer named `"ground"`, else the first layer — see api.md §4): - `"map"` — any tile id on the map is buildable (no tile-type restriction). - `{ "tiles": ["grass", ...] }` — every footprint cell must carry one of the listed tile ids (non-empty list). - `"any"` — no tile-type restriction at all (the tile check is skipped entirely; the escape hatch for non-terrain/debug structures). `"map"` and `"any"` behave identically in v1 — `"map"` signals the def was authored against the map's terrain and is the explicit form reserved for a future map schema with unwalkable tile flags. - `blocksMovement` — whether a placed instance blocks NPC grid movement (consumed by MovementSystem, Task B, via `placement.occupies`). A `false` building is walk-over-able by NPCs but still blocks placement overlap (buildings block buildings). - `name` / `description` — optional display metadata. - `shop` — optional string (D35): a registered `shop` def id the building is a front for. `placement.place` copies it onto the placed instance (`building.shop`), so UIs discover shops from world entities (the dev app's shop panel opens when a shop-carrying building is clicked). **Runtime semantics**: `placement.place {buildingId, x, y}` → the placed entity `{id: "building:::", type: "building", buildingId, x, y, w, h, blocksMovement}` or an error result (unknown building; footprint out of map bounds; a footprint cell on a non-buildable tile; overlap with an existing building). `placement.remove {x, y}` → the removed entity or an error result (no building occupies the cell). `placement.list` → `{buildings: [placed entities...]}`. `placement.occupies {x, y}` → the building whose footprint contains the cell, or `null`. Every place/remove emits a `placement-change` bus event `{type: "place"|"remove", buildingId, x, y, w, h}` → hook `onPlacementChange` (Mods that track "what got built where" — build-quests, achievements — get their machinery). v1 occupancy rule: **buildings block buildings**; the player and non-building entities are transient and do not occupy cells in this slice (NPC blocking is MovementSystem's concern, Task B). **Design decisions (D22)**: (1) `buildable` as a three-form union (`"map"` / `{tiles}` / `"any"`) over a boolean flag — the map's tile ids ARE the terrain vocabulary, so restricting by tile id list is the natural authoring form; `"map"`/`"any"` collapse behaviorally in v1 (documented); (2) buildings as world entities (`type: "building"`) so `save.*` persists them with zero system-side storage; (3) buildings-block-buildings only in v1 — the player is transient, NPC occupancy is MovementSystem's job (Task B); (4) `placement-change` event + `onPlacementChange` hook per the tri-criterion: placement outcomes are spatial (not behavior-language-expressible), build/removal Mods are plausible, and the debug tools already exist; (5) creation tools named `placement.building.*` — buildings are the subsystem's content kind, so lifecycle tools live under the namespace like `item.*`/`crop.*` do; (6) no `placement` content kind named `building` tool aliases — `placement.building.list` (defs) vs `placement.list` (placed instances) stay distinct. --- ## `npc.*` — NPC Grid Movement (engine primitive, D19/D22) Grid movement + blocking is an **engine primitive** (boundaries.md item 5): `moveTo` is tile-to-tile with a simple blocking check — **no pathfinding (A*/BFS) and no collision system** (out of scope for the engine). NPC **schedules** are behavior language v2 content (state machines) — this system does NOT schedule anything. NPCs are **world entities** `{id, type: "npc", x, y, ...}`; there is **no new schema** — the movement API is `MovementSystem` (system id `"movement"`, MCP namespace `npc.*`). | Kind | Name | Description | |---|---|---| | tool | `npc.list` | Debug: all npc entities (`{npcs:[{id,x,y}]}`) | | tool | `npc.get` | Debug: one npc entity (unknown → error result `npc not found: `) | | tool | `npc.spawn` | Debug: create an npc entity at a grid cell (rejects blank id, bad coords, duplicate id) | | tool | `npc.move` | Debug: `moveTo` — validates npc, map bounds, walkable tile, blocking; emits `npc-move` | | tool | `npc.teleport` | Debug: no-check position set (emits `npc-move`; skips bounds/walkability/blocking) | | tool | `npc.path` | Debug (D33): compute an A* grid path for an npc (4-neighbor, Manhattan; blocks = `blocksMovement` buildings + other grid entities). Pure query — does not move the npc | | resource | `npc://{npcId}` | Live npc entity (missing id → MCP error `npc not found: `) | **No prompt and no creation tools**: NPCs are runtime entities, not authored content — there is no `content/npcs/` kind and nothing to author (a creator gives an NPC its schedule via `statemachine` content). `npc.*` is debug-only (D3), so the subsystem checklist's lifecycle/prompt clauses do not apply (mirroring `save.*`, which is also a content-free mechanism). **Runtime semantics**: `npc.spawn {id, x, y}` → the created entity `{id, type: "npc", x, y}`. `npc.move {npcId, x, y}` → `{npcId, from: {x, y}, to: {x, y}}` after moving, or an error result: `movement: unknown npc: ` / `movement: target out of bounds: (x,y)` / `movement: tile (x,y) is not walkable` / `movement: blocked by building at (x,y)`. `npc.teleport {npcId, x, y}` → the same `{npcId, from, to}` shape but skips bounds/walkability/blocking (still requires a valid npc and non-negative coords). Both emit the `npc-move` bus event `{npcId, from: {x,y}, to: {x,y}}`. **`npc.path` (D33)**: `npc.path {npcId, to: {x, y}}` → `{npcId, from: {x, y}, to: {x, y}, path: [{x, y}, ...]}` — the A* grid path from the npc's current cell to `to` **including start and goal** (4-neighbor, Manhattan heuristic, array-based open set — maps are ~16×16, so no binary heap), or `{npcId, path: null, reason: "unreachable"|"out of bounds"|"blocked"}`. Walkability = the D33 map-walkability resolution (`isWalkableTile`: `map.walkable[tileId]` when declared, else the `UNWALKABLE_TILES` fallback); blocked = a `blocksMovement` building footprint (via `PlacementSystem.occupies`) or another grid entity (npc/player) on the cell. It is a **pure query** — the npc is never moved (stepwise movement along a path is documented future work). Errors: `npc.path: npcId must be a non-empty string` / `movement: unknown npc: ` / `npc.path: to must have integer x and y`. Backs `MovementSystem.findPath(npcId, to) → TilePoint[] | null` (the engine API). **Validation chain (`moveTo`)**: (1) target coords are non-negative integers; (2) the npc entity exists and is `type: "npc"`; (3) the target is within map bounds (map entity `width`/`height`); (4) the target tile is walkable — the **D33 walkability resolution** via the shared `isWalkableTile(map, x, y)` helper (tile id read from the map entity's terrain layer — the layer named `"ground"`, else the first layer; a tileId declared in `map.walkable` uses its boolean, an undeclared tileId falls back to the `UNWALKABLE_TILES` convention — default `{"water"}` — see api.md §4); (5) the target cell is not blocked — a building whose `blocksMovement` is true occupies the cell (via `PlacementSystem.occupies`, resolved from the system host; other NPCs do NOT block in v1). Then `npc.x`/`npc.y` are set and `npc-move` is emitted. `moveTo` semantics are byte-identical to pre-D33 for existing content (water still blocked; a map with no `walkable` declaration is unchanged). **Design decisions (D22)**: (1) `MovementSystem` is a system (id `"movement"`) rather than a free module so it can resolve `PlacementSystem` from the system host (auto-booted before it) and keep one consistent blocking source; (2) **walkability is a tile-id convention**: the map schema has no per-tile walkability flag, so the engine treats the tile id `"water"` as unwalkable (the walking-skeleton map's water tiles are the demonstration) — **superseded in D33**: the map schema now carries an optional `walkable?: Record` flag, and the single source of truth is `isWalkableTile` (declared value when present, else the `UNWALKABLE_TILES` constant); (3) **buildings block, NPCs do not** in v1 — building occupancy is stable geometry (perfect for a blocking check), NPC-vs-NPC overlap is a simulation concern that would need a resolution policy (who yields?) — deferred; **`npc.path` (D33) treats other grid entities as blocked cells** (a pure query — pathfinding sees other npcs/player as obstacles, unlike `moveTo`'s single-tile check, which stays v1); (4) **no `onNpcMove` hook** — the `npc-move` bus event is already matchable by the behavior language (`on: "npc-move"` in a state machine transition), so the tri-criterion's "Mod hook demand" arm fails (Mods get the machinery via content, not a hook); the event's primary consumer is the render/sync layer; (5) `npc.teleport` exists because headless testing/QA often needs to reposition an NPC past blockers without writing a valid path — a no-check primitive; it still emits `npc-move` so render sync stays consistent. **D33**: `npc.path` is an **additive** debug query wrapping `MovementSystem.findPath` (A*, 4-neighbor, Manhattan, array-based open set) — `moveTo`/`teleport` semantics are untouched (stepwise NPC movement along a path is documented future work; pathfinding is a query mechanism today). --- ## `player.*` — Player Intent (engine primitive, D25) Player input is an **engine primitive** (boundaries.md item 8): one `IntentSystem` (id `"intent"`) turns player actions (`move`, `interact`) into world changes. Every input channel — the dev app's local sync, the WebSocket viewport channel (`{"type":"intent"}`), and this MCP debug tool — calls the SAME `handle`, so a player action behaves identically whether driven by a human clicking the canvas or an agent calling `player.intent`. Every `handle` call emits a **`player-intent`** bus event `{action, tile, ok, error?, detail?}` (successes AND failures — browser HUDs and WS viewports rely on it for feedback), which the behavior language can also match (`on: "player-intent"`). Since D29 the event ALSO maps to the **`onPlayerIntent` hook** (`HookContext.payload` = the intent result) — Mods are hooks-only (D6/D13), so the hook is the Mod-visible surface for the transient intent payload (see `decisions.md` D29.2). Hooks are not MCP surface; no tool/resource/prompt change. | Kind | Name | Description | |---|---|---| | tool | `player.intent` | Drive the player entity: `{action: "move"\|"interact"\|"buy"\|"sell"\|"talk"\|"choose"\|"select-seed", ...args}`. Success → the `IntentResult`; failure → error result `player.intent: ` | | resource | `player://state` | The live player entity JSON (`id`, `type`, `x`, `y`, `inventory`, `gold`, `flags`, `selectedSeed?`, ...) | **Action semantics (`IntentSystem.handle(action, args)`):** - **`move {tile:{x,y}}`** — validate the target (non-negative integers → within map bounds → walkable tile via the shared map-grid helper + `UNWALKABLE_TILES = {"water"}` → not blocked by a `blocksMovement` building via `PlacementSystem.occupies`), then set `player.x`/`player.y`. Errors: `intent.move: target out of bounds: (x,y)` / `intent.move: tile (x,y) is not walkable` / `intent.move: blocked by building at (x,y)`. Success detail: `moved to (x,y)`. The player move reuses the shared tile/placement helpers directly — it does NOT go through `MovementSystem.moveTo`, so the npc-move behavior and its tests are untouched (D25). - **`interact {tile:{x,y}}`** — a **smart context action** on a plot tile. The v1 priority order is **content-ized in D33**: the engine reads a single-file `interact` def (`content/interact.yaml`, kind `interact`; no authoring tools — weather/time precedent) at call time — `registry.get("interact", "default")` else the first def of the kind — and iterates its `order` array, executing the FIRST applicable action. **Applicability conditions stay engine mechanisms**; only the ORDER is content policy. Without `content/interact.yaml` the built-in default `[harvest, water, sow, till]` applies (zero behavior change for existing content): 1. **harvest** — plot has a crop at its final stage (detail `harvested `, rolls the drop into inventory). 2. **water** — plot tilled + planted + **not watered** (detail `watered plot`). 3. **sow** — plot tilled + empty + player has a seed. **D35**: the seed comes from `player.selectedSeed` when that field names an **owned** seed-kind item (detail `sowed `), else the FIRST seed-kind item in `player.inventory` (first key whose item def `kind === "seed"`; consumes 1). A successful sow **keeps** `player.selectedSeed` — clearing it is creator policy (the field is set via the `select-seed` action; an invalid/stale selection degrades to the first-seed fallback). 4. **till** — plot absent or **not tilled** (creates the plot entity, detail `tilled plot`). Non-applicable actions in the list are skipped; if none apply, the existing error falls through (`crop is not ready to harvest` for a planted watered immature crop, else `no seed in inventory`). Interact delegates to the existing `PlantingSystem` mechanisms (`till`/`sow`/`water`/`harvest`) — no new farming logic. Validation failures are `ok:false` `IntentResult`s, never throws. - **`buy {shopId, itemId, qty?}`** (D35) — delegate to `EconomySystem.buy` (default `qty` 1). Success detail = the buy result `{shopId, itemId, qty, total, gold}`. Errors (all `ok:false`): `player.intent: buy requires itemId` (missing itemId), `player.intent: buy requires shopId`, `player.intent: buy qty must be a positive integer`, and the economy errors pass through unchanged (`economy: unknown shop: `, `economy.buy: does not sell `, `economy.buy: insufficient gold (need N, have M)`). - **`sell {shopId, itemId, qty?}`** (D35) — delegate to `EconomySystem.sell`. Success detail = the sell result `{shopId, itemId, qty, total, gold}`. Errors mirror buy (`player.intent: sell requires itemId` / `... requires shopId` / `... qty must be a positive integer`; economy errors pass through: `economy.sell: does not buy `, `economy.sell: insufficient in inventory (have N, need M)`). - **`talk {npcId?|tile?}`** (D35) — start an NPC's dialogue. Resolution: `npcId` (must be a `type: "npc"` entity) else `tile` (the npc entity ON that grid cell) else error `player.intent: talk requires npcId or tile`. The dialogue id comes from the NPC entity's `dialogue?: string` field (creator content/Mod spawns set it — the sample mayor carries `dialogue: "mayor"`); an NPC without the field errors `player.intent: npc has no dialogue`. Errors: `player.intent: unknown npc: ` / `player.intent: no npc at tile`. Success calls `DialogueSystem.start(dialogueId)` and the detail = the resulting `DialogueState` `{dialogueId, nodeId, text, choices}`. - **`choose {index}`** (D35) — advance an active dialogue: `DialogueSystem.choose(index)`; detail = the new node state. Errors: `player.intent: choose requires a non-negative integer index` (non-integer/negative), `player.intent: no active dialogue` (nothing active), `dialogue: choice index out of range: ` (index into the available choices; a terminal/all-gated node auto-ends the dialogue on entry — D21). - **`select-seed {seedItemId?}`** (D35) — set/clear the sow seed selection: a non-empty `seedItemId` that names a registered `seed`-kind item sets `player.selectedSeed`; absent/empty clears the field (`delete`). Success detail `{selectedSeed: string|null}`. Error: `player.intent: is not a seed item` (unknown item or non-seed kind). The field is a plain player-entity field — snapshot-able, save.*-covered, Mod-readable. **No prompts and no creation tools**: the player is a runtime entity, not authored content. `player.*` is a debug/input surface (D3 precedent — runtime MCP is for debugging and now for driving the same intent mechanism a human drives from the browser). **Design decisions (D25)**: (1) one engine mechanism, three thin channels — MCP `player.intent`, the WS `{"type":"intent"}` message, and `connectLocal.intent` all call `IntentSystem.handle`; (2) the `player-intent` event fires for EVERY intent including failures so HUDs/viewports always get feedback; (3) the interact priority order was the v1 hardcoded engine default, explicitly documented as content-izable later — **D33 content-izes it** (a single-file `interact` content kind carries the order; applicability stays engine mechanism); (4) player move reuses the shared map-grid/placement helpers instead of `MovementSystem.moveTo`, keeping npc-move behavior and tests untouched. --- ## `animal.*` — Animals (GrowthSystem reuse, D19/D23) Animals **reuse GrowthSystem** (boundaries.md item 6): an animal is a world entity `{id: "animal::", type: "animal", animalId, x, y, stageIndex?, daysInStage?, fed?}` that advances through the **same stage-progression mechanism as crops**, with `feedRequired` replacing `waterRequired` on its stages. There is **no new animal system** — GrowthSystem `step` (driven by the tick loop, Task A) advances both `plot` and `animal` entities in one pass. The animal **definition** (growth cycle + produce table) is data (`content/animals/.yaml`, content kind `animal`); **feed/produce rules** (what food, when, what the produce yields) are behavior-language content (D19) — the engine only emits the events the language hooks. | Kind | Name | Description | |---|---|---| | tool | `animal.validate` / `animal.create` / `animal.update` / `animal.list` / `animal.get` | Definition lifecycle, as `item.*`/`crop.*` (`content/animals/.yaml`) | | tool | `animal.spawn` | Debug: create an animal entity at a grid cell (`{animalId, x, y}` → the entity) | | tool | `animal.feed` | Debug: mark an animal entity fed (`{id}` → `{id, animalId, fed: true}`) — sets the `fed` flag a `feedRequired` stage gates on | | tool | `animal.stage` | Debug: force an animal entity into a growth stage (fires `animal-grow`) | | tool | `animal.entities` | Debug: all placed animal entities (`{animals:[...]}`) | | resource | `animal://catalog` | All animal definitions (normalized JSON) | | resource | `animal://{id}` | Single animal definition (missing id → MCP error `animal not found: `) | | prompt | `animal.create-prompt` | Guided animal authoring (stages, feed needs, produce table) | **`AnimalDef` shape** (content kind `animal`): ```json { "id": "chicken", "stages": [ { "name": "chick", "days": 2, "feedRequired": false }, { "name": "grown", "days": 2, "feedRequired": true }, { "name": "laying", "days": 1, "feedRequired": true } ], "produces": [{ "itemId": "egg", "qty": 1, "chance": 1 }] } ``` - `stages[]` — at least one stage; each `{ name, days, feedRequired }` mirrors the crop stage model with `feedRequired` in place of `waterRequired`. `days` positive int; `feedRequired` boolean. - `produces[]` — at least one `{ itemId, qty, chance? }` (same shape as crop `drops`). This is the **produce table (data only)** — the actual granting is behavior-language content (D19). `chance` optional 0..1 (omitted = always). **Runtime semantics**: `animal.spawn {animalId, x, y}` → `{id: "animal::", type: "animal", animalId, x, y, stageIndex: 0, daysInStage: 0, fed: false}` (`n` = one past the highest existing suffix so multi-instances and post-restore spawns don't collide) or an error result (unknown animal def → `growth: unknown animal def: `; bad coords → `growth: x and y must be non-negative integers`). `animal.feed {id}` → `{id, animalId, fed: true}` (unknown/non-animal entity → `growth: unknown animal: `); feeding emits the `animal-fed` bus event `{animalId, animalDefId}` (behavior-language-matchable via `on: "animal-fed"`; no hook — see below). `animal.stage {id, stageIndex}` → `{id, animalId, animalDefId, stageIndex, stage}` (unknown entity / unknown def / out-of-range index → error result); delegates to `GrowthSystem.forceAnimalStage`, which also emits `animal-grow`. `animal.entities` → `{animals:[placed entities...]}`. **Growth cycle (mechanism)**: each in-game day (GrowthSystem's day boundary, driven by the tick loop) every animal is evaluated: if the current stage's `feedRequired` is true and the animal's `fed` is false, the day is skipped (no growth, no produce — exactly like an unwatered `waterRequired` crop stage). Otherwise a mature animal (final stage) emits **`animal-produce`** `{animalId, animalDefId, stageIndex, stage}` and its `fed` resets to false (feeding is consumed daily); a non-final animal increments `daysInStage` and on reaching `stage.days` advances `stageIndex`, resets `daysInStage` and `fed`, and emits **`animal-grow`** `{animalId, animalDefId, stageIndex, stage}` (the same shape as `crop-grow`, per stage advance including into the final stage). Both events map to hooks **`onAnimalGrow`** / **`onAnimalProduce`**. There is deliberately **no separate `animal-mature` event and no `onAnimalFed` hook**: `animal-grow` already covers reaching maturity, `animal-produce` covers ongoing production, and the `animal-fed` bus event is already behavior-language-matchable — the tri-criterion's "Mod hook demand" arm fails for feed (D22.4's reasoning applies identically). **Design decisions (D23)**: (1) animals are an **entity kind + GrowthSystem generalization**, not a new system — animals are isomorphic to crops (stages, days-per-stage, a per-day gate), so the only new mechanism is the `fed` flag mirroring `watered` (D19 verdict); (2) the animal schema holds the **growth cycle + produce table ONLY** — feed rules and produce/harvest rules are behavior language (D9/decision #9): the engine emits `animal-grow`/`animal-produce`/`animal-fed` bus events and grants nothing; (3) `animal.spawn`/`feed`/`stage`/`entities` are debug tools (D3) — animal instances are runtime entities, but the definition lifecycle is creation-first (mirroring `crop.*`); (4) `animal.list` lists **definitions** (consistent with `item.list`/`crop.list`); placed instances are a separate debug tool `animal.entities` (mirroring `placement.list` vs `placement.building.list`). --- ## `sprite.*` — Procedural Sprites (presentation data, D26) Sprites are the **presentation half** of the rendering presentation pipeline (D26/D31): a `sprite` content kind (`content/sprites/.yaml`) carrying **programmatic draw instructions** — an ordered list of primitive shape layers per animation frame, plus the **`image` layer** (D31: `{shape:"image", src: , x?, y?, w?, h?}` referencing a binary file in `content/assets/`). They are data (authored in YAML, validated by zod) and MCP-addressable like every other content kind. **The image layer references assets by id — assets themselves are NOT a content kind and have NO MCP namespace** (binary files are filesystem content; MCP-crafted YAML references them by id; see D31). A def with `spriteRef` missing on a crop stage/building/animal stage simply means the renderer falls back to the existing shape drawing — `spriteRef` is always optional. | Kind | Name | Description | |---|---|---| | tool | `sprite.validate` | Validate a sprite definition (JSON) against the sprite schema; returns the normalized def | | tool | `sprite.create` | Create a sprite definition: validates, writes `content/sprites/.yaml`, registers live | | tool | `sprite.update` | Patch an existing sprite definition (merge + validate + rewrite + re-register) | | tool | `sprite.list` | List registered sprite ids with one-line summaries | | tool | `sprite.get` | Fetch one sprite definition as normalized JSON | | resource | `sprite://catalog` | Full sprite catalog (normalized JSON) | | resource | `sprite://{id}` | Single sprite definition (missing id → MCP error `sprite not found: `) | | prompt | `sprite.create-prompt` | Guided template for authoring a procedural sprite definition | **`SpriteDef` shape** (content kind `sprite`): ```json { "id": "parsnip-mature", "frames": [ { "name": "idle", "durationTicks": 1, "layers": [ { "shape": "rect", "x": 8, "y": 8, "w": 16, "h": 16, "fill": "#2e7d32" }, { "shape": "circle", "x": 16, "y": 8, "r": 6, "fill": "#66bb6a" } ] } ] } ``` - `id` — unique kebab-case string. - `frames[]` — at least one frame; each frame is a time slice of the sprite. `name` (optional display label) and `durationTicks` (optional non-negative int, how many engine ticks the frame plays before the next; a single-frame sprite is static) describe the frame; `layers` is an ordered paint list (1+, drawn back-to-front). - `layers[]` — each layer is one of `{ shape: "rect"|"circle", x, y, ... }` (per-shape sizing + required `fill` matching `"#rrggbb"`) or `{ shape: "image", src, x?, y?, w?, h? }` (D31; zod **strict**, `shape` is the union discriminator — unknown keys are rejected). `rect` uses `w`/`h` (positive ints); `circle` uses `r` (positive int). The `image` layer's `src` is a required non-empty asset id (a file in `content/assets/`), `x`/`y` position the image's top-left in tile space (default 0,0), and `w`/`h` size it in tile-space units (the crop never exceeds the source image; absent = natural pixel size). Coordinates are relative to the sprite's tile-space anchor (top-left origin; the render layer maps them to tile pixels). **Runtime semantics**: sprite defs are inert data consumed by the render layer (round 2). Entities carrying a `spriteRef` field (player, plots, buildings, animals — set by the systems in api.md §4) reference a def by id; the renderer draws the def's current frame when the ref resolves, falling back to the existing shape drawing when it does not. There are **no new systems and no hooks** — sprites are a content kind + appearance field, and the snapshot frame carries the full `sprites` catalog so viewports never need a second request (D26). **Design decisions (D26/D31)**: (1) programmatic shapes over atlas assets — primitive draw instructions keep content data-driven, testable, and dependency-free; the **`image` layer (D31)** adds per-asset image references (the engine never loads an atlas format — single-tile assets by id; a sprite-sheet atlas is a documented future path); (2) `spriteRef` on crop **stages** / animal **stages** / building defs (and the player entity seed) is optional — a missing ref degrades to shape drawing, so existing content and tests are untouched; (3) the snapshot carries the sprite catalog so render viewports get draw instructions in-band with the world state they present. --- ## `sound.*` — Synthesized Sounds (presentation data, D26) The **audio half** of the presentation pipeline (D26/D31): a `sound` content kind (`content/sounds/.yaml`) carrying **synthesizer instructions** (`synth`) and/or an **audio asset reference** (`src` — a file in `content/assets/`, D31). `play-sound` actions (behavior language v1, §behavior) reference sounds by id and emit a **`sound-play`** bus event `{soundId}` — behavior-language-matchable and broadcast as an event frame so the render layer can play the sound. **Assets themselves are NOT a content kind and have NO MCP namespace** — `src` references an asset by id; binary files are filesystem content (D31). | Kind | Name | Description | |---|---|---| | tool | `sound.validate` | Validate a sound definition (JSON) against the sound schema; returns the normalized def | | tool | `sound.create` | Create a sound definition: validates, writes `content/sounds/.yaml`, registers live | | tool | `sound.update` | Patch an existing sound definition (merge + validate + rewrite + re-register) | | tool | `sound.list` | List registered sound ids with one-line summaries | | tool | `sound.get` | Fetch one sound definition as normalized JSON | | resource | `sound://catalog` | Full sound catalog (normalized JSON) | | resource | `sound://{id}` | Single sound definition (missing id → MCP error `sound not found: `) | | prompt | `sound.create-prompt` | Guided template for authoring a synthesized sound definition | **`SoundDef` shape** (content kind `sound`): ```json { "id": "coin", "synth": { "wave": "sine", "freq": 880, "durationMs": 120, "gain": 0.5, "slideTo": 1320 } } ``` - `id` — unique kebab-case string. - `synth` — the v1 synthesis spec (zod **strict**): `wave` is one of `sine|square|triangle|sawtooth`; `freq` is a positive number (Hz); `durationMs` a positive number; `gain` optional 0..1 (output level); `slideTo` optional positive number (end frequency — a sweep). - `src` — optional **audio asset id** (D31): a file in `content/assets/` the render player decodes and plays (base64 → `decodeAudioData`); **`src` takes precedence over `synth`** at play time, and a `src` whose asset is missing or undecodable is silently skipped. - **At least one of `synth`/`src` is required** (schema `.refine`). **Runtime semantics**: sound defs are inert data consumed by the render layer (round 2). The behavior-language `play-sound {sound}` action emits the **`sound-play`** bus event `{soundId}` (the sound id is emitted regardless of whether the def is registered — the engine's mechanism is the event, matching the audio row of boundaries.md: "engine emits events, render layer plays"). There are **no new systems and no hooks**; the snapshot frame carries the full `sounds` catalog so viewports can synthesize on demand. **Design decisions (D26/D31)**: (1) synth + asset-source sounds over raw audio files — a small declarative synth spec keeps audio data-driven and dependency-free; **`src` audio assets (D31)** add file playback through the same asset pipeline (a `.ogg` in `content/assets/` referenced by id); (2) `play-sound` upgrades from a headless no-op to the documented audio event emitter (boundaries.md "Audio" row) — the event is the mechanism, playback is render-layer freedom. --- ## `rng.*` — Deterministic Seeded RNG (engine mechanism, D28) The engine's **single randomness mechanism** (D28): a deterministic seeded RNG mirroring LÖVE's `love.math.RandomGenerator`. `createEngine({ seed?, ... })` seeds the engine's one `RandomSource` (`engine.rng`, exposed on the facade); every randomness consumer draws from it via `SystemInitCtx.rng` (WeatherSystem day-start rolls and PlantingSystem harvest drop chances — previously bare `Math.random`). Same seed → identical world randomness (replay/tests/Mods); `save.*` snapshots carry `rngState` so restore re-anchors the sequence. MCP is a **debug surface** (D3) for this mechanism — it reads/writes `engine.rng` directly, no system behind it. | Kind | Name | Description | |---|---|---| | tool | `rng.get` | Debug: current RNG state `{seed, calls}` — the seed the world's determinism is anchored to and how many draws have been made since that seed | | tool | `rng.set` | Debug: reset the engine RNG to a seed (finite number required) → `{seed, calls: 0}`; subsequent world randomness becomes deterministic from that seed | | resource | `rng://state` | Current RNG state (same shape as `rng.get`) | **Runtime semantics**: `rng.get` → `{seed, calls}` where `seed` is the anchor (the value most recently passed to `setSeed`/the engine seed) and `calls` counts draws since that seed. `rng.set {seed}` validates the seed is a finite number (non-number/`NaN`/`Infinity` → error result `rng.set: seed must be a finite number`), resets the sequence and the draw counter to 0, and returns the new `{seed, calls: 0}`. **Determinism contract**: every world-random draw (weather rolls on day-start, crop/animal drop chances on harvest) comes from this one source, so two engines with the same seed produce identical simulations; `rng.set` mid-run makes all *subsequent* randomness deterministic from the new seed. **Design decisions (D28)**: (1) one engine-owned RNG injected via `SystemInitCtx` rather than per-system instances — draw order is globally orderable and replayable, and `rng.*` has exactly one surface to report; (2) `rng.get`/`rng.set` + `rng://state` only, **no prompts** — the RNG is not authored content and offers no authoring guidance (D3's debug-surface precedent, mirroring `weather.*`/`time.*`); (3) `save.*` carries `rngState {seed, calls}` (optional, migration-free) and restore re-anchors to the stored seed with `calls` reset to 0 (draw replay is documented as not built — D28). --- ## `particle.*` — Particle Definitions (presentation data, D34) A `particle` def is a **pure-visual emission profile** (content kind `particle`, `content/particles/.yaml`, zod **strict**): `{ id, emitRate?, lifetime?: {min,max}, speed?: {min,max}, angle?, spread?, gravity?, size?: {min,max}, colors?: ["#rrggbb",...], fade? }` — all fields optional; the render layer applies documented engine defaults (emitRate 10, lifetime 500–1000 ms, speed 10–30 px/s, angle 0°, spread 360°, gravity 0 px/s², size 2–4 px, single `#ffffff`, fade true). Particles are **inert presentation data** — no engine system consumes them; content triggers a burst via the behavior-language `emit-event` action (`{action:"emit-event", event:"particle", payload:{particleId, x?, y?, entityId?}}`, payload x/y are tile coords, absent → the named entity or the `player`), the render layer listens for the `particle` bus event and spawns/animates the burst (D34). | Kind | Name | Description | |---|---|---| | tool | `particle.validate` | Validate a particle definition (JSON) against the particle schema → normalized def or error result | | tool | `particle.create` | Validate + write `content/particles/.yaml` + register live (overwrites on duplicate id) | | tool | `particle.update` | Merge a patch over the existing def (validate + rewrite + re-register; unknown id → error `particle.update: particle not found: `) | | tool | `particle.list` | → `{particles: [{id, emitRate}]}` summaries (registration order) | | tool | `particle.get` | → one `ParticleDef` JSON or error result (`particle.get: particle not found: `; blank id → `particle.get: particleId must be a non-empty string`) | | resource | `particle://catalog` | → `{particles: [defs...]}` | | resource | `particle://{id}` | → one def (missing id → MCP error `particle not found: `) | | prompt | `particle.create-prompt` | Guided particle-def authoring (field defaults, emit-event trigger example) | --- ## `shader.*` — Built-In Filter Definitions (presentation data, D34) A `shader` def **selects one of the engine's BUILT-IN parameterized filters** (content kind `shader`, `content/shaders/.yaml`, zod **strict**): `{ id, filter: "blur"\|"tint"\|"grayscale"\|"sepia"\|"hue"\|"brightness", params?: {[k]: number\|string} }` — **no GLSL/shader code in content** (the implementations ship with the engine; content only parameterizes them). Parameter semantics: blur `{strength}` (default 4), tint `{color:"#rrggbb"}` or `{r,g,b}` (0–255, default white), grayscale `{factor}` (0–1 desaturation, default 1), sepia `{}`, hue `{degrees}` (default 0), brightness `{factor}` (default 1). Shader ids are referenced from `SpriteDef.filters` (per-sprite) and from the weather visual mapping (whole-map, D34). Unknown shader id at use time → silently skipped by the render layer. | Kind | Name | Description | |---|---|---| | tool | `shader.validate` | Validate a shader definition (JSON) against the shader schema → normalized def or error result | | tool | `shader.create` | Validate + write `content/shaders/.yaml` + register live (overwrites on duplicate id) | | tool | `shader.update` | Merge a patch over the existing def (validate + rewrite + re-register; unknown id → error `shader.update: shader not found: `) | | tool | `shader.list` | → `{shaders: [{id, filter}]}` summaries (registration order) | | tool | `shader.get` | → one `ShaderDef` JSON or error result (`shader.get: shader not found: `; blank id → `shader.get: shaderId must be a non-empty string`) | | resource | `shader://catalog` | → `{shaders: [defs...]}` | | resource | `shader://{id}` | → one def (missing id → MCP error `shader not found: `) | | prompt | `shader.create-prompt` | Guided shader-def authoring (built-in filter library, params per filter) | --- ## `animation.*` — Animation State Machines (presentation data, D38) An `animation` def is an **animation state machine** (content kind `animation`, `content/animations/.yaml`, zod **strict**): `{ id, initial, states: [{id, spriteRef, fps?, loop?}], transitions?: [{from, to, on?}] }`. Each state maps a **named state** (e.g. `idle`, `walk`, `dance`) to a **sprite def id** — that sprite's internal frames give the state's animation at the state's `fps` (default 10, the `DEFAULT_ANIMATION_FPS`); `loop: false` plays the state once then holds its last frame. `transitions` is an **optional authoring aid** (an allowed-transition graph) — the runtime does **NOT** enforce hard transitions (content can jump between states freely; enforcement would break content freedom, D38). The state machine is **driven by an entity's `anim?: string` field** (a plain entity field the behavior language already sets/clears via `set-state player.anim "dance"` / `set-state player.anim null` — null is a supported clear): the render layer resolves the state as (1) the explicit `anim` field when it names a declared state, else (2) **motion inference** — `"walk"` while the entity is tweening between tiles / `"idle"` when still, else (3) `initial`. An entity whose `spriteRef` matches an animation def id uses that def; no match → today's rendering unchanged (additive, no regression). | Kind | Name | Description | |---|---|---| | tool | `animation.validate` | Validate an animation definition (JSON) against the animation schema → normalized def or error result (bad `initial` / duplicate state ids / transition to an undeclared state / strict-key violations) | | tool | `animation.create` | Validate + write `content/animations/.yaml` + register live (overwrites on duplicate id) | | tool | `animation.update` | Merge a patch over the existing def (validate + rewrite + re-register; unknown id → error `animation.update: animation not found: `) | | tool | `animation.list` | → `{animations: [{id, stateCount}]}` summaries (registration order) | | tool | `animation.get` | → one `AnimationDef` JSON or error result (`animation.get: animation not found: `; blank id → `animation.get: animationId must be a non-empty string`) | | resource | `animation://catalog` | → `{animations: [defs...]}` | | resource | `animation://{id}` | → one def (missing id → MCP error `animation not found: `) | | prompt | `animation.create-prompt` | Guided animation-def authoring (states, spriteRef, fps/loop, state resolution, set-state null clear) | --- ## `scene.*` — Initial-World Scenes (seed-time instantiation, D40.9) A `scene` def is an **initial-entity list** (content kind `scene`, `content/scenes/.yaml`, zod **strict**) the engine instantiates at seed time — the D40.9 mechanism that makes editor scene placement persistable and playtestable (WYSIWYG). `{ id, entities: [{ type: "building", buildingId, x, y } | { type: "npc", npcId, x, y, dialogue?, label? } | { type: "animal", animalId, x, y }] }` — a `z.discriminatedUnion("type", [...])`; `x`/`y` are non-negative integer tile coords on every entity. **Structural validation only** (references to building/animal defs are validated at seed time, not schema time — loading order makes schema-time reference checks unsafe). **Instantiation semantics (seed time, after systems boot, before Mods load, in entity order)**: `building` → `PlacementSystem.place(buildingId, x, y)` (full footprint/bounds/buildable/overlap validation; unknown buildingId or a placement conflict FAILS boot loudly); `npc` → a plain npc entity `{id: npcId, type: "npc", x, y, dialogue?, label?}` (duplicate npc ids FAIL boot loudly — `world.create` silently overwrites, so the seed checks first); `animal` → `GrowthSystem.spawnAnimal(animalId, x, y)` (unknown animalId FAILS boot loudly). Scene instances use the SAME entity id formats as the runtime systems (`building:::`, `animal::`, npc = the `npcId` directly), so they are indistinguishable from runtime-placed entities — save snapshots, the WS snapshot, and viewport rendering all work for free. **v1: every registered scene instantiates** — multi-scene selection is a future creator-content decision. | Kind | Name | Description | |---|---|---| | tool | `scene.validate` | Validate a scene definition (JSON) against the scene schema → normalized def or error result (bad discriminated-union `type`, negative coords, strict-key violations) | | tool | `scene.create` | Validate + write `content/scenes/.yaml` + register live (overwrites on duplicate id; a NEW scene takes effect on the next engine start — instances are seeded at boot) | | tool | `scene.update` | Merge a patch over the existing def (validate + rewrite + re-register; unknown id → error `scene.update: scene not found: `) | | tool | `scene.list` | → `{scenes: [{id, entityCount}]}` summaries (registration order) | | tool | `scene.get` | → one `SceneDef` JSON or error result (`scene.get: scene not found: `; blank id → `scene.get: sceneId must be a non-empty string`) | | resource | `scene://catalog` | → `{scenes: [defs...]}` | | resource | `scene://{id}` | → one def (missing id → MCP error `scene not found: `) | | prompt | `scene.create-prompt` | Guided scene-def authoring (entity types, id formats, seed-instantiation semantics, overlap/buildability notes) | --- ## Subsystem Checklist (MANDATORY) When adding a subsystem, complete ALL of: - [ ] Definition lifecycle tools: `validate` / `create` / `update` / `list` / `get` (writing `content//.yaml`, hot-reloading) - [ ] Debug tools only where runtime control is meaningful (D3) - [ ] At least one resource exposing the subsystem's data as normalized JSON - [ ] At least one authoring prompt - [ ] Schema + system + namespace + hook points (plugin template, D12) - [ ] `docs/mcp.md` section AND `docs/api.md` mapping updated **in the same change** (D-op2) --- ## Change Log | Date | Change | Files | |---|---|---| | 2026-08-13 | **`scene` content kind + `scene.*` MCP namespace (D40.9) — MCP surface now 124 tools / 36 resources / 17 prompts / 22 namespaces.** New content kind `scene` (`content/scenes/.yaml`, zod **strict** — `{id, entities: [{type: "building", buildingId, x, y} \| {type: "npc", npcId, x, y, dialogue?, label?} \| {type: "animal", animalId, x, y}]}` via `z.discriminatedUnion("type")`; `x`/`y` non-negative integers; **structural validation only** — building/animal references are validated at seed time, not schema time). New engine start step **`seedScenes`** (AFTER `initSystems`, BEFORE Mods load — system APIs are callable): every registered scene instantiates in entity order — `building` via `PlacementSystem.place` (full footprint/bounds/buildable/overlap validation; unknown buildingId or placement conflict FAILS boot loudly), `npc` as a direct entity `{id: npcId, type: "npc", x, y, dialogue?, label?}` (duplicate npc id FAILS boot loudly — `world.create` silently overwrites), `animal` via `GrowthSystem.spawnAnimal` (unknown animalId FAILS boot loudly). Scene instances use the SAME entity id formats as the runtime systems (`building:::`, `animal::`, npc = the `npcId`) — save.*, the WS snapshot, and the viewport work for free. v1: ALL registered scenes instantiate (multi-scene selection is a future creator-content decision). Namespace mirrors the authoring namespaces through the shared `authoring.ts` layer: `scene.validate`/`create`/`update` + `scene.list` (`{scenes: [{id, entityCount}]}`) + `scene.get` (`scene not found: `) + resources `scene://catalog`/`scene://{id}` + prompt `scene.create-prompt` (entity types, id formats, seed-instantiation semantics, overlap/buildability notes). `server.orient` lists the new tools and the `content/scenes/.yaml` authoring path. **No scene content added to examples/sample-game/ or content/** (the sample mod spawns the mayor and players place buildings — a sample scene would collide with existing tests/play loops); scene demonstration lives in tests + docs. Tests: core `scene.test.ts` (13), mcp `scene-namespace.test.ts` (9). **758 tests green** (was 736). See `decisions.md` D40. | `mcp.md`, `api.md`, `decisions.md` (D40), `AGENTS.md`, `packages/core` (schema/scene.ts, schema/index.ts, engine.ts, index.ts), `packages/mcp` (namespaces/scene.ts, index.ts, namespaces/server.ts, test/scene-namespace.test.ts), `apps/mcp-server/test/client-surface.test.ts` | | 2026-08-13 | **Shared authoring layer extraction + `map.*` creation tools (D40 foundation) — MCP surface now 119 tools / 34 resources / 16 prompts / 21 namespaces.** The validate/create/update definition-lifecycle logic duplicated in every authoring namespace (`item`/`crop`/`shop`/`behavior` trigger+statemachine/`dialogue`/`placement` building/`animal`/`sprite`/`sound`/`particle`/`shader`/`animation`) is extracted into a single shared module **`packages/mcp/src/authoring.ts`**: `validateDef(kind, raw, tool)` (schema `safeParse` → normalized def or formatted error), `writeContentFile(engine, kind, def)` (mkdir + write `${def.id}.yaml` + live registry register, behind the `requireContentDir` bundle-mode guard), `updateContentFile(engine, kind, id, patch, {tool, errorNoun})` (merge + validate + remove + rewrite + re-register), and `authoringTools(engine, kind, opts)` generating the three MCP lifecycle tools with per-kind descriptions (tool prefix `item`/`behavior.trigger`/`placement.building`, display noun, error noun, article, schema label). **All existing authoring namespaces now call the shared module — zero behavior change** (all 727 pre-existing tests green proves byte-identical semantics: identical error strings, write paths, register/overwrite/remove-re-register order). **`map.*` gains `validate`/`create`/`update`** (5 tools total with `list`/`get`; the D40 early ruling that "map has no creation tools" is overturned — the shared layer adds map write, and visual map editing in the GUI editor writes map YAML through the same path): identical semantics to `item.*` — create writes `content/maps/.yaml` + `registry.register`, error result on invalid without writing, update merges, `map.update: map not found: `, map authoring touches only registry/file (the world map ENTITY is not re-seeded). New prompt **`map.create-prompt`** (grid dimensions, layers, spawn, walkability authoring guidance). Resources unchanged (34); namespaces unchanged (21). New `packages/mcp/test/map-namespace.test.ts` (9 — mkdtemp lifecycle: validate valid/invalid, create write+register+duplicate overwrite, update merge+rewrite/unknown/invalid, list/get, `map://{mapId}`, create-prompt). **736 tests green** (was 727). See `decisions.md` D40 (implementation note). | `mcp.md`, `api.md`, `decisions.md` (D40), `AGENTS.md`, `packages/mcp` (authoring.ts, namespaces/{item,crop,shop,behavior,dialogue,placement,animal,sprite,sound,particle,shader,animation,map}.ts, test/map-namespace.test.ts), `apps/mcp-server/test/client-surface.test.ts` | | 2026-08-13 | **Engine rename to nEngine (D39) — NO MCP surface change (116 tools / 34 resources / 15 prompts / 21 namespaces held).** Cosmetic identity/branding rename only: packages `@farm/*` → `@nengine/*`, MCP stdio server identity `farm-mcp-server` → `nengine-mcp-server`, `server.info`/`server://status` engine id `"farm-engine"` → `"nengine"` (shape unchanged), save/bundle format literals `farm-save`/`farm-bundle` → `nengine-save`/`nengine-bundle` (v1-era breaking change — old files not accepted; `CURRENT_SAVE_VERSION`/`BUNDLE_VERSION` stay 1), packaging CLI `farm-pack` → `nengine-pack`, Vite defines `__FARM_BUNDLE_DEFAULT__`/`__FARM_ASSETS_BASE__` → `__NENGINE_*`, env vars `FARM_CONTENT_DIR`/`FARM_MODS_DIR`/`FARM_WS_PORT` → `NENGINE_*` (CLI flags unchanged), split-assets dir `farm-assets` → `nengine-assets`, `appId` prefix `com.farm.` → `com.nengine.`. `server.orient` + the creation-namespace prompts address "the nEngine". **No tool/resource/prompt signatures change, no protocol changes, no system behavior changes**; creator content is untouched (map id `"farm"`, `"farm-tiles"`, game name "Pinefield Valley"). Historical changelog entries keep their original text (append-only). See `decisions.md` D39. | `mcp.md`, `api.md`, `architecture.md`, `boundaries.md`, `decisions.md` (D39), `AGENTS.md`, `docs/engine-guide-zh.md`, `examples/sample-game/README.md`, `README.md`, `package.json`, all workspace package.json files, `apps/mcp-server` (src + tests), `apps/pack` (src + tests), `apps/dev` (src + vite config), `packages/core` (schema/save.ts, content/bundle.ts, content/loader.ts + tests), `packages/mcp` (namespaces + tests), `packages/render` (src + tests) | | 2026-08-13 | **Animation state machines + smooth movement (D38) — MCP surface now 116 tools / 34 resources / 15 prompts / 21 namespaces.** One additive creation-first namespace mirroring `sprite.*`/`sound.*`/`particle.*`/`shader.*` (D26/D34): **`animation.*`** (content kind `animation`, `content/animations/.yaml` — `{id, initial, states: [{id, spriteRef, fps?, loop?}], transitions?: [{from, to, on?}]}` zod strict; a **state machine over sprite defs** — each state references a sprite def whose frames animate at the state's `fps` (default 10), `loop: false` plays once then holds; `transitions` is an optional authoring aid the runtime does NOT enforce). The state machine is driven by an entity's `anim?: string` field (behavior-language `set-state player.anim "dance"` / `set-state player.anim null` — null is a supported clear, no language change needed) with render-layer **motion inference** fallback (`"walk"` while moving, `"idle"` when still, else `initial`); an entity whose `spriteRef` matches an animation def id uses it, otherwise rendering is unchanged. Namespace: `validate`/`create`/`update`/`list`/`get` + `animation://catalog`/`animation://{id}` resources + `animation.create-prompt`, exact shapes/errors in the section above. The WS snapshot frame gains an additive `animations: AnimationDef[]` catalog (same pattern as `particles`/`shaders`/`shops`). `server.orient` lists the new tools. **Smooth movement is render-layer-only** (engine stays authoritative instant grid-jumps — `moveTo`/`IntentSystem.move` untouched): the render viewport tweens consecutive known tile coords over `MOVE_DURATION_MS` (250 ms, easeOutQuad) with walk/idle motion inference driving the animation state; no protocol changes. See `decisions.md` D38. | `mcp.md`, `api.md`, `decisions.md` (D38), `AGENTS.md`, `packages/core` (schema/animation.ts, schema/index.ts, index.ts), `packages/mcp` (namespaces/animation.ts, index, server), `packages/render` (tween.ts, viewport, sprite-draw, sync), `apps/mcp-server` (ws-broadcast, tests), `examples/sample-game` (content/animations/player.yaml, content/sprites/player-walk.yaml) | | 2026-08-13 | **WS snapshot assets on-demand (D37) — NO MCP surface change (20 namespaces / 111 tools / 32 resources / 14 prompts held).** Protocol evolution in the WebSocket world-sync channel only (a render-viewport transport, NOT an MCP namespace): the snapshot frame's `assets` field (D31 — full base64 table in EVERY snapshot) is replaced by a **metadata-only `assetsIndex`** (`Record` — no bytes); a new client→server **`{"type":"assets-request","ids":string[]}`** message (bounded — the client coordinator batches ≤ 64 ids, the server truncates over the cap) is answered per-client with a new **`{"type":"assets","assets":Record}`** frame (requested ids only, unknown ids ignored, empty request → empty response). **Versioned protocol change**: old clients reading `snapshot.assets` break — `connectWs` now reads `assetsIndex`, derives the wanted ids from the snapshot's presentation (`map.tiles` + sprite `image` layers + `sound.src` — pure `snapshotAssetRefs`), dedupes in-flight/loaded ids (`AssetCoordinator`, pure node-testable), and feeds `{type:"assets"}` responses into the viewport/sound via the existing `setAssets`/async-load paths (late-arriving assets re-render through the viewport's existing redraw-on-ready). New ws-broadcast tests: snapshot carries `assetsIndex` without base64, request→response roundtrip, unknown ids ignored, empty request → empty `assets:{}`, 64-id truncation. See `decisions.md` D37. | `mcp.md`, `api.md`, `decisions.md` (D37), `AGENTS.md`, `packages/render` (sync/ws, sync/asset-coordinator, sync/asset-refs), `apps/mcp-server` (ws-broadcast + tests) | | 2026-08-13 | **Mod packaging + browser Mod execution + installer generation (D36) — NO MCP surface change (111 tools / 32 resources / 14 prompts / 20 namespaces held).** Mods are **code, not data** (D6), so Mod packaging touches **no MCP surface**: no new namespace (`pack.*` still deliberately does not exist — packaging stays an offline build-time tool), no tools, no resources, no prompts, no content-kind registration, no protocol changes. What changed is the engine's **Mod mechanism**: `ContentBundle` gains an additive `mods?: Record` (zod-validated; `BUNDLE_VERSION` stays 1 — old bundles load unchanged), `ModLoader` gains `loadFromSource(source, id, version?)` + `reloadFromSources(sources)` (ESM source executed via a base64 `data:text/javascript` dynamic import — the SAME mechanism in Node ≥ 20 and browsers, zero fs access, validation/binding identical to file mods), and `createEngine({bundle})` boots packaged Mods from `bundle.mods` when no `modsDir` is configured (`modsDir` wins when both are given). The `mod.*` namespace surface is unchanged — `mod.list`/`mod.status`/`mod.reload` already read `engine.mods`, which now reflects bundled Mods in bundle mode (all-at-once reload). `farm-pack` gains `--mods ` (packages Mods into `bundle.mods`) and `--installer` (implies `--desktop`; writes an `electron-builder.yml` + `dist`/`dist:win` scripts so the desktop shell can build installers). See `decisions.md` D36. | `mcp.md`, `api.md`, `decisions.md` (D36), `AGENTS.md`, `packages/core` (content/bundle.ts, mods/loader.ts, engine.ts, index.ts), `apps/pack` (src/index.ts, src/desktop.ts, test/pack.test.ts), `package.json`, `examples/sample-game/README.md`, `docs/engine-guide-zh.md` | | 2026-08-13 | **In-game UI + player-intent action extension (D35) — MCP surface held at 111 tools / 32 resources / 14 prompts / 20 namespaces.** `player.intent`'s inputSchema action enum extends from `move`/`interact` to `move`/`interact`/`buy`/`sell`/`talk`/`choose`/`select-seed` (the tool count stays 1 — **no new tools**, no new resources/prompts). New args on the schema: `itemId`/`qty`/`shopId` (buy/sell, defaults qty 1), `npcId`/`tile` (talk), `index` (choose), `seedItemId` (select-seed). Exact action semantics and error strings are documented in the `player.*` section above (D35) — `buy`/`sell` delegate to `EconomySystem` with the transaction as result detail, `talk` starts an NPC's dialogue (from the NPC entity's new `dialogue` field — sample mayor carries `dialogue: "mayor"`), `choose` advances the active dialogue, `select-seed` sets/clears `player.selectedSeed`. The WS snapshot frame gains an additive `shops: ShopDef[]` catalog (D35 — the dev UI shop panel reads buy/sell rows from it in ws mode; local mode reads the registry) and the server-side intent gate accepts all seven actions (message shapes unchanged). No other tool/resource/prompt signatures change. See `decisions.md` D35. | `mcp.md`, `api.md`, `decisions.md` (D35), `AGENTS.md`, `packages/core` (systems/intent.ts, schema/building.ts, systems/placement.ts), `packages/mcp` (namespaces/player.ts), `packages/render` (viewport, sync/local, sync/ws), `apps/mcp-server` (ws-broadcast, tests), `apps/dev` (index.html, src/main.ts, src/ui.ts + ui.test.ts), `examples/sample-game` (content/buildings/shop.yaml, content/sprites/shop.yaml, mods) | | 2026-08-13 | **Render-layer completions (D34) — MCP surface now 111 tools / 32 resources / 14 prompts / 20 namespaces.** Two additive creation-first namespaces mirroring `sprite.*`/`sound.*` (D26): **`particle.*`** (content kind `particle`, `content/particles/.yaml` — `{id, emitRate?, lifetime?: {min,max}, speed?: {min,max}, angle?, spread?, gravity?, size?: {min,max}, colors?, fade?}` zod strict; pure-visual emission profile; the render layer spawns event-triggered bursts from the `particle` bus event, which content fires via the existing behavior-language `emit-event` action) and **`shader.*`** (content kind `shader`, `content/shaders/.yaml` — `{id, filter: blur\|tint\|grayscale\|sepia\|hue\|brightness, params?}` zod strict; parameterized BUILT-IN filters, **NO GLSL in content**; referenced from `SpriteDef.filters` and the weather visual mapping). Each namespace: `validate`/`create`/`update`/`list`/`get` + `catalog`/`{id}` resources + `create-prompt`, with the exact shapes/error conventions documented in their sections. **No other tool/resource/prompt signatures change**: particles/shader are render-layer + content-kind only — no engine systems, no hooks, no protocol changes (the WS snapshot frame only gains additive optional `particles`/`shaders` catalogs; the renderer also gains entity text labels via the plain `label` entity field and sprite-sheet frame crops — both outside the MCP surface). `server.orient` lists the new tools. See `decisions.md` D34. | `mcp.md`, `api.md`, `decisions.md` (D34), `AGENTS.md`, `packages/core` (schema/particle.ts, schema/shader.ts, schema/sprite.ts), `packages/render` (viewport, particles, filters, weather, labels, sprite-draw, sync), `packages/mcp` (namespaces/particle.ts, namespaces/shader.ts, index, server), `apps/mcp-server` (ws-broadcast, tests) | | 2026-08-13 | **Core mechanism completions (D33) — MCP surface now 101 tools / 28 resources / 12 prompts / 18 namespaces.** One additive tool: **`npc.path`** (debug A* grid pathfinding query — `{npcId, to:{x,y}}` → `{npcId, from, to, path: [{x,y},...]}` including start and goal, or `{npcId, path: null, reason: "unreachable"\|"out of bounds"\|"blocked"}`; errors `npc.path: npcId must be a non-empty string` / `movement: unknown npc: ` / `npc.path: to must have integer x and y`; pure query — never moves the npc; backs `MovementSystem.findPath`). No other tool/resource/prompt signatures change: **map walkability is data-driven** (optional `MapDef.walkable?: Record`, declared-true overrides the `UNWALKABLE_TILES` fallback — zero content change for existing maps; consumed via the new shared `isWalkableTile` helper in map-grid, replacing the direct constant checks in movement/intent/placement buildable `"map"`), the **behavior language gains the `add` incremental action** (`{action:"add", path, value}` — `set-state` stays absolute; missing field = 0; non-numeric field throws; value must be finite), a **new single-file content kind `interact`** (`content/interact.yaml` `{id, order: [harvest, water, sow, till]}` — content-izes the player interact priority ORDER; applicability stays engine mechanism; NO MCP tools — single-file-kind weather precedent), and **`save.*` covers TimeSystem** (optional additive `time: {day, season, tickInDay}` in `WorldSnapshot` — old snapshots remain valid, `CURRENT_SAVE_VERSION` stays 1; snapshot captures `TimeSystem.get()`; restore calls `time.set(day, season, tickInDay)`). `interact` is registered as a content kind but the sample game's def carries the default order, so no MCP authoring tools were added. See `decisions.md` D33. | `mcp.md`, `api.md`, `decisions.md` (D33), `AGENTS.md`, `packages/core` (map-grid, movement, intent, placement, time, save, behavior-runtime, pathfinding, schema), `packages/mcp` (npc.ts, behavior.ts, dialogue.ts, server.ts), `apps/mcp-server/test/client-surface.test.ts`, `apps/mcp-server/test/sample-game.test.ts`, `packages/core/test`, `packages/mcp/test/npc-namespace.test.ts`, `examples/sample-game/content` | | 2026-08-13 | **Desktop packaging (D32) — NO MCP surface change (100 tools / 28 resources / 12 prompts / 18 namespaces held).** Packaging is an **offline build-time tool**; the desktop shell adds **no runtime engine capability** — the Electron app serves the exact same bundle-mode static site over a local HTTP server. Nothing MCP-visible changes: no new namespace (`pack.*` still deliberately does not exist), no tools, no resources, no prompts, no content-kind registration, no protocol changes. The engine-side mechanisms the shell wraps are unchanged (`ContentBundle` + `ContentLoader.loadBundle` + `createEngine({bundle})` + LocalSync). See `decisions.md` D32. | `mcp.md`, `api.md`, `decisions.md` (D32), `architecture.md`, `AGENTS.md`, `examples/sample-game/README.md`, `docs/engine-guide-zh.md`, `apps/pack/src/index.ts`, `apps/pack/src/desktop.ts`, `apps/pack/test/pack.test.ts`, `package.json`, `pnpm-workspace.yaml` | | 2026-08-13 | **Dev-app/packaged-game day driver fixed (D-op2) — NO MCP surface change (100 tools / 28 resources / 12 prompts / 18 namespaces held).** The apps/dev "Advance day" button (reused verbatim by the packaged static site) previously called `time.advance` — the D23 **clock mover** — so a day jump moved the clock and fired `day-start` triggers/weather but never advanced `GrowthSystem`; in the packaged game a planted crop never progressed past its first stage. It now calls `engine.tick(dayLengthTicks)` — the D23 **simulation driver** (the same path the headless core's `engine.tick` and MCP `time.tick` use) — stepping time AND growth together. No engine-core/schema/MCP changes. Regression guard: the `apps/pack` packGame integration test now asserts the full sow → water → two `tick(480)` → grown cycle incl. `spriteRef` progression (parsnip-sprout → parsnip-grown); E2E (headless chromium) re-verified the packaged site's asset-rendered tiles/crops/player and that a watered parsnip reaches the grown sprite in the browser. | `api.md`, `mcp.md`, `apps/dev/src/main.ts`, `apps/pack/test/pack.test.ts`, `AGENTS.md` | | 2026-08-13 | **Asset pipeline (D31) — NO MCP surface change (100 tools / 28 resources / 12 prompts / 18 namespaces held).** Binary assets (`content/assets/`: `.png`/`.ogg`) are **filesystem content, not a content kind and not an MCP namespace** — there is deliberately **no `assets.*` namespace, no new tools, no new resources/prompts, no schema-kind registration, and no protocol-message changes** (the WS snapshot frame only gains an additive `assets` field). The MCP-visible changes are **schema-shape only** on existing content kinds: `sprite` layers accept a third shape `{shape:"image", src: , x?, y?, w?, h?}` (creation tools `sprite.validate`/`create`/`update` accept it; `src` is validated as a non-empty string — the loader resolves the id against `content/assets/` at render time, and a missing asset degrades to the shape fallback), and `sound.src` is now a real audio asset id (previously reserved). `map` gains an optional `tiles` mapping (not MCP-created — maps are authored YAML, `map.*` is read-only). Rationale (D31.8): binary files are not MCP-creatable in a JSON-tool workflow, not introspection targets (the snapshot `assets` field is the debug surface), and have no runtime behavior to drive — the "MCP manipulability" arm of the tri-criterion fails, so no namespace is built. See `decisions.md` D31. | `mcp.md`, `api.md`, `decisions.md` (D31), `architecture.md`, `boundaries.md`, `AGENTS.md`, `examples/sample-game/README.md`, `docs/engine-guide-zh.md` | | 2026-08-12 | **Game packaging (D30) — NO MCP surface change (100 tools / 28 resources / 12 prompts / 18 namespaces held).** Packaging is an **offline build-time tool** (`apps/pack`, `farm-pack`), not a runtime capability, so it deliberately gets **no MCP namespace**: `pack.*` tools would be a debug/creation surface (D3) for a step that has no runtime engine counterpart — the engine-side mechanism it serves IS exposed (the `ContentBundle` type + `ContentLoader.loadBundle` + `createEngine({bundle})`, see `api.md` §2/§9). The MCP creation namespaces' write helpers gained a guard: `contentDir` is now `string \| undefined` on the engine (bundle-mode engines are read-only) and the write helpers throw a clear error on `undefined` — an error result through the gateway, never a crash; surface counts unchanged. See `decisions.md` D30. | `mcp.md`, `api.md`, `decisions.md` (D30), `architecture.md`, `AGENTS.md`, `examples/sample-game/README.md`, `docs/engine-guide-zh.md` | | 2026-08-12 | **Tier 3 content round (D29) — NO MCP surface change (100 tools / 28 resources / 12 prompts / 18 namespaces held).** The only engine change is the **`onPlayerIntent` hook** (HOOK_NAMES 15→16, wired in `engine.wireEvents` from the `player-intent` bus event; fires per `IntentSystem.handle` call, success AND failure, with `HookContext.payload` = the intent result `{action, tile, ok, error?, detail?}`). Hooks are NOT MCP surface (D-op2) — the hook exists to serve the "Mod hook demand" arm of the tri-criterion: Mods (hooks-only, D6/D13) cannot subscribe to the raw `player-intent` bus event that content matches via `on: "player-intent"`, and the intent payload is transient (not readable from world state), so the D25.4 no-hook stance is reversed (see `decisions.md` D29.2). Sample-game content demonstrates the split: the fishing Mod hooks `onPlayerIntent` to settle catches (fish item — existing `item` kind, `mod:fish-caught`/`mod:fish-too-tired` custom events), and the `fishing-treat` state machine matches `mod:fish-caught` for a reward. No new namespaces, no new content kinds, no tool/resource/prompt signature changes. | `mcp.md`, `api.md`, `decisions.md` (D29), `boundaries.md`, `AGENTS.md`, `examples/sample-game/README.md` | | 2026-08-12 | **`server.enable_subsystem`/`server.disable_subsystem` tools implemented** (closes the D9 contract gap — the tools were spec'd in this section since the walking skeleton; surface now 100 tools / 28 resources / 12 prompts / 18 namespaces). Two new `server.*` tools wrapping the gateway's existing `enable(name)`/`disable(name)`: `server.enable_subsystem {name}` → `{name, enabled: true}`, `server.disable_subsystem {name}` → `{name, enabled: false}`. Errors: non-string/empty `name` → `: name must be a non-empty string`; unknown namespace → `: unknown subsystem: ` (the gateway's `McpGatewayError` throw on a missing namespace is caught and converted, consistent with other namespaces' errorResult patterns); no gateway option → `: gateway not available`. Disabling a namespace removes its tools/resources/prompts from `listTools`/`listResources`/`listPrompts` and makes its tools reject calls (`namespace "" is disabled`); `server.info`/`server.status`/`server://status` reflect `{name, enabled: false}`; `server.enable_subsystem` restores them. **Guard rail (documented decision):** `server.disable_subsystem {name:"server"}` is rejected with `server.disable_subsystem: cannot disable the server subsystem` — the `server` namespace hosts the very tool used to re-enable it. State is per-gateway in-memory (D9 — no persistence, no engine-system impact). `server.orient` lists the new tools automatically when gateway-backed (fallback static list updated). `time`/`crop`/`server` used as the enable/disable test subjects. | `mcp.md`, `api.md`, `decisions.md`, `AGENTS.md`, `apps/mcp-server/test/client-surface.test.ts`, `packages/mcp/test/server-namespace.test.ts` | | 2026-08-12 | **`rng.*` namespace implemented — deterministic seeded RNG (D28; surface now 98 tools / 28 resources / 12 prompts / 18 namespaces).** New engine mechanism mirroring LÖVE's `love.math.RandomGenerator`: `packages/core/src/random.ts` exports `RandomSource` (`{next() /* [0,1) */, nextInt(max) /* [0,max) */, setSeed(seed), state(): {seed, calls}}`) + `seedRandom(seed)` (mulberry32-style, zero deps — same seed → identical sequence) + `randomSeed()` (unseeded default from `Math.random` + `Date.now`). `createEngine({seed?, ...})` seeds the facade's `readonly rng: RandomSource`; `SystemInitCtx` gains optional `rng?: RandomSource` (passed at init). WeatherSystem's default RNG now resolves `ctx.rng` when available (constructor-injected `WeatherRng` still wins for tests); PlantingSystem's harvest drop chance draws from `ctx.rng.next()` instead of bare `Math.random` — the engine's only two randomness consumers now share one seeded source. **MCP surface**: `rng.get` (no args → `{seed, calls}` — the current anchor + draw count), `rng.set` (`{seed}` finite number → `{seed, calls: 0}`, subsequent world randomness deterministic from that seed; non-finite/non-number → error result `rng.set: seed must be a finite number`), resource `rng://state` (same shape), **no prompts** (debug surface, D3). **Save integration**: `WorldSnapshot` gains optional top-level `rngState?: {seed, calls}` (zod optional — old snapshots remain valid, migration-free); `save.snapshot` includes it from `ctx.rng`, `save.restore` re-anchors via `setSeed(rngState.seed)` with `calls` reset to 0 (draw replay not built — documented). See `decisions.md` D28 (full LÖVE-gap adjudication) and `api.md` §1/§4/§7. | `mcp.md`, `api.md`, `decisions.md` (D28), `boundaries.md`, `AGENTS.md` | | 2026-08-12 | **`sprite.*` and `sound.*` namespaces implemented — presentation pipeline data/protocol half (D26; surface now 96 tools / 27 resources / 12 prompts / 17 namespaces).** Two new content kinds: `sprite` (`content/sprites/.yaml`, `spriteSchema` — zod strict; `{id, frames: [{name?, durationTicks?, layers: [{shape:"rect"\|"circle", x, y, w?/h? or r?, fill:"#rrggbb"}]}]}`, programmatic draw instructions, no atlas/image in v1) and `sound` (`content/sounds/.yaml`, `soundSchema` — zod strict; `{id, synth?: {wave:"sine"\|"square"\|"triangle"\|"sawtooth", freq, durationMs, gain?, slideTo?}, src?}` with at least one of `synth`/`src`). Both namespaces mirror `item.*` exactly (creation-first): `sprite.validate`/`create`/`update`/`list`/`get` + resources `sprite://catalog`/`sprite://{id}` + prompt `sprite.create-prompt`; `sound.validate`/`create`/`update`/`list`/`get` + resources `sound://catalog`/`sound://{id}` + prompt `sound.create-prompt` — validate against the schema (no write on failure), write `content//.yaml`, register live (`create` registers/overwrites, `update` removes then re-registers). Exact shapes: `sprite.list` → `{sprites:[{id, frameCount}]}`; `sound.list` → `{sounds:[{id, hasSynth, hasSrc}]}`; catalog resources → `{sprites:[defs...]}`/`{sounds:[defs...]}`; `{id}` resources → one def (missing → MCP error `sprite not found: `/`sound not found: `). **WS protocol**: snapshot frame gains `sprites: SpriteDef[]` and `sounds: SoundDef[]` (from `registry.list` at snapshot time — the render viewport needs the draw/audio instructions in-band); new client→server `{"type":"input","input":string}` message → the server emits an **`input`** bus event `{input}` broadcast as an event frame (behavior-language-matchable `on:"input"`), and `key:ArrowUp`/`ArrowDown`/`ArrowLeft`/`ArrowRight` additionally drive `IntentSystem.handle("move", {tile})` one tile from the player's **current** position (player-centered); unknown prefixes/keys are broadcast-only; a non-string/empty `input` never crashes — a synthesized `input` error frame `{input, ok:false, error:"input must be a non-empty string"}` is broadcast instead. **Behavior language**: `play-sound` upgrades from a no-op to emitting the **`sound-play`** bus event `{soundId}` (documented mechanism; render layer plays in round 2). New schema/def fields: `CropDef.stages[].spriteRef?`, `BuildingDef.spriteRef?`, `AnimalDef.stages[].spriteRef?` (all optional strings — missing = renderer falls back to shape drawing); systems maintain entity appearance: `PlantingSystem.sow` sets `plot.spriteRef` (crop stage 0), `GrowthSystem` sets `plot.spriteRef` on crop stage advance and `animal.spriteRef` on spawn + animal stage advance, `PlacementSystem.place` copies `building.spriteRef` onto the entity, and the engine seeds `player.spriteRef = "player"`. No new systems and no new hooks (sprites/sounds are content kinds; the input mapping wires into the existing `IntentSystem` from the broadcast server). See the `sprite.*`/`sound.*` sections above and `decisions.md` D26. | `mcp.md`, `api.md`, `decisions.md` (D26), `boundaries.md`, `architecture.md`, `AGENTS.md` | | 2026-08-12 | **`player.*` namespace implemented — player input via IntentSystem (D25; surface now 86 tools / 23 resources / 10 prompts / 15 namespaces).** New engine primitive `IntentSystem` (id `"intent"`, auto-booted): `handle(action, args)` → `IntentResult {action, tile, ok, error?, detail?}` for `move {tile:{x,y}}` (validate + set `player.x`/`player.y`, reusing the shared map-grid/placement helpers — `MovementSystem.moveTo` untouched) and `interact {tile:{x,y}}` (smart context action with the v1 priority order: mature crop → harvest; planted & not watered → water; tilled & empty & seed → sow with the FIRST seed-kind inventory item; not tilled → till; else error). Every `handle` call emits the `player-intent` bus event (successes AND failures — HUD/viewport feedback depends on it). **MCP surface**: one tool `player.intent` (`{action, tile}`; errors as errorResult `player.intent: `) + one resource `player://state` (live player entity JSON). **WS protocol**: snapshot frame gains `time: {day, season, tickInDay}` (fixes the viewport HUD stuck on "booting…" — `ws.ts` `applyTime` now also reads snapshot.time); new client→server `{"type":"intent","action","args"}` message → `IntentSystem.handle` → `player-intent` event frame broadcast (bad input → synthesized error frame, never a crash). **Sync handles**: `LocalSyncHandle`/`WsSyncHandle` both gained `intent(action, args)` (local delegates to the engine IntentSystem; ws sends the intent message) — both dev modes are now playable. `viewport.tileAt(clientX, clientY)` → screen→tile. `apps/dev` gains an Interact/Move toolbar + canvas-click intents + HUD (day/season, seed counts, gold, auto-clearing intent feedback). **No schema/content/hook changes**; prompts unchanged (10). | `mcp.md`, `api.md`, `architecture.md`, `boundaries.md`, `decisions.md` (D25), `AGENTS.md` | | 2026-08-12 | **WS multi-viewport sync implemented (D8/D24)** — the final Tier 2 item. `apps/mcp-server` gains an optional `--ws-port ` / `FARM_WS_PORT` WebSocket broadcast channel running in the same process (zero-flag behavior unchanged). **No MCP surface change** — the channel is separate, NOT a namespace: 85 tools / 22 resources / 10 prompts / 14 namespaces stay identical. Protocol (transport section above): snapshot on connect (map def + world entities) and on `{"type":"sync"}`, event frames broadcast to all clients for every bus emission, defensive JSON serialization (non-serializable payloads skipped with a log line, never a crash). `packages/render` gains `sync/ws.ts` `connectWs` (same handle shape as `connectLocal`) and snapshot entity drawing in the viewport (plots with tilled/watered/crop-stage colors, buildings as footprint blocks, npc/player/animal dots — no art). `apps/dev` is parameterized via URL query (`?contentDir=`/`?modsDir=` default to the repo roots + LocalSync; `?sync=ws&url=ws://...` renders a headless core's world instead of booting one). Core change: `engine.events` (readonly `WorldEventBus`) exposed on the facade so the broadcast server can subscribe (`api.md` §1). | `mcp.md`, `api.md`, `architecture.md`, `boundaries.md`, `decisions.md`, `AGENTS.md` | | 2026-08-12 | **`crop.water` fixed to mirror the mechanism (MCP-surface only; `@farm/mcp`; surface unchanged at 85 tools / 22 resources / 10 prompts).** The debug tool previously *toggled* the plot's `watered` flag (true→false→true), which broke the MCP-driven farming loop (water → tick → water → tick… flipped watered back to false, so `waterRequired` stages never advanced). It now calls `PlantingSystem.water(x,y)` (the SET mechanism — same precedent as `crop.stage`→`GrowthSystem.forceStage` and `crop.till`/`crop.sow`/`crop.harvest`→`PlantingSystem`): result stays `{plotId, x, y, watered}` (always `true` after a successful water; watering an already-watered plot keeps it watered). Error paths follow `crop.till`'s conventions — shared `resolveCoordArgs` (`crop: provide plotId or x/y coordinates`, `crop: no plot found: `), system errors pass through (`planting: no plot at (x,y)`, `planting.water: tile (x,y) is not tilled`). No engine-core changes, no new tools/hooks/events. | `mcp.md`, `api.md` | | 2026-08-12 | **`crop.till` / `crop.sow` / `crop.harvest` debug tools added** (the core farming loop till → sow → water → harvest is now drivable through MCP; surface now 85 tools / 22 resources / 10 prompts). `crop.till {x?, y?, plotId?}` → `{plotId, x, y, tilled}` — wraps `PlantingSystem.till(x,y)` (idempotent: creates the plot entity if absent, re-tills otherwise); no coords/plotId → error result `crop: provide plotId or x/y coordinates`. `crop.sow {x?, y?, plotId?, seedItemId}` → `{plotId, x, y, cropItemId, seedItemId}` — wraps `PlantingSystem.sow(x,y,seedItemId)`: consumes 1 seed from `player.inventory` (emitting `item-use`), sets `cropItemId` to the crop **def** id resolved from the seed; missing/blank `seedItemId` → error result `crop.sow: seedItemId must be a non-empty string`; system errors pass through (`planting.sow: tile (x,y) is not tilled`, `planting.sow: no in inventory`, `planting: no plot at (x,y)`, …). `crop.harvest {x?, y?, plotId?}` → `{plotId, x, y, cropItemId, drops: [{itemId, qty}]}` — wraps `PlantingSystem.harvest(x,y)`: requires the final stage, rolls drops, grants them to `player.inventory` (emitting `item-acquire` per drop), emits `crop-harvest`, clears the crop fields (plot stays tilled); system errors pass through (`planting.harvest: plot has no crop`, `planting.harvest: crop on plot is not mature (stage N of M)`). All three accept `{x, y}` or `{plotId}` (identical coordinate/plotId validation to `crop.stage`/`crop.water`; plotId resolves via `plotIdFor`). Debug tools (D3) wrapping the PlantingSystem mechanism — no schema/content changes, no new hooks, no new events. | `mcp.md`, `api.md` | | 2026-08-12 | **`apps/mcp-server` entry parameterized + `examples/sample-game/` added** (transport section above). The stdio entry serves any content/mods pair via exported `resolveDirs({argv, env, cwd})` (CLI `--content-dir`/`--mods-dir` > env `FARM_CONTENT_DIR`/`FARM_MODS_DIR` > monorepo defaults; relative paths against cwd) and `main(dirs)` (auto-runs only under direct execution). `examples/sample-game/` ships complete game content + a mod exercising every namespace; `apps/mcp-server/test/sample-game.test.ts` proves it boots and plays through MCP. | `mcp.md`, `api.md`, `architecture.md` | | 2026-08-12 | **Tick loop + `animal.*` namespace implemented** (Tier 2 items 5 & 6 final; surface now 82 tools / 22 resources / 10 prompts). **Tick loop (D23)**: `engine.tick(dt)` is the headless core's explicit simulation driver (dt in ticks, 1 tick = 1 engine time unit, `dayLengthTicks` from the `time` def) — it advances `TimeSystem` (firing `day-start`/`season-change` boundaries as before) and then runs every system's `step(dt)` in registration order. `TimeSystem` gained `step(dt)` which emits the new **`time-tick`** bus event `{day, season, tickInDay}` after each advance; `GrowthSystem.step` accumulates day-fractions and advances crops/animals on day boundaries. New tool **`time.tick`** (`{ticks?}`, default 1) → the post-step time state `{day, season, tickInDay}`; it is semantically distinct from `time.advance` (clock-only, no systems step, no `time-tick`). Optional auto-tick: `createEngine({ tickIntervalMs, tickDeltaTicks? })` drives `tick` via a `node:timers` interval (default 1 tick per interval, `unref`'d, cleared on `engine.stop()`); OFF by default so tests stay deterministic. **`onSchedule` intra-day form**: `at: { tickInDay: n }` (n ∈ [0, dayLengthTicks)) added to the schedule-at union — evaluated on the `time-tick` event (schema requires at least one of day/season/tickInDay in the object form; the existing `"day-start"`/`"season-change"`/`{day?, season?}` forms are unchanged and still evaluated only on those boundary events). **`animal.*` namespace** — 9 tools: `animal.validate`/`create`/`update`/`list`/`get` (definition lifecycle against `animalSchema`, mirroring `crop.*` exactly — write `content/animals/.yaml`, `create` registers/overwrites, `update` removes then re-registers) + debug `animal.spawn` (`{animalId,x,y}` → `{id:"animal::", type:"animal", animalId, x, y, stageIndex:0, daysInStage:0, fed:false}`; unknown def → `growth: unknown animal def: `; bad coords → `growth: x and y must be non-negative integers`) + `animal.feed` (`{id}` → `{id, animalId, fed:true}`; unknown → `growth: unknown animal: `; emits `animal-fed` `{animalId, animalDefId}`) + `animal.stage` (`{id, stageIndex}` → `{id, animalId, animalDefId, stageIndex, stage}`; delegates to `GrowthSystem.forceAnimalStage`) + `animal.entities` (→ `{animals:[...]}`). Resources `animal://catalog` + `animal://{id}` (missing → MCP error `animal not found: `). Prompt `animal.create-prompt`. **`AnimalDef`** (content kind `animal`, `content/animals/.yaml`): `{ id, stages: [{name, days, feedRequired}], produces: [{itemId, qty, chance?}] }` — the growth cycle + produce table (data only); feed/produce rules are behavior-language content. GrowthSystem `step` advances **both** `type:"plot"` (CropDef, zero regression) and `type:"animal"` entities (AnimalDef): per day, a `feedRequired` stage gates on the entity's `fed` flag (mirroring `watered`); non-final stages accumulate `daysInStage` then advance `stageIndex` (resetting `daysInStage`/`fed`, emitting `animal-grow`); a mature (final-stage) animal emits `animal-produce` per eligible day and its `fed` resets. New hooks `onAnimalGrow`/`onAnimalProduce` wired in `engine.wireEvents`; `animal-fed` is bus-only (no hook — behavior-language-matchable). See `api.md` §4/§7 and `decisions.md` D23. | `mcp.md`, `api.md`, `decisions.md`, `boundaries.md`, `AGENTS.md` | | 2026-08-12 | Initial draft (pre-implementation) | `mcp.md`, `api.md`, `architecture.md`, `decisions.md`, `AGENTS.md` | | 2026-08-12 | **Walking skeleton implemented**: `server.*`/`map.*`/`time.*` namespaces live (7 tools, 3 resources, 1 prompt). `item/crop/shop/save/behavior/mod` remain spec-only. `server.enable_subsystem`/`disable_subsystem` deferred (gateway `enable`/`disable` exists, no tools yet). Resource contract: `uri` template + `read(params)` (see note above). | `mcp.md` | | 2026-08-12 | **Client-surface shapes confirmed by integration tests** (`apps/mcp-server/test/client-surface.test.ts`, 12 cases): success results omit `isError` (absent, not `false`); `map.list` returns `{maps:[...]}` (id/width/height/tilesetRef summaries); `map.get` returns the map def plus a `player` key (spawn entity, `null` if absent) and defaults to the first map when `mapId` omitted; `time.*` args validated (missing/negative `ticks` → error result). | `mcp.md` | | 2026-08-12 | **`item.*` namespace implemented** (creation-first, D3): `item.validate`/`item.create`/`item.update`/`item.list`/`item.get` + debug `item.query_inventory`/`item.give`; resources `item://catalog` + `item://{id}`; prompt `item.create-prompt`. Creation tools validate the JSON def against `itemSchema` (no write on failure), then write `content/items/.yaml` (YAML `stringify`) and register live directly against the registry — mirrors D13 hot-reload (`create` registers/overwrites; `update` removes then re-registers). Exact shapes: `item.list` → `{items:[{id,name,kind}]}`; `item.validate`/`item.create`/`item.update`/`item.get` → normalized `ItemDef` JSON or error result (`item not found: `); `item.query_inventory` → `{inventory:{itemId:qty}}`; `item.give` → `{itemId,qty,total}`; `item://catalog` → `{items:[defs...]}`; `item://{id}` → one def (missing id → MCP error). Duplicate `item.create` overwrites. Inventory lives on the player entity (`player.inventory`); `give`/`take` emit `item-acquire`/`item-use` bus events → hooks `onItemAcquire`/`onItemUse`. | `mcp.md`, `api.md` | | 2026-08-12 | **`crop.*` namespace implemented** (creation-first, D3): `crop.validate`/`crop.create`/`crop.update`/`crop.list`/`crop.get` + debug `crop.stage`/`crop.water`; resources `crop://catalog` + `crop://{id}`; prompt `crop.create-prompt`. Creation tools mirror `item.*` exactly: validate the JSON def against `cropSchema` (no write on failure), write `content/crops/.yaml` (YAML `stringify`), register live (`create` registers/overwrites; `update` removes then re-registers). Exact shapes: `crop.list` → `{crops:[{id,seedItem,stageCount}]}`; `crop.validate`/`crop.create`/`crop.update`/`crop.get` → normalized `CropDef` JSON or error result (`crop not found: `); `crop.stage` → `{plotId,x,y,cropItemId,stageIndex,stage}` (accepts `{x,y,stageIndex}` or `{plotId,stageIndex}`; delegates to `GrowthSystem.forceStage`, fires `onCropGrow`); `crop.water` → `{plotId,x,y,watered}` (accepts `{x,y}` or `{plotId}`; toggles the plot's `watered`); `crop://catalog` → `{crops:[defs...]}`; `crop://{id}` → one def (missing id → MCP error). `cropItemId` on a plot is the crop def id, not the seed item id. Growth stage advance itself is simulation (`GrowthSystem.step`), not an MCP tool — MCP only forces stages for debugging (D3). | `mcp.md`, `api.md` | | 2026-08-12 | **`shop.*` namespace implemented** (creation-first, D3): `shop.validate`/`shop.create`/`shop.update`/`shop.list`/`shop.get` + debug `shop.buy`/`shop.sell`; resources `shop://catalog` + `shop://{shopId}`; prompt `shop.create-prompt`. Creation tools mirror `item.*`/`crop.*` exactly: validate the JSON def against `shopSchema` (no write on failure), write `content/shops/.yaml` (YAML `stringify`), register live (`create` registers/overwrites; `update` removes then re-registers). Exact shapes: `shop.list` → `{shops:[{id,buyCount,sellCount}]}`; `shop.validate`/`shop.create`/`shop.update`/`shop.get` → normalized `ShopDef` JSON or error result (`shop not found: `); `shop.buy` → `{shopId,itemId,qty,total,gold}` or error result (unknown shop, item not sold, insufficient gold, non-positive qty); `shop.sell` → `{shopId,itemId,qty,total,gold}` or error result (unknown shop, item not bought, insufficient inventory); `shop://catalog` → `{shops:[defs...]}`; `shop://{shopId}` → one def (missing id → MCP error). Buy/sell delegate to `EconomySystem.buy`/`sell` (each fires `onTransaction` via the `transaction` bus event; buy also emits `item-acquire`, sell also emits `item-use`). Buy/sell convention: `sells` = shop sells to the player (player pays `sells[].price`), `buys` = shop buys from the player (player earns `buys[].price`). `stock` is optional and informational in this slice (no depletion/restock). | `mcp.md`, `api.md` | | 2026-08-12 | **`save.*` namespace implemented** (mechanism only, D14): `save.snapshot` (no args → full `WorldSnapshot` JSON `{format:"farm-save", version, savedAt, world: Entity[]}`); `save.validate` (`{snapshot}` → `{ok:true}` or `{ok:false, errors:[...]}` — always a normal result; validation failure is a report, not an error result); `save.restore` (`{snapshot}` → `{restored:N}` or error result — validates, rejects future versions, migrates to the current version via registered migrations, then replaces ALL world entities; malformed input → error result `invalid snapshot`); `save.migrate` (`{snapshot, fromVersion?, toVersion?}` → the migrated snapshot or error result — no-op when versions equal, throws on backwards migration, versions without a registered migration are identity steps, so the v1 empty registry returns the snapshot unchanged after validation; non-positive `fromVersion`/`toVersion` → error result); resource `save://state` (latest snapshot, live — reflects mutations); prompt `save.authoring-prompt` (save format, versioning, migration authoring). The snapshot covers world entities only; `TimeSystem` state is not serialized in v1. Save policy (when to save, slots, autosave, UI) is creator content/Mods (D14) — no policy tools. `save` has no content files (`content/saves/` does not exist). | `mcp.md`, `api.md` | | 2026-08-12 | **`behavior.*` namespace implemented** (v1 core: conditions + triggers, D15), 7 tools + 1 resource + 1 prompt. Creation tools mirror `item.*`/`crop.*`/`shop.*` exactly (tools named `trigger.*` per the contract, but the content kind is `behavior` — so files live in `content/behaviors/.yaml` and the list key is `triggers`): validate the JSON def against `triggerSchema` (no write on failure), write the YAML via `stringify`, register live (`create` registers/overwrites; `update` removes then re-registers). Exact shapes: `behavior.trigger.list` → `{triggers:[{id,actionCount}]}` (registration order); `behavior.trigger.validate`/`behavior.trigger.create`/`behavior.trigger.update`/`behavior.trigger.get` → normalized `TriggerDef` JSON or error result (`behavior.trigger.get: trigger not found: ` / `behavior.trigger.update: trigger not found: `); `behavior.fire` (`{triggerId}`) → `{triggerId, fired}` — `fired: true` when the condition evaluated true (actions ran, `trigger-fire` emitted, `onTriggerFire` hook fired), `fired: false` when the condition was false (no-op); unknown trigger → error result (`unknown trigger: `; non-empty-string validation also errors). `behavior.condition.validate` (`{condition}`) → `{ok:true}` or `{ok:false, errors:[...]}` — always a normal result (mirrors `save.validate`); the expression is validated alone against `conditionExprSchema`. Resource `behavior://triggers` → `{triggers:[defs...]}` (exact-URI, no template). Prompt `behavior.trigger-prompt` → guided authoring (condition grammar, action set, path conventions). No deviations from the `behavior.*` section contract above beyond the documented shapes; condition evaluation semantics (path resolution convention, `trigger-fire` loop guard, action behavior) are in `api.md` §4. | `mcp.md`, `api.md` | | 2026-08-12 | **`mod.*` namespace implemented** (introspection only, D10; last Tier 1 subsystem), 3 tools + 1 resource, no prompts. The engine facade gained a read-only mods surface (`api.md` §1): `engine.mods` (the loaded `ModStatus[]`, live) and `engine.modsDir` (`string | undefined`), plus `engine.reloadMods(id?)` behind `mod.reload`. Exact shapes: `mod.list` (no args) → `{mods:[ModStatus...]}` where each `ModStatus = {id, version, loaded, hooks: string[], errors?: string[]}` — `version` from the Mod's inline `manifest {id, version}` (Mods are code at `mods//index.mjs`; the loader does NOT read a `content/mods//manifest.yaml` or any `contentOverrides` field, so neither is surfaced — reported as-is rather than invented); `mod.status` (`{id}`) → one `ModStatus` JSON or error result (`mod not found: `; blank id → `mod.status: id must be a non-empty string`); `mod.reload` (`{id?}`) → `{reloaded: true, mods: ModStatus[]}` with fresh statuses (unknown id → error result `mod not found: `; blank id → `mod.reload: id must be a non-empty string when provided`); resource `mod://{id}` → one `ModStatus` (missing id → MCP gateway error `mod not found: `). **`mod.reload` semantics (v1)**: in-process re-import + re-bind, NOT a process restart — `ModLoader.reload(modsDir, id?)` unbinds that Mod's previously-registered hook functions (each `HookRegistry.on` returns an unsubscriber; the loader keeps per-mod unsubscriber records) and re-imports the Mod entry with a cache-busting `?t=-` query so changed `.mjs` files are actually re-read and re-bound without duplicate hook bindings. With no `id`, reload unbinds all recorded Mods and re-scans `modsDir` (newly added Mod dirs are picked up; deleted Mods are unbound). A Mod that was never loaded successfully in this process cannot be reloaded by id (no entry record → treat as unknown). D13's authoritative path remains unchanged: code Mod changes are picked up for real by restarting the headless core (`apps/mcp-server`); `mod.reload` is the in-process convenience for the dev loop. Errors during reload (bad import, bad manifest) surface as `loaded: false` + `errors` in the returned statuses and in `engine.mods`. Deviation from the draft contract: the `mod.*` section lists no authoring prompt and none was added (consistent with the contract — introspection only). | `mcp.md`, `api.md` | | 2026-08-12 | **Behavior language v2 implemented** (state machines + `onSchedule` time primitive, D15/D20; Tier 2 lead item), 7 tools + 1 resource + 1 prompt added to the `behavior.*` namespace (surface now 49 tools / 13 resources / 7 prompts). **Separate content kind `statemachine`** (`content/statemachines/.yaml`, `stateMachineSchema` in `schema/statemachine.ts`) — not a discriminated union inside `behavior` — so `behavior.trigger.*` stays intact. New tools: `behavior.statemachine.validate`/`create`/`update`/`list`/`get` (creation-first, mirror `trigger.*` exactly: validate against `stateMachineSchema` with cross-field checks — `initial`/`transitions[].from/to`/`onSchedule[].to` must reference declared states, duplicate state ids rejected, each transition needs at least one of `when`/`on`; no write on failure; write `content/statemachines/.yaml`; `create` registers/overwrites, `update` removes then re-registers) + debug `behavior.statemachine.status` (`{machineId}` → `{machineId, state, started}` — `started: false` with `state` = declared `initial` before the instance exists) + `behavior.statemachine.transition` (`{machineId, to}` → `{machineId, from, to, transitioned}` — force a transition regardless of declared conditions, fires onEnter/onExit and emits `state-change`; unknown machine/state → error result). Resource `behavior://statemachines` → `{statemachines:[defs...]}`. Prompt `behavior.statemachine-prompt` → guided authoring (states, transitions, schedule entries, quest-progress convention). **Design decisions (D20)**: (1) separate kind vs `behavior`-union — separate keeps trigger tool names truthful and content dirs 1:1; (2) `onSchedule` (schedule entries over the existing `day-start`/`season-change` bus events) instead of a tick loop — the engine has no auto-driven tick, so no new game loop; intra-day timing awaits a tick loop; (3) TriggerSystem extended in place (id stays `"trigger"`) so triggers and machines share one `match` pass and one re-entrancy guard; (4) machine state lives on a **world entity** `{id: "sm:", type: "statemachine", machineId, state}` (lazily created on first transition) so `save.*` snapshots it for free and MCP inspects it. Each transition emits `state-change` `{machineId, from, to}` → new hook `onStateChange`. | `mcp.md`, `api.md`, `boundaries.md`, `decisions.md` | | 2026-08-12 | **`dialogue.*` and `weather.*` namespaces specced** (MCP-first, D-op1; Tier 2 items 2 & 3) before implementation. `dialogue.*`: 7 tools (5 lifecycle + debug `start`/`choose`), 2 resources (`catalog` + `{id}`), 1 prompt (`create-prompt`). `weather.*`: 2 debug tools (`get`/`set`), 1 resource (`weather://state`), no prompts — single-file content kind makes creation tools meaningless. Design decisions (D21) recorded in the sections above: dialogue state on a world entity `dialogue:active`; linear `to` nodes surface a synthesized `Continue` choice; available choices are `when`-filtered and `choose` indexes into that list; terminal/all-gated nodes auto-end; weather state on a boot-created `weather` entity; injectable RNG; initial weather = current season's first table entry. | `mcp.md` | | 2026-08-12 | **`placement.*` and `npc.*` namespaces specced** (MCP-first, D-op1; Tier 2 items 4 & 5) before implementation. `placement.*`: 9 tools (5 building lifecycle + debug `place`/`remove`/`list`/`occupies`), 3 resources (`placement://buildings`, `placement://{id}`, `placement://placed`), 1 prompt (`create-prompt`). `npc.*`: 5 debug tools (`list`/`get`/`spawn`/`move`/`teleport`), 1 resource (`npc://{npcId}`), no prompts (runtime-entity namespace — no content kind). Design decisions (D22) recorded in the sections above: `BuildingDef` buildable as `"map"`/`{tiles}`/`"any"` union; buildings as world entities; buildings-block-buildings v1 occupancy; `placement-change` event + `onPlacementChange` hook; `MovementSystem` resolving `PlacementSystem` from the host; tile walkability as a "water" tile-id convention; buildings block NPCs but NPCs do not block each other (v1); no `onNpcMove` hook (the `npc-move` bus event is behavior-language-matchable). | `mcp.md` | | 2026-08-12 | **`placement.*` and `npc.*` namespaces implemented** (Tier 2 items 4 & 5; surface now 72 tools / 20 resources / 9 prompts). **placement.*** — 9 tools: `placement.building.validate`/`create`/`update`/`list`/`get` (creation-first, mirror `item.*` exactly against `buildingSchema` — footprint positive ints, `buildable` as `"map"`/`{tiles:[...]}`/`"any"` union with non-empty tile lists, required `blocksMovement` boolean; no write on failure; write `content/buildings/.yaml`; `create` registers/overwrites, `update` removes then re-registers) + debug `placement.place` (`{buildingId,x,y}` → placed entity `{id:"building:::", type:"building", buildingId, x, y, w, h, blocksMovement}` or error result: `placement: unknown building: `, `placement: footprint out of bounds at (x,y)`, `placement: tile (,) is not buildable for `, `placement: footprint overlaps existing building `) + `placement.remove` (`{x,y}` → removed entity or error `placement: no building at (x,y)`) + `placement.list` (→ `{buildings:[placed...]}`) + `placement.occupies` (`{x,y}` → building entity or `null`). Resources `placement://buildings` → `{buildings:[defs...]}` + `placement://{id}` → one def (missing → MCP error `building not found: `) + `placement://placed` → `{buildings:[placed...]}` (live). Prompt `placement.create-prompt` → guided building authoring. **npc.*** — 5 debug tools (D3): `npc.list` (→ `{npcs:[{id,x,y}]}`), `npc.get` (`{npcId}` → entity or error `npc not found: `), `npc.spawn` (`{id,x,y}` → created entity; rejects blank id / bad coords / duplicate id), `npc.move` (`{npcId,x,y}` → `{npcId, from, to}` or error result: `movement: unknown npc: ` / `movement: target out of bounds: (x,y)` / `movement: tile (x,y) is not walkable` / `movement: blocked by building at (x,y)`), `npc.teleport` (`{npcId,x,y}` → same shape, no bounds/walkability/blocking checks); resource `npc://{npcId}` → live entity (missing → MCP error). `npc.*` has **no prompts** (NPCs are runtime entities — no authored content kind). New content kind `building` (`content/buildings/`), new hook `onPlacementChange` wired to the `placement-change` bus event. Movement blocks on `blocksMovement: true` buildings read via `PlacementSystem.occupies`; tile walkability uses the "water" convention. | `mcp.md`, `api.md` | | 2026-08-12 | **`dialogue.*` and `weather.*` namespaces implemented** (Tier 2 items 2 & 3; surface now 58 tools / 16 resources / 8 prompts). **dialogue.*** — 7 tools: `dialogue.validate`/`create`/`update`/`list`/`get` (creation-first, mirror `item.*` exactly against `dialogueSchema` with cross-field checks — unique node ids, `startNode`/`to`/choice `to` must be declared nodes, `choices`+`to` mutually exclusive, non-empty choices; no write on failure; write `content/dialogues/.yaml`; `create` registers/overwrites, `update` removes then re-registers) + debug `dialogue.start` (`{dialogueId}` → `{dialogueId, nodeId, text, choices: [{text, to}]}` — choices are the `when`-filtered available list; unknown → error result `dialogue: unknown dialogue: `) + `dialogue.choose` (`{index}` → next node state; index into the available list; errors: `dialogue.choose: index must be a non-negative integer`, `dialogue: no active dialogue`, `dialogue: choice index out of range: `; a terminal/all-gated node ends the dialogue — text delivered in the result, `dialogue-end` emitted, active entity removed). Resources `dialogue://catalog` → `{dialogues:[defs...]}` + `dialogue://{id}` → one def (missing → MCP error). Prompt `dialogue.create-prompt` → guided authoring (nodes/choices/when/actions). **weather.*** — 2 debug tools (D3): `weather.get` (no args → `{weatherId, name?, season}`) + `weather.set` (`{weatherId}` → `{weatherId, name?, season, from?}` — `from` present only on change; unknown id → error result `weather: unknown weather id: `); resource `weather://state` → current state; **no prompts** (single-file content kind — no creation tools by design). Both systems' state persists on world entities (`dialogue:active`, `weather`), so `save.*` covers them. New hooks `onDialogueStart`/`onDialogueEnd`/`onWeatherChange` wired in `engine.wireEvents`. Deviation from the spec's parenthetical surface-count estimate (+1 dialogue resource; see the `dialogue.*` section — `catalog` + `{id}` follow the `item.*`/`crop.*`/`shop.*` pattern). | `mcp.md`, `api.md`, `boundaries.md`, `decisions.md` |