Validates the three finalists by clone, build, test run and live local Ollama inference, then answers the fork question with measurements rather than static review. Recommendation: fork AI-DnD, confidence high. The Phase 0A call holds, but it was wrong that AI-DnD's undo is non-destructive — retry preserves the replaced take, undo hard-deletes it. A follow-up spike fixed that in 3 files (+130/-31): undo now moves a head cursor, redo round-trips, writing below a moved-back head forks and keeps the abandoned line, branch-scoped memory isolation survives, suite 627/632 with all 5 failures asserting the deleted-row behaviour that was replaced. Findings that change the plan: - AI-DnD cannot take a turn air-gapped as shipped; tiktoken fetches its encoding from a CDN. Proven on an internal Docker network, proven fixed by vendoring the file. - ai-adventure needs zero code for Ollama — two config lines — and its turn/head/checkpoint schema is the target model to build to. - Open Dungeon has zero automated tests and a positional summary watermark, making its branch retrofit larger than Phase 0A costed. - The world-state referee takes relative deltas; a 3B model sent absolute values under full context, so a wounded player ended at full health. Validation cannot catch this, so prefer ai-adventure's typed-event vocabulary when generalising narrative state. - Export/import recomputes head depth, so a round-trip silently undoes an undo. Must be fixed alongside the undo work. Docs only; no production code. Working tree from the runs stays untracked under phase0b/. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015gUPLuxLs8wypxZPEmccJu
28 KiB
Phase 0B — Local Validation Findings and Recommendation
Date: 2026-09-01 Status: Complete, and updated after the follow-up undo/redo spike. Stop condition reached; no production work started. Method: Clean clones, pinned SHAs, documented installs, full test runs, real local Ollama inference, and targeted disposable experiments.
Update — the recommended spike was run and it passed. Section E previously recommended proving non-destructive Undo/Redo in AI-DnD before committing to the fork. That spike is now complete: 3 files, +130/-31 lines, undo deletes zero rows, redo round-trips exactly, writing below a moved-back head forks and preserves the abandoned line, branch-scoped memory isolation survives, and the suite goes 627/632 with all 5 failures asserting the deleted-row behavior that was deliberately replaced. Section E has been rewritten accordingly, and the open questions it resolved are marked in section F. Full detail:
reports/PHASE-0B-UNDO-SPIKE.md.Update 2 — four follow-up checks run. The world-state referee was exercised against local models for the first time, and export/import, story-card lineage safety and Postgres removability were settled. One new defect found (a round-trip silently undoes an undo) and one design conclusion that changes what we should build (§H). Detail:
reports/PHASE-0B-FOLLOWUP-CHECKS.md.
Pinned commits (cloned 2026-09-01):
| Repo | SHA | Last commit |
|---|---|---|
| AI-DnD | d72f7c1bda0f34fccd84afb7a25c34eb01c901de |
2026-08-31 |
| Open Dungeon | b0a79f96bf852be7b4e53908dff6a7f7c179da23 |
2026-07-06 |
| ai-adventure | 873ea9180d5b611576cddb155921fc16a17ae88b |
2026-07-21 |
Inference used for every live test: Ollama in Docker, qwen2.5:0.5b and
qwen2.5:3b-instruct for narration, nomic-embed-text for embeddings.
CPU only, no GPU.
A. Executive Recommendation
Fork AI-DnD as the production base. Confidence: high.
The Phase 0A recommendation held up, but for partly different reasons than it gave, and it was wrong about one important thing.
What held: AI-DnD really does implement the expensive correctness work, and it survived every entanglement test Phase 0A worried about. The RPG machinery is optional, the scripting sandbox is removable, and the branch-scoped memory isolation — the single hardest requirement in the specification — works correctly against real local embeddings.
What did not hold: Phase 0A's claim that AI-DnD's undo is non-destructive is
false. POST /undo hard-deletes rows. Retry is non-destructive; undo is not.
There is no Redo. This is a direct conflict with DECISIONS/005-branch-preserving-history.md
and specification §4.
That discovery does not change the decision, because the comparison is relative: AI-DnD needs one bounded change at a chokepoint that every read already passes through. Open Dungeon needs the entire non-destructive history subsystem built from nothing, under a 3,991-line component, with zero automated tests.
That "one bounded change" is no longer an estimate. The follow-up spike implemented it in 3 files and +130/-31 lines, and it works: undo deletes nothing, redo restores exactly, the abandoned tail becomes an ordinary branch, and memory isolation holds. Confidence in the fork decision moves from "high on the evidence" to "high and demonstrated".
The decision gate from reports/PRELIMINARY-RECOMMENDATION.md said to select
AI-DnD unless a blocker is confirmed. All four were tested and none is a blocker:
| Phase 0A blocker | Result |
|---|---|
| Branching/state inseparable from RPG mechanics | No. A scenario-less adventure with empty world state played, branched, and retrieved memories correctly. |
| Removing hosted/scripting destabilizes many tests | No. 19 of 632 (3%), and every one fails for instrumentation reasons, not coupling. |
| Local-only still needs external services | Partly — but both are packaging fixes. tiktoken downloads its encoding from a Microsoft CDN; the SPA fetches Google Fonts. |
| Dependency/security burden worse than expected | No. 80 packages total vs Open Dungeon's 335 with 6 high-severity advisories. |
B. What We Learned About Each Candidate
AI-DnD — recommended base
What worked
632 passedin 236s, first try, no fixes needed.- Docker image builds clean;
docker composepath is real. - Default model endpoint is already
http://localhost:11434/v1. Connection test and model discovery against Ollama worked with no code change. - Streamed a full multi-turn story over SSE against local Ollama.
- Genre-neutrality is real. An adventure created with
scenario_id: nullgets an empty world state and no RPG scaffolding, and plays normally. Noir prose, no fantasy code paths, no stats. - Retry is genuinely non-destructive. Retrying kept the replaced attempt as a
sibling take at the same coordinate (rows 6 and 7 both present; both listed by
/variants). - Forking is genuinely non-destructive. Adding an alternate take at a passed
turn created branch 2 while every row of branch 1 survived, with
parent_branch_idandfork_depthrecorded. - Branch-scoped memory isolation is correct. Two memories were written on
branch 1 (depths 5 and 11) from real
nomic-embed-textembeddings. On branch 2, retrieval returnedused: []and none ofbrass key,revolver,lockbox,ashtrayorbulletsappeared anywhere in the assembled prompt. The negative control passed: switching back to branch 1 returned both memories with similarity scores (0.8087) and the abandoned facts reappeared. Retrieval is working and correctly scoped. - Prompt transparency is already there. The context endpoint returns labeled
sections with token costs:
narrator 57, ai_instructions 9, persona 15, plot_essentials 24, history 988, used_memories 143, length_hint 46.
What failed
- Undo hard-deletes. Measured: rows 7 → 4, deleting ids 5, 6 and 7 — that
is, both the player action, the live AI take, and the alternate take the
earlier retry had preserved.
delete_turnalso callsmemorybank.forget_node. A retry's preserved alternative does not survive a later undo. There is no Redo endpoint. Resolved in the follow-up spike — see section G. - Fails offline on a clean install. First turn in a network-isolated container
died with
NameResolutionErrorforopenaipublic.blob.core.windows.net/encodings/cl100k_base.tiktoken.tiktokenfetches its BPE encoding on first use. Seeding the 1.7 MB file into the image made a full turn generate with no internet at all. Packaging fix, not architecture — but a hard blocker until fixed, and static review missed it. - Runtime remote asset. The built SPA still requests Google Fonts; the CSP
explicitly allows
fonts.googleapis.comandfonts.gstatic.com.
Strongest reusable pieces
Immutable parent-linked story tree with lineage entries; alternate takes; branch
fork/switch/rename/delete; per-node world-state and script-state snapshots;
branch-scoped memories with local embeddings; the Insights prompt snapshot;
ai-dnd-adventure-v2 whole-tree export; the 632-test suite; SSE streaming.
Major architectural problems
Destructive undo and no redo. No named checkpoints (branch names are the nearest
thing). Multi-user/hosted concerns are spread across ten modules. netguard.py
is the inverse of what we need — it blocks non-public endpoints in hosted mode
and does nothing locally; we want a warning on non-loopback endpoints.
Adaptation required
Bounded and mostly subtractive. The one genuinely additive change is non-destructive undo/redo, now measured at 3 files and +130/-31 lines (section G).
Open Dungeon — not recommended as base
What worked
Installed and ran on first try. Plays a real story against Ollama through its
"custom" OpenAI-compatible provider. Clean browser UX. No runtime Google Fonts
(next/font self-hosts at build time). Local SQLite, local image generation,
character portrait continuity.
What failed
- Every history operation is destructive, confirmed by measurement.
- Edit is an in-place
UPDATE messages SET content = ?. After editing, the original textbrass key from the ashtraywas gone from every column of every table. - Retry and Erase both call
DELETE /messages/{id}?after=1, which runsDELETE FROM messages WHERE chat_id = ? AND (created_at > ? OR ...). Measured rows 5 → 4, with no archive anywhere. - The database has exactly four tables —
chats,messages,characters,app_settings. No parent pointer, no branch, no take, no checkpoint.
- Edit is an in-place
- Zero automated tests. No
testscript, and no test framework indevDependenciesat all. Phase 0A listed this as "BUILD/VERIFY"; it is zero. - The local provider hard-codes a five-model Gemma 4 allowlist
(
src/lib/text-models.ts). Arbitrary local Ollama models are only reachable through the "custom" OpenAI-compatible path. - Next.js telemetry is on by default and printed its notice on first start.
- 335 npm packages, 6 high-severity advisories.
Cost of the branch retrofit — the Experiment B answer
Larger than Phase 0A estimated, because of a dependency Phase 0A did not identify:
messagesneedsparent_id,branch_id,depth, and a live/take flag.- Of 20 exported
db.tsfunctions, the four history ones need rewriting, andgetChatmust stop returning a flat list and become a lineage query. story-prompt.tswindows history by array index (messages.slice(dropped),messages.length - 1). All of it becomes an ancestry walk.- The rolling summary watermark is positional.
chats.story_summary_countmeans "how many of the chat's oldest messages the summary already covers", androute.tscomparesevicted.length > stored.coveredCount. On a branch, "the first N messages" has no meaning. Summaries must be re-anchored to a turn id and made per-lineage. Phase 0A did not identify this. page.tsxis a single 3,991-line component holding retry/erase/edit as array slicing, and there is no take stepper, branch panel, or tree view to extend.- All of the above lands with no test suite underneath it.
Worth reusing as reference: the reading UI, inline scene images, the character-portrait-as-reference-image continuity idea, and the scene/media architecture. None of it ports directly — Open Dungeon is Next.js and AI-DnD is React + FastAPI.
ai-adventure — not the base, but promote it to the architectural reference
What worked — this is the strongest result of the round
Ran 76 tests ... OKin 1.9s.- The Ollama "adapter" is zero code.
LMStudioBackendis a generic OpenAI-compatible client hitting/v1/chat/completionsand/v1/models. I changed two lines ofworld.toml—base_urland modelname— and the doctor reported[PASS] LM Studio reachable: http://127.0.0.1:11434and[PASS] Configured model visible: qwen2.5:3b-instruct. A real turn then committed to the database withstatus='committed', zero validation errors, and amodel_callsrow recording backend, model and attempt. Phase 0A's "MODIFY — LM Studio primary provider" was wrong; it is a config change. - Its history model is exactly what the specification asks for. Verified
independently, not just by trusting its tests:
- Undo deleted 0 rows, rolled
has_brass_keyback fromFalsetoTrue, and the abandoned turn was still in the database. - Branching from the restored point produced
gate_openon the branch and did not leak it into the original session. - Checkpoint restore rolled state back with the later turn still present.
- Total turns in the database never decreased.
- Undo deleted 0 rows, rolled
- Schema is the target data model:
turns.parent_turn_id,status, append-onlystate_events,state_cachekeyed byhead_turn_id,model_callswith request/response hashes,named_checkpoints,summariesanchored bythrough_turn_id, and lore in FTS5. - The cleanest privacy posture by a wide margin. Exactly one outbound call
site in the entire codebase (
lm_studio.py'surlopen), no hardcoded remote host anywhere, and one third-party dependency (pydantic). It is the only candidate that already warns whenmodel.base_urlis not loopback — which is specification §12, implemented.
What failed
- With
qwen2.5:0.5bthe turn failed validation and nothing was saved. That is the model-proposes/app-validates discipline working correctly, but it means the typed-event contract needs a reasonably capable model; the freeform-prose candidates tolerate a weak one.qwen2.5:3b-instructsucceeded. - Product distance is real and large: terminal only, worlds authored as hand-written TOML and Markdown files rather than set up in-app, no streaming, no embeddings/semantic memory, no media, no import of user documents.
Adaptation required: building essentially the entire product on top of a correct core.
C. Key Technical Findings
| Requirement | AI-DnD | Open Dungeon | ai-adventure |
|---|---|---|---|
| History/undo model | Tree + takes; undo deletes, no redo (both fixed in the spike, §G) | Flat list; retry/edit/erase all destroy | Parent-linked, head pointer, undo deletes nothing |
| State rollback | Per-node snapshots, verified | None | Event replay, verified |
| Memory isolation across branches | Verified correct, with negative control | N/A (no branches) | Verified correct |
| Local Ollama | Default endpoint; verified streaming | Verified via "custom" provider; local provider allowlists 5 Gemma builds | Verified, zero code change |
| Offline | Blocked by tiktoken CDN fetch until vendored; then fully offline | Runtime clean; build needs network; Next telemetry on by default | Verifiably clean — 1 outbound call site, 1 dependency |
| Browser suitability | React SPA + FastAPI, already there | Next.js, already there | None |
| Prompt inspection | Per-action snapshot with labeled sections and token costs | Not exposed | Prompt hashes; store_prompts off by default |
| Imported knowledge | Story cards (keyword-triggered) — closest starting point | None | Local FTS5 lore, deterministic |
| Named checkpoints | Absent (branch names only) | Absent | Present |
| Future media | Absent | Present and working | Absent |
| Tests | 632 | 0 | 76 |
D. Important Surprises
Ordered by how much they should change the plan.
- AI-DnD's undo is destructive, and it destroys retry's preserved
alternatives too.
reports/PRELIMINARY-RECOMMENDATION.mdandREUSE-MATRIX.mdboth credit AI-DnD with non-destructive rollback. Retry earns that; undo does not. Measured: undo deleted the player action, the live take, and the sibling take a prior retry had kept. The follow-up spike has since replaced it with a head-cursor undo and added Redo (§G), so this is a corrected planning assumption rather than an outstanding defect. - AI-DnD cannot take a single turn on an air-gapped machine as shipped.
tiktokenfetchescl100k_basefrom a Microsoft CDN on first use. Proven by running it on a Docker--internalnetwork, and proven fixed by seeding the file. - ai-adventure needs no Ollama adapter at all. Two config lines. The "LM Studio" name is a misnomer for a generic OpenAI-compatible client.
- Open Dungeon has zero automated tests — not few, none, and no framework installed.
- Open Dungeon's summary watermark is positional, which adds a summary redesign to its branch retrofit that Phase 0A did not cost.
- Open Dungeon's "local" provider only offers five Gemma 4 QAT builds. A genre-agnostic app that lets the user pick any installed Ollama model needs that lifted.
- AI-DnD fetches Google Fonts at runtime and its CSP is written to allow it, violating specification §12's "no remote fonts".
- AI-DnD's
netguardis the opposite of our requirement — it protects a hosted deployment from SSRF and is inert locally. ai-adventure has the loopback warning we actually want. - Repo maturity differs sharply: AI-DnD is 172 commits / 3 authors, Open Dungeon 45 / 1, ai-adventure 20 / 1. All three are young. Since we fork, this matters less than it would for a dependency, but there is no upstream to lean on.
E. Recommendation for Next Step
Fork AI-DnD and begin the controlled strip-down. The blocking uncertainty is gone: the spike this section previously asked for has been run and passed (section G).
Recommended order, cheapest and most load-bearing first:
- Close the two offline blockers. Vendor the
tiktokenencoding and self-host the three font families, then re-run the air-gapped test. Until this is done the application does not satisfy specification §2, and it is a day's work. - Strip the hosted surface. Remove multi-user/auth, the demo key, analytics,
render.yaml, the Postgres path, and the quickjs sandbox. Re-instrument the ~19 tests that use JS hooks to observe state, moving them onto theworld_state_aftersnapshots that already exist. - Land the undo/redo work properly, promoting the spike from a proof to a
feature: fix the four rough edges it left (undo floor at the opening node,
retry/
add_takewhile behind the head, marking abandoned turns disposable, and the frontend Redo control and branch labelling) — and addheadDepthto the export bundle in the same change, or a round-trip silently undoes every undo (§H). - Add named checkpoints, using ai-adventure's
named_checkpoints(session_id, turn_id, name)as the model. With an explicit head that can sit anywhere, a checkpoint is now just a named head position. - Add the loopback-endpoint warning, modeled on ai-adventure's
_endpoint_warnings. - Then imported knowledge, and only after that, media.
Treat ai-adventure as the written specification for state and history semantics throughout. Its schema is the target shape and its 76 tests are the behavioral contract worth copying — the spike already reproduced its central property (undo that moves a head instead of deleting) and benefited from having that reference to aim at. §H extends this: when generalizing AI-DnD's world state into genre-neutral narrative state, take ai-adventure's typed-event vocabulary rather than AI-DnD's relative-delta vocabulary, because the delta protocol has a failure mode that validation cannot catch and that worsens as context grows.
One addition to the ordering above: whatever narrative-state engine we build must be exercised at realistic context length against the models users will actually run, not only in a clean harness. That is where the referee's weakness showed up, and it would not have appeared in a unit test.
Do not merge repositories. Open Dungeon remains a UX and media reference only.
F. Open Questions
Resolved by the undo/redo spike (§G):
Does cappingNo.Path.clauseby head depth break the memory and summary cursors?test_memory_settling,test_memory_nodes,test_memory_retrieval,test_memory_rewriteandtest_history_windowall passed unchanged, and memory retrieval was verified end-to-end against real embeddings with a control.How should named checkpoints sit on top of takes and branches?Largely answered. The spike introduces a head that can sit behind the tip, so a checkpoint becomes a named(branch_id, depth)position. Still needs a decision on whether restoring one forks immediately or on first write — the spike chose "on first write" for undo, and consistency argues for the same.
Resolved by the follow-up checks (§H):
What is the model capability floor?Characterized, not a blocker. The delta protocol is within a 3B model's reach in a clean prompt (3/3 correct) but degraded to absolute values under the full application context.0.5bcannot do it at all. See §H for the design consequence.CanYes. Three places, no blockers: one branch inpsycopg/Postgres be dropped cleanly?database.py, two migration helpers, one import inside analytics.Do story cards become imported knowledge, or is a new table needed?A new table.StoryCardcarries nobranch_id/depth, so it is not lineage-scoped and inherits neither branch nor undo isolation.Does the export format survive the history rework?No — and this is now a known defect to fix, not a question. See §H.
Still open:
- When does abandoned history get cleaned up? Nothing marks it disposable yet, and undone turns now accumulate rather than being deleted. This is the intended trade, but it needs a retention story.
- Is Open Dungeon's image path portable at all? It is Next.js calling a local FLUX worker over HTTP. The worker protocol may be reusable even though none of the UI is.
- What is the real model recommendation for users? The probe establishes a floor and a failure mode, not a ranking. A proper capability matrix across the models a user would actually run belongs in the build phase, measured at realistic context length.
- Not tested in any round: import/export round-trip behavior beyond the head-depth defect found by reading, multi-hour long-story context behavior, or any concurrency beyond the single-turn lock.
G. Follow-Up Spike: Non-Destructive Undo/Redo — Passed
Run after the main round, on a disposable copy of AI-DnD d72f7c1b. Full detail
in reports/PHASE-0B-UNDO-SPIKE.md.
The change is three files, +130 / -31 lines, roughly a fifth of it comments:
| File | Change | What |
|---|---|---|
context/lineage.py |
+26 / -3 | Read an uncapped lineage entry as capped at self.tip (the head), in clause and contains; add uncapped(). |
routers/adventures/takes.py |
+74 / -27 | undo_turn moves head_depth instead of deleting; new redo_turn. |
routers/adventures/turns.py |
+30 / -1 | fork_if_behind_head calls the existing tree.branch_at when a write lands below the head. |
It is this small because the pieces already existed: every read funnels through
lineage.path_of(), adventures.head_depth was already a stored head, and
tree.branch_at already created a branch that leaves a path at a depth without
touching the line it leaves.
Results against the success criteria:
| Criterion | Result |
|---|---|
| Undo deletes zero rows | Pass. 6 → 6 rows across three undos; separately, 11 consecutive undos on a 23-action story with 23 actions still present. Clears the "at least five, unlimited preferred" requirement. |
| Abandoned tail stays reachable as a branch | Pass. Undo-then-write forked branch 2 at fork depth 3; branch 1 kept all six of its rows, live and readable, and the branch switcher reaches it. |
| Redo restores | Pass. Three redos returned the head from -1 to 5 and the transcript to all six actions, exactly. |
| Branch-scoped memory isolation holds | Pass, with control. An embedded memory at depth 5 was retrieved at the tip (similarity 0.689), returned used: [] once the head moved to depth 3, and became eligible again on redo — without being deleted. None of seven terms unique to the hidden turns appeared in the assembled prompt. |
| Suite stays green apart from delete-behavior tests | Pass. 627 passed, 5 failed (0.8%). All five assert deleted rows or pruned memories. The state assertions inside those same tests still pass — assert adv.script_state == {"gold": 0} succeeds in both test_state_revert failures, and the fork-boundary guard in test_undo_stops_at_the_fork is untouched. |
Rough edges left for the real implementation (none architectural): undo can
walk past a user-written opening to an empty transcript; retry and add_take
were not routed through the fork check; abandoned turns are not yet marked
disposable; and the frontend has no Redo control.
What this changes about the recommendation: nothing about the choice, and the one thing about the plan — the strip-down can start now rather than after another investigation.
H. Follow-Up Checks — Referee, Export, Cards, Postgres
Run after the spike. Detail in reports/PHASE-0B-FOLLOWUP-CHECKS.md.
The finding that changes what we build
AI-DnD's world-state referee asks the model for relative deltas
(new = old + delta, then clamped). Playing the seeded RPG scenario with
qwen2.5:3b-instruct, the model sent absolute values — {"player.hp": 92}
meaning "hp is now 92". The engine read +92, clamped to +35, then to the
ceiling of 100:
player.hp 100 -> 100 "did not move. It is already at its maximum of 100"
The player took an arrow to the shoulder and finished the turn at full health.
The referee did its job — nothing was corrupted, and it fed corrective text back.
But no validator can catch this, because +92 is a legal proposal. The
application stayed authoritative while the authoritative state silently stopped
tracking the narration. That is the exact divergence specification §2 exists to
prevent.
An isolated probe using AI-DnD's own EMIT_RULE and parser shows the protocol is
not beyond a small model — qwen2.5:3b-instruct and qwen2.5:7b-instruct
both got 3/3 correct relative deltas, and qwen2.5:0.5b emitted no block at all.
The difference is context load: in a short prompt the 3B model follows the rule,
and under the full application prompt it drifts to absolutes.
Consequence for the design: when we generalize AI-DnD's world state into
genre-neutral narrative state, take ai-adventure's typed-event vocabulary
(set_flag key=… value=… — explicit and absolute) rather than AI-DnD's delta
vocabulary. The delta protocol has a failure mode that is invisible to validation
and worsens with context length; the event protocol cannot express the mistake.
This is the second time in this round that ai-adventure's core has turned out to
be the right specification to build to.
One new defect
Export/import silently redoes an undone story. bundle.py:_point_the_head
recomputes head_depth = max(depths) on import, and the bundle format carries
headBranch but not the head depth. That was correct when undo deleted rows; with
the spike's non-destructive undo, a round-trip restores every abandoned turn. Fix
is one optional headDepth field plus honouring it on import, falling back to
max(depths) so existing v2 files still load. It must land with the undo work,
not after it.
Two smaller answers
- Story cards are not lineage-scoped.
StoryCardhas nobranch_idordepth, unlikeMemory. Harmless today because cards are authored rather than derived — but any imported-knowledge source that can be created from story content must carry those coordinates and be read throughlineage.Path.clause, or it inherits neither branch nor undo isolation. Argues for a new table rather than extendingstory_cards. - Postgres is cleanly removable. One branch in
database.py, two migration helpers, one import inside analytics.psycopg[binary]then drops out.
Supporting Evidence
reports/PHASE-0B-BASELINE.md— installs, test runs, ports, storage, versions.reports/PHASE-0B-AI-DND-EXPERIMENT.md— branch/undo/retry measurements, memory isolation with negative control, scripting removal.reports/PHASE-0B-OPEN-DUNGEON-HISTORY.md— destructive-history proof and retrofit cost map.reports/PHASE-0B-AI-ADVENTURE-OLLAMA.md— zero-change Ollama run and the fixture checks.reports/PHASE-0B-OFFLINE-NETWORK.md— offline runs and egress inventory.reports/PHASE-0B-UNDO-SPIKE.md— the non-destructive undo/redo spike: the diff, the measurements, and the rough edges it left.reports/PHASE-0B-FOLLOWUP-CHECKS.md— the world-state referee under local models, the export/import head defect, story-card lineage safety, Postgres.
Working tree, scripts and databases from both rounds are under phase0b/
(untracked scratch, safe to delete). The spike itself is phase0b/spike/, a
disposable copy of the AI-DnD backend — it is a proof, not a branch to merge.