Commit Graph
69 Commits
Author SHA1 Message Date
John SmithandClaude 4e8477d08b feat(gateway): admin bootstrap allowlist, fail-closed secret, lobby membership gate (iteration 12)
Signup accepts only player/spectator — staff roles come from the
VTT_ADMIN_EMAILS allowlist (rotatable without restart). Gateway aborts
startup without AUTH_SECRET/VTT_ENGINE_SECRET, mirroring engine semantics.
GET /lobbies/{id} member-or-staff only with no existence oracle; invite
codes stay visible to members. 20 tests red-first; pytest 672 passed.

Co-Authored-By: Claude <[email protected]>
2026-08-24 21:14:10 -04:00
John SmithandClaude fa18ebfff8 feat(gateway,client): auth + spend buckets on agent/dynasty/simulation/LLM routes (iteration 10)
Closes audit A1 blockers F1-F4: /agent/turn gated to GM or bound-session
participants; dynasty inject-lore and simulation tick GM-only; empirical-
benchmark GM-only with 5/min bucket; intent/orchestrator/narrative routes
authenticated in a 30/min llm bucket. Client stream/dynasty call sites send
credentials with honest sign-in gating. Compose now passes overridable
VITE_YSYNC_WS_URL to vtt-client. 33 red-first pytest tests, 6 vitest;
pytest 651 passed / 22 skipped, vitest 102.

Co-Authored-By: Claude <[email protected]>
2026-08-24 20:48:36 -04:00
John SmithandClaude b7bb40b160 feat(gateway): durability-bridge auth + header-first tokens on legacy routes (iteration 8)
persist/hydrate require auth with x-card-style participation gating
(gm/admin any session, others via bound lobby roster, unbound fail closed).
All remaining token:Query routes migrated to _require_auth (Bearer preferred);
EnginePersistRequest.owner_user_id spoofing removed — ownership from verified
token. Sweep documented remaining unauthenticated LLM-spend/agent routes for a
coordinated follow-up. 11 new tests red-first; pytest 618 passed / 22 skipped.

Co-Authored-By: Claude <[email protected]>
2026-08-24 19:26:51 -04:00
John SmithandClaude f6ee77c8fc feat(gateway): epistemic ladder + x-card/spotlight authorization (iteration 5)
Lore assertions enter at SUBJECTIVE_RUMOR; GM promotes one step per call
through paradox review, players never self-promote (403 LORE_TIER_FORBIDDEN).
X-card requires token + session participation (403 X_CARD_NOT_A_PARTICIPANT);
spotlight recording validates speaker identity (403 SPOTLIGHT_SPOOFED_SPEAKER).
29 new tests red-first; pytest 607 passed / 22 skipped.

Co-Authored-By: Claude <[email protected]>
2026-08-24 19:04:53 -04:00
John SmithandClaude c1fc3dbebb feat(gateway,client): mandatory auth on every engine proxy, client sends tokens everywhere (iteration 3)
All narrative-facing /engine/* proxies resolve callers via _require_auth and
forward verified identity (_caller_actor); optional-token service-principal
fallback removed. 28-route anonymous-401 contract test matrix. Client now
attaches ?token=/Bearer on session/attack/check/map/metrics/presence with an
honest NOT_AUTHENTICATED rejection when signed out. Full benchmark run green:
MCR 100%, HCI 1.0, AFPR 0.0%, recall 100%, trust probes 10/10.

Co-Authored-By: Claude <[email protected]>
2026-08-24 18:46:58 -04:00
John SmithandClaude 3bf115ea0e feat(gateway): strict typed spawn schema blocks smuggled combat math (iteration 2)
EngineSpawnEntity mirrors vtt-core EntityState field-for-field with
extra=forbid; malformed/smuggled spawn bodies now 422 instead of
reaching the engine. Deploy-path class attack derivation collapsed
into one disclosed helper (_derive_class_attack) pending a Rust-side
class-aware EntitySpec follow-up. 11 new tests, red-first.

Co-Authored-By: Claude <[email protected]>
2026-08-24 18:15:24 -04:00
John SmithandClaude c5d9cc6b37 fix(gateway): server-side auth for quest routes (iteration 94)
Closes the close-out audit's top known-limit: quest generation
replaces the shared active_campaign_quest global every player's
journal reads, yet took no token — any unauthenticated caller could
rewrite the table's quest. Generate now requires GM/admin (403
QUEST_GENERATION_GM_ONLY, reject before mutation); active-quest read
and Concordia negotiation require authentication with no role gate.
7 red-first tests; suite at 558.

Co-Authored-By: Claude <[email protected]>
2026-08-24 16:34:20 -04:00
John SmithandClaude d9c3973838 feat(gateway): GM campaign autosave from live engine state (iteration 85)
POST /api/v1/campaign/autosave: fail-closed order — role check before
any touch (403 AUTOSAVE_GM_ONLY), identity-forwarded engine fetch
(502 honest when down), verbatim snapshot persisted through the
existing save path owned by the caller. Repeated autosaves upsert one
rolling per-session slot instead of growing rows. 4 red-first tests;
suite at 551.

Co-Authored-By: Claude <[email protected]>
2026-08-24 15:40:12 -04:00
John SmithandClaude 703f5c1e00 fix: sixth-audit remediation — gate reliability + honesty corrections (iteration 84)
- Gate reliability: autouse conftest fixture clears the shared auth
  rate-limit windows around every test; full suite now 3x-consecutive
  green (was nondeterministically red via cross-module bucket pressure).
- Test quality over claim-laundering: computedHP gains the real SRD
  hit-die table (Wizard/Sorcerer d6, Barbarian d12, per-die average
  growth) — the old flat d8 fallback had been pinned by a test falsely
  citing "hit-die max"; tests now cite SRD values explicitly.
- Scripted-mode social phase force-pins llm_gateway=None so an
  LLM-gatewayed persona can't leak upstream calls the report claims
  never happened; no-network test extended with a recording gateway.
- Dynasty test tautology replaced with real lineage assertions.
- Ambience-honesty test derives expected ids from AudioManager recipes
  (source of truth) with a drift guard, not the copied list.
- Tracker honesty note appended to iteration 79: default quest output
  changed materially; only quest_id and summary preserved.

Gates: pytest 547x3 green, vitest 69 green, build 13.3s.

Co-Authored-By: Claude <[email protected]>
2026-08-24 15:32:44 -04:00
John SmithandClaude 1a36cb6113 feat(gateway): role-enforced handout persistence (iteration 83)
Turns iteration 31's demo-data finding into a real backend: handouts
table via the idempotent DDL pattern, full CRUD under /api/v1/handouts
with visibility enforced server-side from the HMAC-verified role —
players/spectators see only all+party rows in SQL and process
(defense-in-depth), gm_only content 404s indistinguishably from
nonexistent for unauthorized callers (no existence oracle), updates/
deletes creator-or-GM. Typed client store mirrors lobby conventions.
8 red-first tests; suite at 547. Shared auth rate-limit bucket noted
as a future test-isolation follow-up.

Co-Authored-By: Claude <[email protected]>
2026-08-24 14:32:40 -04:00
John SmithandClaude dac7f36d77 feat(simulation): multi-generation dynasty lineages, alliances, prestige (iteration 82)
Pillar 8 depth: extend_lineage compounds trait inheritance across
generations (0.65 base + 0.08/ancestor, capped 0.95) with atavism for
ancestral traits; form_alliance marries houses with dominance-weighted
trait pools (Iron Will 0.70 → Cursed Bloodline 0.15); deterministic
house_prestige from traits, pacts, feuds, and unbroken-bloodline depth;
lore injection emits generation-appropriate canon (young bloodline as
PROPOSED_FACT vs N-generation UNBROKEN as VALIDATED_CANON). Legacy API
and seed output pinned unchanged; probabilistic gates asserted exactly
via ScriptedRandom. 12 red-first tests; suite at 539.

Co-Authored-By: Claude <[email protected]>
2026-08-24 14:18:22 -04:00
John SmithandClaude e51bb2b8c0 feat(simulation): parametrized quest generation from curated theme tables (iteration 79)
The quest engine built one hardcoded intrigue DAG regardless of theme
args — rng instantiated but never used. Now: three curated theme tables
(court/crypt/wilderness with a new TRAVEL_HAZARD node type) drive node
types, titles, prompts, skills, and factions; party level scales DCs
and rewards; length plans 8/10/12-node DAGs; seed drives all selection
deterministically. Unknown themes raise instead of silently
substituting; thin tables truncate with an explicit coverage_note
rather than padding with invented filler. Legacy call signature and
default output preserved. 20 red-first tests; suite at 527.

Co-Authored-By: Claude <[email protected]>
2026-08-24 13:59:24 -04:00
John SmithandClaude 7a104a7ebf feat(simulation): Concordia social-dialogue phase in campaign simulation (iteration 80)
Pillar 5 depth: between combat rounds, players attempt one dialogue
against a designated social NPC — LLM decisions via the same strict-
JSON gateway path with stance-branched scripted fallback, replies from
injectable ConcordiaNPC personas (registry=None skips the phase
entirely, never faked), approaches mapped onto disposition kinds so
repeated hostility visibly shifts stances across rounds. Norms are
absolute in both directions: violating utterances never reach the NPC,
violating replies degrade to template — both surfaced as norm_rejected.
Deterministic sim-clock timestamps; 11 red-first tests; suite at 526.

Co-Authored-By: Claude <[email protected]>
2026-08-24 13:58:52 -04:00
John SmithandClaude 7271475539 fix(engine+client): fifth-audit remediation across six findings (iteration 78)
- F1 MED: Help promise burned by LOS/cover-refused attacks —
  consumption moved after all fallible gates; refused attacks leave it
  standing (red-first socket test).
- F2 MED: explicit under-level cast requests now 422 INVALID_SLOT_LEVEL
  instead of silent .max() upgrade (gateway default aligned to 0 =
  unspecified); absurd damage formulas (>2x caps) hard-rejected without
  spending slots, moderate overshoot still gently clamped — validation
  hoisted before expenditure.
- F3 MED: monster spawns require a GM seat (403 MONSTER_SPAWN_
  FORBIDDEN for non-GM unowned NPC spawns; owned-player deploys via the
  service principal unaffected); encounter-builder copy matches reality.
- F4/F5/F6 LOW: viewport fog-parity claim corrected to the actual union
  semantics; ability-score defaulting warns instead of silently
  defaulting; .expect() calls in core replaced with error propagation;
  offhand index 0 refused as matching the main hand.

All gates green: workspace tests, clippy -D warnings, pytest 496, tsc.

Co-Authored-By: Claude <[email protected]>
2026-08-24 13:46:36 -04:00
John SmithandClaude 96676b499f feat(compendium): conservative spell-damage extraction pipeline (iteration 77)
Closes iteration 76's disclosed zero-damage gap at the data layer:
regex extractor over canonical SRD phrasing with plausibility gates
(die-face whitelist, 13-type damage vocabulary) and warn-not-guess
paths for multi-formula, player-chosen-type, and modifier-derived
formulas. Deterministic CLI regenerates an enriched fixture (73/352
spells enriched ~20.7%; 192 genuinely have no damage; the variable-
formula pool of 58 is the largest recoverable set). Original fixture
untouched; positional pairing documented where meta entries share ids.
20 red-first tests; suite at 496.

Co-Authored-By: Claude <[email protected]>
2026-08-24 12:58:51 -04:00
John SmithandClaude d62cc5dade feat(core+engine): two-weapon fighting and Help action (iteration 74)
Completes the SRD standard-action suite. Off-hand attack reuses the
normal hit math with only the damage expression swapped, positive
ability mod withheld per SRD (negative kept), Light property modeled
explicitly (opt-in on stat blocks; undeclared weapons refused, never
silently permitted), and a new spend_bonus_action enforcement point —
plus SRD ordering (Attack action must already be spent). Help stores a
consumable advantage promise ON THE BENEFICIARY, consumed exactly once
by same-side attackers through both attack routes, cleared on refresh,
and rewound with Action refund. Ledger + gateway proxies + client
helpers throughout; seed never client-forwardable. 18 red-first tests;
228 workspace / 476 pytest green, clippy -D clean.

Co-Authored-By: Claude <[email protected]>
2026-08-24 12:30:47 -04:00
John SmithandClaude 511c6271a4 feat+fix: Ready action end-to-end; deploy crash + clamp disclosure (iterations 71-72)
Two workstreams sharing server.py, landed together:

Ready action (71): ReadiedAction on EntityState with Action spend via
the single enforcement point, READY_ACTION_SET ledger event, cleared on
round refresh and rewind; engine route + gateway proxy + CharacterSheet
"Holding:" display with description input. GM adjudication by design —
no automatic trigger matching. 6 red-first tests across layers.

Fourth-audit remediation (72): MED-HIGH deploy crash — Roll20 imports
persisting speed:None crashed float(None) → 500 on every deploy of such
characters; guarded fallback to 30 with math.isfinite checks and the WS
movement budget consuming the same value. LOW: out-of-range import
levels now disclose their clamp instead of silently snapping. 8
red-first tests.

Full suite: 470 passed / 22 skipped; clippy -D warnings clean.

Co-Authored-By: Claude <[email protected]>
2026-08-24 08:55:52 -04:00
John SmithandClaude 133d15b1ad feat: combat maneuvers reachable end-to-end (iteration 71)
All six engine action routes (grapple/shove/dodge/dash/disengage/
stabilize) gain gateway proxies with strict extra=forbid bodies and
identity forwarding — notably the deterministic seed is deliberately
NOT forwardable, so clients can't pin their own rolls. CharacterSheet
gains a Combat Maneuvers panel: target picker from the projected
roster, skill/effect toggles, self-buttons, downed-ally stabilize list
with an honest hint when player-role projection hides ally HP; results
quote engine fields and rejection codes verbatim with zero optimistic
mutation. 10 new proxy tests; suite at 459.

Co-Authored-By: Claude <[email protected]>
2026-08-24 08:32:15 -04:00
John SmithandClaude de7d011175 fix(llm+sla): empty-stream honesty; SLA rows disclose what they measure (iteration 70)
Three small audit notes closed red-first:
- Empty/[DONE]-only SSE reassembly no longer masquerades as a
  completed turn: complete_with_tools raises LLM_UPSTREAM_EMPTY and the
  tool agent surfaces that status instead of COMPLETED with blank
  narration.
- The SLA 'intent_parsing' row is relabelled intent_parsing_keyword
  (it measures regex classification); an LLM-classification row appears
  only under RUN_LIVE_LLM=1 with a real key, counting only genuine llm
  outcomes; SSE first-token requires >=3 samples or WITHHELD.
- Report ends with explicit FAILED/PASSED/WITHHELD tallies and an
  all-withheld run exits nonzero — withheld can never read green.
21 red-first tests; suite at 451.

Co-Authored-By: Claude <[email protected]>
2026-08-24 08:25:37 -04:00
John SmithandClaude 682228cc68 fix(import): Roll20 honesty seam between importer and gateway (iteration 67)
Third-audit web finding: two individually-clean modules broke at the
handoff — unparsable speed strings passed through unwarned and the
gateway fabricated speed=30; missing class/race/background/alignment
silently became "fighter/Human/Soldier" persisted as fact under a
docstring claiming every substitution was pre-warned. Now: unparsable
speed warns and persists None (never invented gameplay data); identity
fields persist neutral-empty with per-field warnings naming exactly
what was absent; missing level defaults to 1 WITH a warning; the
missing-core dedup no longer lets garbage-value warnings suppress
missing-stat notices. 7 red-first tests.

Co-Authored-By: Claude <[email protected]>
2026-08-24 07:58:16 -04:00
John SmithandClaude c3b3f78535 fix: third-audit remediation — gate integrity, wizard export, clipboard honesty (iteration 65)
- P0: pytest was RED in any clean environment — dead skip guards fired
  only after ConnectError had already escaped. Both helpers now skip
  properly when the engine is down; suite green with no live engine.
- HIGH: starter-adventure export required ?token= while the wizard sent
  an Authorization header — every click was a guaranteed 422 under a
  feature claiming to work. Route migrated to _require_auth
  (header-first); tests pin header-auth success and 401 semantics.
- MED: clipboard 'Copied!' claimed success unconditionally; now awaits
  the write and shows an honest Ctrl+C fallback on rejection or missing
  Clipboard API.

Co-Authored-By: Claude <[email protected]>
2026-08-24 07:50:36 -04:00
John SmithandClaude 1409e3b2de feat(playtest): SLA measurement harness — honest latency tables (iteration 62)
GOALS.md's latency targets were removed from the docs as unverifiable;
now they're measured. measure_slas.sh drives 200 real network calls
per category against live services and prints p50/p95/max vs targets
with a documented +5ms transport allowance: rules 0.39ms p50 (≤10ms),
spatial LoS/path 0.35/0.41ms (≤15ms), intent parsing 1.10ms (≤150ms),
SSE first-token 1.86ms (≤1200ms) — all PASS with wide margin.
Unreachable services WITHHOLD rows rather than simulating; rate-limit
429s surface as visible exclusions; sub-30-sample categories withhold
rather than judge rump samples.

Co-Authored-By: Claude <[email protected]>
2026-08-24 07:09:21 -04:00
John SmithandClaude bfe327380c fix(llm): SSE-tolerant non-streaming calls for streaming-only proxies (iteration 56)
The live suite proved llm.heretek.one answers text/event-stream even
to non-streaming requests, silently breaking complete_json /
generate_narrative / complete_with_tools via resp.json() failures. All
three now detect SSE content-type and reassemble assistant messages
from delta frames (tool_calls merged by index), feeding the unchanged
JSON extraction; JSONL records carry an honest transport marker;
malformed/empty streams degrade without guessing. Narrative blank-
stream degrades with degraded:true rather than empty-but-healthy.
11 red-first tests; suite at 436.

Co-Authored-By: Claude <[email protected]>
2026-08-24 06:27:49 -04:00
John SmithandClaude 30850a5e59 test(llm): opt-in live-LLM suite that never runs in CI (iteration 55)
pytest 'live' marker + conftest auto-skip unless LLM_KEY is set AND
RUN_LIVE_LLM=1 — structurally CI-proof. Six tests exercise the REAL
endpoint: mechanical/safety classification provenance, Concordia
generator-marker-vs-reality (a template marker while the endpoint is
alive FAILS), strict-JSON parsing, soft 60s latency budget, and a gate
proving non-mock configuration. Connection failures degrade to SKIP.

First live run surfaced two real integration bugs, reported honestly
as skips rather than fabricated passes: .env named a nonexistent model
that hangs instead of 404ing (fixed in .env), and the endpoint returns
text/event-stream even for non-streaming calls which breaks resp.json()
(fix iteration pending). Default suite unchanged: 406 passed / 8 skips.

Co-Authored-By: Claude <[email protected]>
2026-08-24 06:24:12 -04:00
John SmithandClaude fdca883144 feat(simulation): autonomous AI companion PCs with tactical roles (iteration 54)
GOALS.md Pillar 1's drop-in AI party members: tank / skirmisher /
healer roles execute coherent turns through the same authenticated
proxy path as campaign_sim (no second HTTP stack), with optional LLM
targeting refinement validated against known entities and per-turn
provenance accounting (decision_source, fallback_reason, degraded).
Honest realizability matrix: ally-heal triage from a plain player seat
is REFUSED rather than fabricated because the role projection redacts
other PCs' HP — the companion falls back to tank behavior instead.
19 red-first tests; suite at 425.

Co-Authored-By: Claude <[email protected]>
2026-08-24 06:22:25 -04:00
John SmithandClaude db0f8a137e feat(gateway): Roll20 import endpoint with owned persistence (iteration 51)
POST /api/v1/import/roll20 accepts single or campaign exports (2 MiB
bound), maps through the importer onto the existing character storage
path so imports are owned by the caller and owner-scoped everywhere;
importer ValueErrors map to 422 with the fail-loud reason, all-
malformed campaigns refuse rather than fake a 200-with-zero. Foundry
preview route is a deliberate documented 501 — directory trees need
multipart support, pinned by test so it can't regress into fake
success. 7 red-first tests; suite at 406.

Co-Authored-By: Claude <[email protected]>
2026-08-24 06:07:20 -04:00
John SmithandClaude 24ead361b2 feat(compendium): Roll20 character importer (iteration 50)
Pillar 10's second interop path, grounded in researched sources
(Roll20 API object docs, real exporter payload, 5e OGL sheet attribute
index): single-character and campaign-envelope imports, explicit-mod
preference with disagreement warnings, score-from-lone-mod derivation
always warned, honest None+warning for missing core stats, unmapped
attribute accounting, first-wins duplicates, fail-loud ValueErrors,
deterministic sorted output. 26 red-first tests; suite at 399.

Co-Authored-By: Claude <[email protected]>
2026-08-24 05:54:59 -04:00
John SmithandClaude fb42d4367b fix(gateway): bounded rate windows, capped NPC interaction magnitude (iteration 48)
Two minor audit notes closed: the gateway's sliding-window store now
sweeps stale keys past a 100k cap (mirroring the Rust twin's guard,
same sweep-on-overflow shape, injectable for tests), and NPC
interaction magnitude is bounded to (0,10] matching the disposition
engine's band-saturation scale so one call can no longer pin a stance
to an extreme. 5 new tests; suite at 373.

Co-Authored-By: Claude <[email protected]>
2026-08-24 05:50:02 -04:00
John SmithandClaude 7f592fe7a8 fix(gateway): audit remediation — session-state auth, ledger redaction, IDOR (iteration 44)
Second independent audit findings, fixed red-first:
- HIGH: /engine/session-state required no token and returned FULL
  engine state verbatim via a service-principal fallback. The route
  now requires valid auth (401 otherwise) and no code path returns
  anonymous full state.
- MED-HIGH: ledger events bypassed role projection, leaking HP/damage
  numbers for hidden entities while replay export redacted them. Both
  now share one projection policy (fail-closed summaries for
  spectators/unknown roles).
- MED: GET /characters/{id} had an IDOR — any authenticated user could
  read any character; now owner-scoped with 404 for foreign/garbage ids
  (no existence oracle).
Six remediation tests verified failing before each fix.

Co-Authored-By: Claude <[email protected]>
2026-08-24 05:32:46 -04:00
John SmithandClaude 6db24190e1 feat(compendium): Foundry module importer; tracker through iteration 41
Pillar 10 interop grounded in live schema research (foundryvtt.com
manifest docs, foundry-vtt-types, NDJSON export format): module.json +
packs parsing onto AetherTable canon via explicit per-entry mapping
tables, unmapped fields accounted per entry rather than dropped or
guessed, LevelDB-format pack dirs detected and skipped, fail-loud
ValueErrors on manifest problems, deterministic sorted output.
Roll20 guarded out of scope pending its own fixture corpus. 25
red-first tests; suite at 363.

Co-Authored-By: Claude <[email protected]>
2026-08-24 05:01:41 -04:00
John SmithandClaude 35de8764a9 feat(gateway): Concordia NPC dialogue endpoint with starter personas (iteration 38)
Pillar 5 sub-agents become reachable: GET /api/v1/npc/ lists public
persona metadata; POST /npc/{id}/respond returns norms-gated
{reply, generator, stance} (taboo-violating LLM text degrades to the
template fallback with an honest norm_rejected marker); POST
/npc/{id}/interactions shifts persistent stances through the shared
disposition engine. Three personas bound to starter-adventure lore
(Drowned Steward, Baron Vane, Cabal Emissary) with distinct goals,
norms, and voices on a deterministic shared clock. 13 red-first tests;
suite at 338.

Co-Authored-By: Claude <[email protected]>
2026-08-24 04:49:44 -04:00
John SmithandClaude e65dee3e28 feat: combat lifecycle + role-projected session state (iterations 36-37)
Two features sharing server.py, landed together:

Initiative order end-to-end (5.6): vtt-core begin/end_combat roll
d20+DEX initiative with deterministic seeded order (total desc, DEX
desc, name asc), COMBAT_BEGAN/ENDED ledger events; engine routes
/sessions/{id}/combat/{begin,end} with RBAC; gateway proxies forward
caller identity; the HUD renders the real rolled order with an honest
"Combat not started" empty state and GM controls. 7 core unit tests +
4 integration + mocked proxy tests.

Role-projected session state (spectator follow-up): session-state and
session-replay now project by caller role — GM/admin see everything,
players see own entities in full plus others as public board-token
projections, spectators get visible-only projections with HP/damage
amounts stripped from replay summaries; service-principal reads stay
verbatim; unknown roles fail closed to spectator view. is_player/
is_dead kept in projections because campaign_sim's targeting depends
on them through this endpoint. 20 red-first tests.

Suite at 325 passed.

Co-Authored-By: Claude <[email protected]>
2026-08-24 04:42:39 -04:00
John SmithandClaude 6d6e914f23 feat(agents): Concordia entity-component NPC sub-agents (iteration 34)
GOALS.md Pillar 5's dedicated NPC sub-agents: explicit Memory (bounded
episodic, relevance+recency recall), Goals (priority/satisfaction/
feasibility walk with survival fallback), Social Norms (taboo +
obligation rules), and Linguistic Style components composing a persona.
respond_to() builds structured prompts from all four components plus
live disposition stance; LLM replies that violate norms are discarded
entirely for the deterministic stance/style-shaped fallback — violating
text never reaches output. Deterministic without an LLM; honest
generator markers. 17 red-first tests; suite at 296.

Co-Authored-By: Claude <[email protected]>
2026-08-24 03:52:00 -04:00
John SmithandClaude b9539dc8c3 feat(lore): Qdrant-backed compendium RAG with honest provenance (iteration 32)
Multi-tenant retrieval per GOALS.md Pillar 7: compendium spells/
monsters/magic items indexed into the qdrant_rules collection named
and dimensioned by the repo's own collections config, payload-filter
namespacing (category + system_id + tags), deterministic UUID5 point
ids, keyword indexes on filter fields — all over raw httpx REST so the
transport stays trivially mockable. Embedding is a documented
lexical-hash embedder (MD5-bucketed, L2-normalized), not semantic;
real models are a single-function swap. lore-lookup gains semantic=true
with provenance retrieval: qdrant | substring | substring_fallback,
zero network when disabled, one honest warning when unreachable,
read-path degradation only (canon-write strictness unchanged).
29 red-first tests; suite at 279.

Co-Authored-By: Claude <[email protected]>
2026-08-24 03:41:46 -04:00
John SmithandClaude 2517c0991e feat(compendium): Sunken Crypt of Karas starter adventure (iteration 30)
GOALS.md Pillar 2's first out-of-the-box module: three encounters
referencing ONLY real srd_5_2_monsters stat blocks (unknown ids raise
at definition time, so invented stats cannot ship), deterministic
seeded layout resolved via the engine's live WFC with a documented
in-module fallback and provenance recorded in the artifact, crypt-theme
loot name references, lore seeds reusing exact canon node ids, X-card-
safe content note. Ships as a real .vttbundle through the packager
(new backward-compatible adventure.json member) behind GET/POST
/api/v1/adventures/starter routes. 12 red-first tests; suite at 250.

Co-Authored-By: Claude <[email protected]>
2026-08-24 03:19:53 -04:00
John SmithandClaude 2e973769c4 feat(gateway+client): header-based auth migration; Neo4j epistemic graph (iteration 28)
Two features sharing server.py, landed together:

Auth headers: new client auth_headers.ts migrates lobby/character/
campaign REST calls to Authorization Bearer headers (five POSTs
previously sent NO credential at all); gateway gains a _require_auth
dependency reading header-first with ?token= fallback on migrated
routes (missing token is now 401, not 422). WS query tokens unchanged.
7 new tests.

Neo4j lore: Neo4jEpistemicGraph speaks to the dockerized neo4j:5 HTTP
transactional endpoint over httpx (zero new deps), same public surface
as the in-memory manager, all three paradox checks in one tx round
trip, tier weights matching canon, startup-only selection with honest
in-memory fallback logging, request-time failures raise rather than
silently splitting canon, predicates validated UPPER_SNAKE_CASE before
Cypher assembly. 24 tests incl. in-memory parity cases.

Co-Authored-By: Claude <[email protected]>
2026-08-24 03:03:33 -04:00
John SmithandClaude 9a75612782 feat(gateway): session replay export as portable JSON (iteration 26)
GET /api/v1/engine/session-replay projects the live engine ledger into
a downloadable artifact: events reduced to {sequence_id, actor_id,
event_type, is_reverted, summary} with human-readable summaries derived
only from fields actually present — partial payloads render what
exists, unknown event types pass through verbatim, reverted events are
flagged not dropped. Authenticated via forwarded caller identity;
Content-Disposition attachment naming falls back to -unknown.json when
no combat round exists. 15 red-first tests.

Co-Authored-By: Claude <[email protected]>
2026-08-24 02:45:37 -04:00
John SmithandClaude b4baa54546 feat(simulation): social-state telemetry in campaign simulation (iteration 24)
NpcDispositionEngine wired into CampaignSimulation: accepted attacks
record 'attacked' on the target's disposition toward the attacker
(rejected actions and non-attacks record nothing), decision prompts and
scripted-policy inputs receive each visible target's current stance
toward the acting player, and reports carry per-player end-of-run
stances plus interaction totals. All disposition timestamps use an
explicit sim clock — deterministic across identical runs; the engine's
clock is pinned so wall-clock can never fire. 'aided' hook deliberately
absent (no heal action reachable through proxies) and documented.
8 new tests; suite at 192 passed.

Co-Authored-By: Claude <[email protected]>
2026-08-24 02:36:38 -04:00
John SmithandClaude 53c42b4397 fix(gateway): LLM error logs name the exception; tracker through iteration 22
httpx transport exceptions stringify to "" so the JSONL audit log
recorded status:null + error:"" — indistinguishable from nothing.
Error fields now always carry the exception type. Recorded evidence
from the first LIVE campaign-simulation run against the configured
custom endpoint (llm.heretek.one): endpoint currently unreachable
from this network; the harness degraded per-turn exactly as designed,
counted every attempt, and the engine accepted 100% of executed
actions.

Co-Authored-By: Claude <[email protected]>
2026-08-24 02:25:26 -04:00
John SmithandClaude b4913c67ac feat(simulation): agent-driven campaign simulation harness (iteration 21)
The deferred flagship: synthetic players authenticate, host/join/launch
a lobby, deploy RBAC-owned characters, and take turns through the same
authenticated engine proxies real clients use. Decisions come from the
configured custom LLM endpoint via complete_json with strict decision
validation (unknown actions / malformed JSON / bad coordinates fall
back per-turn while still counting the attempt); no key means honest
scripted mode with zero network. Every LLM call flows through the
existing gateway so logs/llm_calls.jsonl captures it. Reports carry
only counted metrics with enforced invariants (accepted+rejected ==
attempted). 13 tests, fully mocked; __main__ entry runs live from .env.

Co-Authored-By: Claude <[email protected]>
2026-08-24 02:17:38 -04:00
John SmithandClaude 39609aede7 feat(gateway): wire LLM intent classification into the turn flow (iteration 19)
Audit follow-up: classify_with_llm existed only as tested library.
execute_orchestrator_turn now classifies FIRST — keyword safety hits
short-circuit before any network call or grounding; the LLM path
engages only when VTT_LLM_CLASSIFIER != 0 and a key is configured,
falling back to keywords with provenance. Non-mechanical intents are
gated away from the engine cycle (SKIPPED_NON_MECHANICAL / SAFETY_
INTERVENTION statuses), matching the playtest harness semantics.
Responses gain classifier/classified_intent/fallback_reason fields;
unconfigured environments see zero behavior change. 8 new tests with
network blocked by an exploding httpx fake.

Co-Authored-By: Claude <[email protected]>
2026-08-24 02:02:43 -04:00
John SmithandClaude 0a182da6c1 feat(engine): post-rewind snapshot in X-card response + session-state read proxy (iteration 15)
The browser had no readable path to authoritative post-rewind state,
so local tokens drifted until next hydration. The x-card response now
carries a full serialized GameSession snapshot taken after the rewind
(Null-degrading on serialization failure), and the gateway gains POST
/api/v1/engine/session-state — the first browser-readable proxy to
authoritative engine state, with identity forwarding when a token is
present. Per-role visibility filtering documented as future work.
8 pytest proxy tests + 1 integration test pinning post-heal rewind HP.

Co-Authored-By: Claude <[email protected]>
2026-08-24 01:50:51 -04:00
John SmithandClaude 50fe903023 feat(simulation): genuine GOAP planner for faction simulation (iteration 13)
Replaces greedy first-fit with STRIPS-style forward search: actions
declare preconditions and effects (thresholds + delta wrappers), the
planner runs uniform-cost A* over world-state snapshots with
deterministic tie-breaking, dominance dedup, depth/expansion caps
bounding EspionageRaid's positive resource loop. Risk folds into
planning cost; execution stays deterministic. Legacy tick behavior is
preserved verbatim when no goal is set — same three actions, same log
lines — so existing callers are untouched. New API: set_goal/clear_
goal/plan/execute_plan/goal_progress_pending. 19 new tests including
optimality-over-greedy and legacy-equivalence pins.

Co-Authored-By: Claude <[email protected]>
2026-08-24 01:42:18 -04:00
John SmithandClaude 4527774ccb feat(gateway): heal + rest engine proxies with identity forwarding (iteration 12)
POST /api/v1/engine/heal and /api/v1/engine/rest follow the established
proxy pattern: required token Query param, _caller_actor forwarded so
engine RBAC authorizes the real caller, strict Pydantic models
(extra=forbid, Literal rest kind) mirroring the engine's
deny_unknown_fields contract so no client math crosses the boundary.
8 new proxy-identity tests; suite at 150 passed.

Co-Authored-By: Claude <[email protected]>
2026-08-24 01:41:35 -04:00
John SmithandClaude 7c755caf4d feat(routing): optional LLM-assisted intent classification (iteration 10)
classify_with_llm() prompts the configured custom endpoint
(LLM_API/LLM_KEY/LLM_MODEL) for JSON classification into the real
IntentType enum, with layered honesty and safety:
- SAFETY_INTERVENTION always wins — keyword safety short-circuits
  before any network call; an LLM safety label overrides keyword
  disagreement
- any failure (kill-switch VTT_LLM_CLASSIFIER=0, no key, malformed or
  unknown output) falls back to the deterministic keyword classifier
  with provenance fields; never raises
- RULE_OF_COOL has no enum counterpart and is deliberately not mapped
complete_json() adds fence-tolerant JSON extraction + non-streaming
chat-completions with response_format json_object; degraded-marker
code preserved. 12 new tests, HTTP fully mocked.

Co-Authored-By: Claude <[email protected]>
2026-08-24 01:37:06 -04:00
John SmithandClaude 9367081b37 feat(gateway): honest degradation markers on SSE narrative fallback (iteration 5)
When the LLM upstream fails (no key, non-200, mid-stream error), the
deterministic fallback generator now announces itself: a sentinel from
stream_narrative makes the gateway emit a leading {degraded, reason}
frame and tag every subsequent frame degraded:true. Healthy streams
carry no degraded key at all. Non-streaming generation gains
generate_narrative() returning {narrative, degraded, reason?} with
shared canned prose so both paths stay identical. Clients can finally
tell real LLM narration from canned fallback.

Co-Authored-By: Claude <[email protected]>
2026-08-24 01:27:11 -04:00
John SmithandClaude 9b6caf12a3 feat(simulation): NPC disposition scoring engine (iteration 2)
Continuous directed relationship scores per (npc, player) pair per
GOALS.md Pillar 8: trust and fear with independent exponential decay
(fear half-life ~3.5x shorter), alignment-compatibility bias, NPC-level
stress that amplifies fear, event-driven interaction recording with
strict kind validation, stance banding (hostile..allied), and lazy
decay computed at query time so scores are pure functions of recorded
events. Injectable clock keeps it deterministic for replay/testing.
Pydantic v2 record model matches project conventions. 15 new tests.

Co-Authored-By: Claude <[email protected]>
2026-08-24 01:20:54 -04:00
John SmithandClaude fdc9f4a6d4 feat(playtest): measurable AFPR + auditor recall probes
AFPR's rejection branch was mathematically unreachable (run_turn_cycle
only returns COMMITTED / FALLBACK_COMMITTED), so false positives were
tautologically 0%. Now:
- grounded proposals that get rejected ARE counted as auditor false
  positives (a grounded draft describes the genuine engine outcome);
- ~10% of audited turns inject a recall probe — a draft narrating the
  death of a target live state says is alive — which the auditor MUST
  catch; new auditor_recall_pct metric (target >= 95%) reported by the
  benchmark suite;
- entity counts fed to the audit come from the live session snapshot.

Live run: MCR 100%, HCI 1.0, AFPR 0.0%, recall 16/16 = 100%.

Co-Authored-By: Claude <[email protected]>
2026-08-24 01:08:47 -04:00
John SmithandClaude 96ced54af3 feat(auditor): ground truth from live engine, audit-before-yield streaming, narrative lore triples
- Grounding: NarrativeGenerateRequest gains engine_session_id /
  target_entity_id. When a session is named, _resolve_audit_inputs fetches
  the live snapshot and the auditor reconciles against IT: lethality fields
  come from the live entity (a payload claiming death while the engine says
  25 HP can no longer smuggle a kill past Vector 3), entity counts come
  from the live snapshot, named-but-missing targets are rejected as ghost
  entities, and an unreachable engine fails closed with
  WORLD_INSPECTOR_UNAVAILABLE instead of auditing stale client claims.
- Streaming: tokens are now HELD until the sentence they belong to has
  passed the auditor; violating sentences and everything after them are
  never emitted — only the corrective SYSTEM frame and the cut.
- Lore continuity: Vector 4 was dead code (nothing ever populated its
  lore_* payload keys). The auditor now derives (subject, predicate,
  object) triples from canon entity names and predicate verbs appearing in
  the draft itself, including reflexive assertions like "Oakhaven Keep
  still stands". Payload triples still take precedence.

Co-Authored-By: Claude <[email protected]>
2026-08-24 01:08:47 -04:00
John SmithandClaude a7dc5868b5 fix(homebrew): fail-loud parsing, never fabricate statblocks
Unparseable input used to silently yield a combat-ready fake: AC 14,
HP 30, invented "Claw Strike" action with +5 / 2d6+3. Core fields now
stay absent when not parsed, every missing field is reported in
warnings, and responses carry parse_ok. Strict mode (new flag on
HomebrewParseRequest) rejects with 422 instead of returning a
half-invented statblock. Placeholder lists are copied per call to stop
cross-parse state leaks.

Co-Authored-By: Claude <[email protected]>
2026-08-24 01:08:27 -04:00