M2 core experiment. Server owns persona+knowledge (spoilers/IP, no DB);
client owns live state and computes available_moves. Free text straight
to the NPC prompt (no Adjudicator). Sibling NpcService + pure MoveValidator;
moves validated against state, invalid dropped, prose always kept (§6).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
M2 client HTTP loop: the client posts its canon log to /dm/narrate, shows
the prose, harvests [FACT] via TagExtractor, and degrades to an authored
fallback (§13) on any failure. First end-to-end vertical slice, live-proven
against qwen3.5 through Docker. 67/67 client tests, whole-branch review
MERGE AS-IS.
Package api/prompts into the Docker image and give the Dockerized proxy a
route to a host-run Ollama. M1's narrator pipeline was proven via venv-on-
host; these were the two Docker-path gaps the client HTTP loop surfaced.
After the prompts fix, /dm/narrate reached the model call and failed with
ConnectError: Connection refused — the container's localhost is not the
host, so the default OLLAMA_BASE_URL=http://localhost:11434 hit nothing.
Ollama runs on the host (M1's venv smoke reached it because uvicorn ran on
the host too; the container never had a route).
Add extra_hosts host.docker.internal:host-gateway so the container can
reach the host, and document using http://host.docker.internal:11434 in
.env.example. Verified a container reaches host Ollama's /api/tags (200).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The narrator pipeline (M1) reads role prompts from /app/prompts, but the
Dockerfile copied only api/app and docs/schemas — never api/prompts — so
/dm/narrate 500'd with FileNotFoundError on narrator.md in any Docker run.
The M1 live smoke passed only because it ran uvicorn from the local venv,
where ../prompts resolves on the host; the container path was never
exercised until the client HTTP loop drove a real request through compose.
Dockerfile now COPYs api/prompts; compose mounts it :ro so prompt edits
reload live like the app code (prompts are source code, §16). Verified by
building the image and confirming /app/prompts/narrator.md is present.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Whole-branch review Minor #1: JSON.parse_string pushes an engine-level
error on a non-JSON body (e.g. a gateway HTML 502), which the project's
gutconfig promotes to a false failure — the same reason FallbackLibrary
already avoids it. Consistency fix; a non-JSON body still degrades.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
First M2 vertical slice: CanonLog -> HTTP -> /dm/narrate -> prose ->
screen -> fact-harvest, with a §13 degraded-DM fallback on any failure.
Pure DmService core behind an injectable DmTransport seam (headless-
testable); throwaway harness scene; one authored fallback line under
client/content (renderer owns the file's home).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regroup the flat status list into milestones with a 'what this proves' goal, and
tag every item with its §2 side (state/text/both), dependencies, and milestone
goal. Sequencing calls locked: M3 leads with the playable dungeon
(Adjudicator→Combat→Luck) before the comedic roles (Improviser/Banter); streaming
deferred until the M2 dialogue experiment proves aliveness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Plan status → COMPLETE, all 8 tasks/44 steps checked. Roadmap: Narrator pipeline
✅, Client HTTP loop promoted to ▶ next.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- call_log._default_write: swallow any exception from the default file/
stdout sink and report to stderr, so a log-write failure (disk full,
bad permissions, misconfigured CALL_LOG_PATH) never turns a successful
narration into a 500 (charter §13). Injected write= sinks (used by
tests) are left to surface their own errors.
- ollama_client.chat: catch ValueError alongside httpx.HTTPError in the
retry loop so a 200 response with a non-JSON body (JSONDecodeError is
a ValueError subclass) counts as a failed attempt and falls through to
ModelError after the one retry, instead of escaping chat() uncaught
(charter §12 — one retry, then ModelError, nothing else escapes).
- main.py: update stale docstrings — /dm/narrate is now fully wired
(routing + model call + logging); the other four roles remain stubs.
Regression tests added for both fixes (TDD: watched RED, then GREEN).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
/dm/narrate now makes a real Ollama call (Task 7). The role-endpoint
loop tests in test_endpoints.py had no mock, so every default pytest
run was silently hitting a live Ollama instance on localhost:11434 —
breaking the hermetic-suite constraint. Add an autouse fixture that
stubs narrate.ollama_client.chat and narrate.call_log.record so all
five endpoints stay hermetic; assertions (200 on valid, 422 on
invalid) are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Client-side GDScript engine that constructs the canon log at new-game
(origin + world + character creation) and maintains its invariants turn
to turn. Headless GUT-tested logic layer (48 tests): typed RefCounted
model with invariants-in-the-model, Luck-centric GameState, ContentDB
mirroring the api id-resolver, NewGame.construct, and a pure §12 tag
extractor. Binds to the Plan A canon log contract.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Whole-branch review flagged that NewGame.construct validated class and
origin refs but not player.name. The canon-log schema requires
player.name with minLength:1, so a missing/blank name previously
returned ok:true and produced a log the API would reject with 422.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Also configure GUT to not treat push_error() as a test failure
(client/.gutconfig.json: failure_error_types = [engine, gut]).
The brief's LogPlayer/Quest setters intentionally push_error() on
rejected input; GUT's default failure_error_types includes
push_error, which would fail test_player_rejects_unknown_class and
test_quest_status_enum despite their assertions passing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The client half of the canon log contract: typed RefCounted model with
invariants-in-the-model, Luck-centric game state, new-game construction
from origin+world+creation, turn-to-turn mutators, and a pure regex tag
extractor. Headless GUT-tested logic layer; UI/HTTP/save-load deferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>