- pf2e parity: a unit test confirms the director carries the system through
(systemConstraint + prompt say Pathfinder 2e, no cross-system leak) and still
produces a grounded deterministic turn. (Degree-of-success/conditions already
branch per system via the shared kernel.)
- UX: the transcript pane is now a bounded, scrollable region (max-h-[60vh]) so
long sessions don't blow the page layout; it still auto-scrolls to the newest line.
- e2e: e2e/director.spec.ts exercises the full flow against a real browser — DM
narrates a grounded scene with no API key (deterministic badge, empty-state
clears, Continue appears), and the player persona seats a party character and
takes a turn.
396 unit tests + 2 director e2e green; lint clean; build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
For live co-GM play, the AI's narration now reaches the players' screens, and its
roll buttons already broadcast their results to the table (via rollAndShow →
broadcastGmRoll).
- When hosting a session (role gm + connected) and "Share narration with players"
is on, each director narration is pushed over the table chat channel (sendChat)
so players see the AI DM speak in their session feed. A no-op when not hosting.
- SceneControls gains the share toggle (default on); directorStore persists it.
Reuses the existing, e2e-tested realtime chat primitive — strictly additive, no
protocol/server change. GM stays authoritative. 395 tests green; lint clean;
build OK; page + controls verified rendering on a clean load.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Director sessions can run for hours; the context would grow without bound. Now a
fixed live window of recent turns is sent each turn, and older turns are folded
into a rolling per-session summary.
- planContext (pure): summary covers [0, summarizedThrough); the live window is
the rest. Once the window exceeds windowSize + SUMMARIZE_BATCH, the oldest turns
fold down to the last windowSize — never leaving a gap between summary and window.
- maybeSummarize (after each turn): folds the batch into AiSession.summary via a
separate complete() call when a key is set, else a deterministic compaction
(one compact line per turn, capped) — bounded with or without an LLM.
- The summary already rides in the system prompt (it never displaces the grounded
roster), so the closed-set anti-hallucination anchor is preserved as history scrolls.
Pure windowing + deterministic-summary covered by unit tests. 395 tests green;
lint clean; build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The director can now play a party character, not just DM. A Mode switch
(Dungeon Master / Player) plus an AI-seat picker choose which PC the AI pilots.
- Scene injection: when seated, the controlled PC's real sheet detail is fed to
the director — attacks with derived to-hit (sys.weaponAttack) + damage
expressions, spells (level/concentration), resources, and spell slots — so the
AI player acts from true numbers, in first person.
- Persona-aware prompt + cue already route DM→runs monsters / player→"it is your
turn"; the seat sets controlledName.
- Caster actions (castSpell/spendResource) flow through the D2 approve-each apply
bridge unchanged — slot/resource spend is enforced by the kernel, concentration
mirrors to the combatant.
- UI: SceneControls (mode + seat), seat-gating ("pick a character"), and
player-flavored buttons (Take turn). directorStore gains controlledCharacterId.
Verified in-app: switching to Player mode, seating "Lia the Brave · Fighter 3",
and taking a turn produced first-person narration. 388 unit tests green (incl. new
player-persona scene/ prompt tests); lint clean; build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The director's proposed state changes are now one-click "Apply" actions. Nothing
mutates a Character or Encounter until the human clicks Apply — the AI never
writes game state on its own.
- useDirectorAction (the 3rd anti-hallucination gate): re-resolves every action's
target against the live roster/encounter and rejects unknowns, then routes
through the pure kernel + combat engine — applyDamage/applyHealing/setTempHp/
updateCombatant (damage/heal/tempHp/condition), nextTurn (advanceTurn),
logEvent (log), and castSpell/spendResource for caster actions. Persists via the
transactional encountersRepo.mutate / charactersRepo.update.
- No-auto-roll preserved: damaging a concentrating creature SURFACES a
concentration save (concentrationDC) as a roll button — it is never auto-rolled.
Massive-damage death is flagged. castSpell mirrors new concentration onto the
combatant so later saves surface.
- ActionCard gains a working Apply button (idempotent: disables after applying,
shows the result/skip reason); applied actions append a system transcript entry
so the next turn sees the new ground truth.
8 integration tests against a real Dexie cover each action→kernel patch, the
concentration-save surfacing, and rejection of off-roster targets. 386 unit tests
green; lint clean; build OK; verified the page renders + runs a turn on a clean load.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The first user-visible slice of the AI DM / AI Player "director": a new
/director page where the AI narrates the scene, voices NPCs/monsters, and runs
enemies — grounded entirely in the campaign's real party and active encounter,
and degrading to a deterministic narrator when no AI key is set.
Engine (pure, framework-free) in src/lib/assistant/director/:
- schema.ts: DeepSeek-tolerant directorTurnSchema (z.preprocess structural
repair + z.coerce + .catch() enums + per-element safeParse-drop) → a single
validated turn { narration, rollRequests[], actions[], suggestions[] }.
- context.ts: buildDirectorScene assembles a CLOSED roster from the active
encounter (or the party) with deriveState badges — the only entities the
director may name.
- prompt.ts: persona-aware system prompt (DM/player) leading with the
systemConstraint; multi-turn message mapping from the transcript.
- engine.ts: runDirectorTurn + sanitizeTurn — the anti-hallucination gate that
drops any action referencing an off-roster entity (and ungrounded
addCombatant), mirroring the encounter advisor's candidate filter.
- fallback.ts: deterministic director that still surfaces roll buttons.
Hard rules, enforced:
- NEVER auto-roll: a roll fires only from RollRequestCard's onClick (rollAndShow).
A unit test asserts the engine layer never imports the roll seam, making
auto-roll structurally impossible.
- Approve-each: D1 is read-only (actions render as previews; the Apply bridge
lands in D2). The director never writes game state.
- Always-on fallback: works with no API key.
Also: Dexie v18 `aiSessions` table + aiSessionsRepo (+ cascade delete), a small
directorStore, a Settings card, and the /director route + nav entry.
Verified in-app on the sample 5e campaign: grounded narration named the real
current combatant; on an enemy's turn a "Aller Rosk attack vs Pip Underbough"
roll card appeared with diceRolls UNCHANGED until the button was clicked, which
then fired exactly one roll and recorded the result back into the transcript.
381 unit tests green; lint clean (3 pre-existing); build OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Layout:
- CombatantRow: action controls (damage/AC/move/remove) grouped into one
right-aligned wrapper so they wrap together instead of scattering on narrow
widths.
- top bar: the campaign switcher now shrinks/truncates instead of overflowing
on tablet widths.
- character sheet header: stat coins go full-width grid on mobile; the name
truncates and scales down on small screens.
UX consistency:
- player Cast and resource Spend/Regain no longer fail silently — Cast shows the
reason, resource −/+ disable at their bounds.
- the AI encounter builder now catches load failures and reports them; level-up
advisor shows the human error message, not the error-kind enum.
- section headers standardized on the .smallcaps design token across 16 files
(replacing an ad-hoc uppercase class).
- player HP bar uses the shared Meter primitive.
pf2e depth: condition-effects table gains drained, dazzled, enfeebled, fatigued.
FeatCard: dropped a dead text-xl wrapper left from the emoji purge.
Left as documented (functional, off-theme, refactor-risky): the native
confirm/prompt in Settings cloud-conflict and the session-password flow.
Gate: 293 unit + 35 e2e + 2 realtime; tsc + build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Crash fix:
- restoreBackup (file + cloud pull) now validates each row through its Zod
schema, backfilling new fields — an older backup no longer lands characters
missing feats/concentration/etc and crashing the sheet. + test.
pf2e parity (closing feature gaps vs 5e):
- +9 missing classes (Animist, Exemplar, Gunslinger, Inventor, Kineticist,
Magus, Psychic, Summoner, Thaumaturge) with data + class tips.
- the wizard now enriches pf2e classes with their caster ability, so pf2e
spellcasters get the Spells step + "Caster" tag like 5e.
- editable Perception rank and spellcasting proficiency on the pf2e sheet
(fixes wrong initiative / spell DC at higher levels); rank-10 spell slots.
- pf2e monster resistances/weaknesses/immunities now apply in combat (flat
amounts: resistance subtracts, weakness adds, 'all' matches any type). + tests.
Glyph purge (finishing the emoji→Lucide migration): replaced ~40 leftover
text-glyphs (✕ − + ✦ 🤫 ⬆ ⬇ ☑ ☐ ▶ ◀ ‹ › ★ ↻ ▸ ↑ ●) with Lucide icons across
Modal (every dialog), the sheet sections, dice, settings, player panels, world
pages, map editor, roll tray, and session UI.
Gate: 293 unit + e2e green; tsc + build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "buttons, not chatbot" core: the app recognizes a situation and offers the
fix; the user never has to formulate a request.
- src/lib/assistant/signals.ts: buildSignals() aggregates the existing advisor
detectors with new kernel-aware ones — quest-complete (all objectives done →
"Mark complete"), caster out-of-slots → "Short rest", concentrating-while-
bloodied warning — deduped and severity-sorted. 6 unit tests.
- extended SuggestAction with shortRest + completeQuest; one shared dispatcher
(useSignalAction) used by both surfaces so behaviour can't drift.
- SignalsBell: an ambient bell + popover in the top bar, visible from every
screen, with a count badge, one-click actions, and per-signal/clear-all
dismiss. AssistantPage refactored onto the same buildSignals + dispatcher.
- the AI cards were already actionable (NpcGen → npcsRepo, EncounterTip → add
to encounter, Assistant encounter builder → combat).
Also fixed a pre-existing invalid-HTML bug: ConditionPicker rendered a <Check>
SVG inside <option> (React hydration error spam) → plain "✓" text.
Build + 277 unit tests green; bell verified live (popover, actions, no errors).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DeepSeek is a reasoning model — its chain-of-thought counts against the
completion budget. The 1024 max_tokens default let it burn the whole budget
"thinking" and get cut off (finish_reason: length) with an empty content, which
read as a parse failure. Raise DEFAULT_MAX_TOKENS to 4096 so it can finish
reasoning AND emit the JSON. Also return a clear error on empty responses.
The spiral was triggered by a contradictory prompt: the fight was already
"deadly" yet the advisor still asked the model to "add creatures to reach
Deadly." Skip the LLM when the target tier isn't actually harder than the
current one and let the deterministic path ease the over-tuned fight instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "creatures" alias fix wraps the balance schema in z.preprocess, whose input
type is `unknown`. complete()'s `schema?: ZodType<T>` pinned input=output=T, so
T inferred as unknown and `res.data.add` failed under tsc -b (project build).
Loosen schema input to `ZodType<T, ZodTypeDef, any>` — T stays pinned to the
schema output, but coerce/preprocess/transform schemas (input ≠ T) are accepted.
Pin the advisor call's type param to BalanceSuggestion, matching the other
complete<...> call sites.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The encounter-balance schema was the only AI schema with a numeric field.
LLMs routinely emit numbers as JSON strings ("count": "2"), which z.number()
rejects, failing the whole safeParse and falling back to deterministic with a
"parse" error. The all-string NPC/session/quest schemas never hit this.
- Coerce monster counts (z.coerce.number) and widen the upper bound 8->12 so a
string or slightly over-eager count is accepted, not rejected. min(1) stays.
- Tell the model in the prompt that count is a plain integer and reasoning /
targetDifficulty are required.
- Surface the real failure message in the advisor instead of the opaque kind.
- Add a regression test that string counts coerce to numbers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Replace all emoji icons (~26 instances) with Lucide icons across the app
(DashboardPage, SessionSidebar, PlayerBoards, MyCharacterPanel,
PlayerViewPage, EncounterTracker, CampaignsPage, HandoutControl,
SessionControl, CharacterSheet, EncounterTipCard, ActionGuide)
- Expose real 5e race data in wizard (ASI/vision/traits instead of stub desc);
add vision field to loadRaces5e return type
- Surface subclass descriptions after picking a subclass in the wizard
- Add per-class tips, race synergy notes, and ability/skill guidance in wizard
- New ActionGuide component: collapsible "What can I do on my turn?" in player view
- New SessionPrepCard: AI + fallback session hook generator on assistant page
- New NpcGenCard: AI + fallback quick NPC generator with "Add to campaign" action
- Inline NPC detail generator (wand button) on each NPC card in NpcsPage
- Quest hook generator button in QuestsPage header
- Condition tooltips in player party view (useConditionGlossary)
- Pass campaign.system through session snapshot so player view is system-aware
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Player view: a Quest log now syncs to players (snapshot += quests; broadcaster
passes campaign quests; PlayerBoards renders titles + objective checkboxes).
- AI (DeepSeek etc.): robust JSON extraction — strip code fences anywhere + a
brace-balanced scanner that ignores prose/braces in strings. Fixes "AI unavailable
(parse)" with OpenAI-compatible providers. +4 tests.
- Encounter advisor: now bidirectional — for an over-tuned (too-hard) fight it
suggests REMOVING/swapping monsters instead of only ever adding. +3 tests.
- Character share: works on iPad — native share sheet → clipboard → a copyable
link modal fallback (was a silently-failing clipboard write).
230 unit + 34 e2e + 2 realtime green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Instead of proposing a fresh alternative encounter, the non-AI path now:
- adds MORE of the creatures already present ('Add 2 more Goblin'), computing the
exact count needed and accounting for the 5e encounter multiplier, or
- adds a thematically related stronger creature from the bestiary (shared name
token, level/CR-appropriate) when that reaches the target with fewer bodies.
Only builds a fresh group when the encounter is empty. Apply clones the existing
combatant (works for homebrew/custom too) or pulls from the pool.
Existing creatures are also offered to the AI as candidates so it can likewise
say 'add another goblin'.
New suggestReinforcements() + baseName() in encounter.ts with unit tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Level-up build-route advisor (src/features/characters/sheet/LevelUpAdvisor.tsx +
useLevelUpAdvisor): ~4 system-aware routes plus a custom one; choosing a route
expands it into concrete next-level steps. AI when configured, deterministic
fallback (src/lib/assistant/levelup.ts) otherwise. Embedded in the existing
HP-only LevelUpModal, which stays primary.
- Level-up prompts/schemas added to prompts.ts (buildRoutes/buildSteps), each
leading with the system constraint + class + next level.
- Campaign Insights section on the Assistant page renders deterministic themes,
with an optional 'ask the assistant to expand' affordance when AI is on.
levelup + prompt unit tests (4 routes both systems, system vocabulary, custom
route); e2e for the deterministic route→steps flow and the insights section.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- src/lib/assistant/prompts.ts: balanceSuggestionSchema + buildBalancePrompt
(system message leads with the grounding constraint; restricts choices to the
provided compendium candidates).
- useEncounterAdvisor hook: builds context → picks system-correct candidates →
asks the LLM (when configured) for a structured pick, validates + filters names
to the candidate set, else falls back to the deterministic buildSuggestedEncounter.
Apply adds the creatures transactionally via encountersRepo.mutate.
- EncounterTipCard in the combat tracker: leads with the detected difficulty
tendency (or the current difficulty), offers a one-click grounded suggestion
(AI or deterministic) with propose→confirm Apply.
prompts unit tests + e2e for both the deterministic apply flow and the AI path
(stubbed provider); the AI test also confirms candidate-grounding (only
shortlisted creatures survive).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Deterministic advisors (src/lib/assistant): party resources (downed/bloodied/
spent slots/rest nudges), session prep (dangling [[wiki links]], active quests,
empty journal), live combat hints (turn, downed combatants) — pure + tested
- Data-driven encounter builder: greedily assembles level/CR-appropriate monsters
to a target difficulty via the Phase-3 budget; one click builds + opens combat
- Assistant page: suggestion cards with one-click actions (goto, long rest,
create note); wired into routes, dashboard, command palette
- 8 advisor/builder unit tests + assistant e2e
Conversational LLM layer intentionally deferred (no provider/key in this env);
the deterministic advisor is the offline default per the plan.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>