Files
zalbot/docs/project-overview.md
Kaysser Kayyali 37a1a3d421 feat(specs): velvet-auction exercises group tools; FU-9 playtest gate; docs drift fix
FU-12 — velvet-auction.yaml now uses the group-encounter tools:
- minPlayers: 3 (lobby-gated party heist, matches PRD UJ-1)
- passiveReveals: Insight/15 (notices Karr's tell — Feature B)
- group_stealth skillChecks entry (group Stealth, successRule: majority,
  durationSeconds: 60) + skill_check_group_emit and character_status added
  to the tools list.
- specsToolsConsistency: emptied the NOT_YET_REFERENCED allowlist
  (skill_check_group_emit + character_status are now referenced); all 8
  registered tools are reachable from specs. Validated: specLoader +
  specsToolsConsistency + full unit suite (527) pass.

FU-9 — docs/release-playtest-checklist.md: the 7-step manual pre-release
  multi-player playtest checklist checked into the repo as a release gate
  (was buried only in the arch doc). Includes pass criteria (no orphaned
  thread / lost roll / raw-JSON leak) + the NFR-3/NFR-4 latency checklist.

docs/project-overview.md drift fix: pino -> src/lib/logger.ts (custom
  plaintext, ADR-002); primary LLM -> minimax-m3 via LiteLLM
  (LITELLM_MODEL); test count 22 -> 58; lib/ description; relabel dynamic
  goal registration as delivered.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-22 16:02:00 +00:00

6.9 KiB

Mardonar Encounter Engine — Project Overview

Discord-native, LLM-driven D&D encounter engine. Generated 2026-06-19 from a deep scan.

What it is

A Discord bot that runs structured D&D encounters. Each Discord thread is an encounter session. The bot loads a YAML spec, narrates the scene via an LLM (minimax-m3 through LiteLLM, with Ollama as fallback), voices NPCs with stable personas, runs skill checks via Discord embeds, and persists NPC memory + encounter history into a graph database through GraphMCP (JSON-RPC over HTTP). Optional Foundry VTT integration pulls live character stats and awards XP via an external relay.

Who it serves

Discord community members playing D&D 5e in the Land of Mardonar. The DM runs /encounter start <spec> to begin; players post their actions in the resulting thread. NPC personas are loaded from specs and grounded in long-term graph memory so that recurring NPCs remember prior interactions across encounters.

Tech stack at a glance

Layer Technology
Runtime Node.js 22 (ESM, TypeScript 5.8 strict)
Discord discord.js v14
LLM (primary) LiteLLM proxy — minimax-m3 (env: LITELLM_BASE_URL, LITELLM_MODEL)
LLM (fallback) Ollama (env: OLLAMA_BASE_URL) — gemma4-it:e2b, 128k context
Session cache Redis (ioredis), 12h TTL
Graph DB Neo4j (via GraphMCP JSON-RPC, not direct)
Lore / NPC memory GraphMCP HTTP JSON-RPC server
Foundry VTT External relay (optional, requires API key)
Validation Zod (env + encounter spec)
Logging src/lib/logger.ts (custom plaintext — pino removed)
Testing Vitest 3 (unit + integration)
Build tsc → multi-stage Node 22 alpine Dockerfile

Architecture type

Layered backend with a plugin-style tool registry.

Discord ──▶ src/bot/ (commands, embeds, handlers)
                │
                ▼
         src/harness/ (promptBuilder, contextAssembler,
                      llmClient, toolParser, toolDispatcher,
                      tools/* plugin registry)
                │
   ┌────────────┼────────────┐
   ▼            ▼            ▼
Redis        GraphMCP      VTT relay
(session     (JSON-RPC:    (Foundry
 state)      NPC memory,   live stats,
             lore, log)    XP grants)

Repository structure

Single-part monolith. All source under src/. The bot is one Node.js process that talks to external services over the network.

src/
├── bot/           # Discord I/O (commands, embeds, event handlers)
├── harness/       # LLM orchestration + 6 tool plugins
├── session/       # Redis-backed registries + session state
├── graphmcp/      # JSON-RPC client + Redis stream ingest
├── vtt/           # Foundry VTT relay client + spin-up
├── db/            # ioredis singleton
├── spec/          # YAML encounter loader + Zod schema
├── persona/       # persona.yaml loader
├── config.ts      # Zod env validation
├── lib/           # logger (custom plaintext), historyTrim, skillCheckMessages
├── scripts/       # deploy-commands (slash command registration)
└── types/         # shared interfaces + CONTEXT_BUDGET

Plus specs/ (8 encounter YAML files), tests/ (58 test files), data/ (runtime tally + summaries), and Docs/ (pre-existing project documentation, partially out of date).

Documentation

Key features in the current codebase

  • Per-encounter tool filtering. Each spec declares which tool plugins are active.
  • Dynamic goal registration (delivered) — tools/goalRegister.ts lets the LLM add new goals mid-encounter.
  • Three-pattern tool parser — handles fenced tool_call, bare tool_call header, and fuzzy bare JSON, so even smaller models can drive tools.
  • Self-spinning VTT relay — when the relay is down, the bot handshakes via RSA-OAEP and launches a headless Foundry session on demand.
  • Burst cap with drop notices — if too many messages arrive before the last LLM response, the bot drops the excess and posts a tone-aware notice.
  • Reaction lifecycle (👀) — visible "I'm working on it" feedback through queued → processing → complete states.
  • NPC memory injection at session start from GraphMCP, filtered by score threshold and capped at top-3 chunks above the threshold.
  • In-world voice for player-facing strings — no utility/jargon (see feedback-in-world-voice).

Known drift and open issues

Resolved in the 2026-06-19 /loop improvement pass (see docs/architecture.md §9 for the full list with dates):

  • Docs/mardonar-encounter-engine.md describes a Go bot with embedded MCP — superseded by docs/architecture.md but still referenced by the README. README now points to the right doc and notes the historical status of Docs/mardonar-encounter-engine.md.
  • README.md's project-structure tree is out of date (mentions src/mcp/, missing commands). README tree now reflects the actual 8-command layout and src/graphmcp/ (Neo4j/old src/mcp/ were both retired).
  • src/types/index.ts EncounterSpec diverged from src/spec/loader.ts Zod schema (missing tone, tools, randomizable, nameKey). EncounterSpec is now z.infer<typeof EncounterSpecSchema> — the static type and the runtime validator cannot drift. src/types/index.ts re-exports it.
  • Duplicate trimHistory between sessionManager.ts and contextAssembler.ts. Extracted to src/lib/historyTrim.ts; tests/unit/historyTrim.test.ts covers the shared module at 100%.

Still open:

  • No production docker-compose.yml, no CI/CD, no HTTP health endpoint. CI was added in 2026-06-19 (.gitea/workflows/test.yml) — the other two remain open.
  • DISCORD_ALLOWED_USERS empty by default — channel-scoped access only.
  • Two tracked dead-code files at the project root: index.ts and promptBuilder.ts. These are stale duplicates from before the project was reorganized into src/; tsconfig.json has rootDir: "src" so they are never compiled or imported. git rm is pending user approval.

See docs/architecture.md §9 for the canonical drift log.

When you're ready to plan new features

Point the PRD workflow at docs/index.md as input. For UI-facing work, architecture.md §5.1 is the primary reference. For backend/LLM feature work, architecture.md §5.2 and docs/data-models.md are the primary references.