Skip to content

bunkerlab-net/vellum

Repository files navigation

Vellum

An agent-driven, text-based D&D 5e (2014) tabletop RPG. The DM is an AI (Claude Code by default); the campaign is markdown on disk — characters, sessions, encounters, quests, world — so play survives interruptions, auto-compaction, and weeks away from the table.

Vellum ships with a themed web frontend (Astro + React, served by Bun) that wraps the agent in a chat-style story view plus a live character sheet — but the entire game still works through the underlying CLI agent if you'd rather skip the browser.

How it works

Claude Code reads AGENTS.md (symlinked from CLAUDE.md) on every session, which establishes the DM role, the 5e ruleset scope (all official 2014 sourcebooks), the campaign state layout, the dice protocol, and the live-persistence rules. Skills under .agents/skills/ drive the structured operations: starting a campaign, rolling up a character, opening or closing a session, building a balanced encounter, scaffolding a quest.

The web frontend is a thin shell over the agent: a Bun process spawns the agent (Claude / OpenCode / Codex) with cwd set to the project root, bridges its streamed output to the browser over a WebSocket, and renders the character panel by parsing the active campaign's markdown live. The agent's skills, mise run roll, and the campaigns/ tree all keep working unchanged — the browser just gives you a nicer surface than a terminal.

The conversation is volatile; the markdown files are authoritative. The DM writes every meaningful change to disk as it happens, so any session can resume cleanly — even after a context-window cut or in a brand-new conversation.

Requirements

Quick start

git clone [email protected]:bunkerlab-net/vellum.git
cd vellum
mise trust                # trust mise.toml in this repo
mise install              # installs bun 1.x and the rest of the toolchain
bun start                 # builds the frontend, spawns the agent, opens the browser

bun start runs astro build if the bundle is stale, binds an HTTP/WebSocket server (default port 4321, falls back to the next free port), spawns the agent with the project root as cwd, and opens your browser. Use the gear icon in the header to switch palette / fonts / story-mode / model / effort / permission mode mid-session.

Choosing an agent

The first non-flag argument after -- picks the agent. --model <value> is recognised by the wrapper and forwarded to whichever agent SDK is in use; everything else is passed through to the agent CLI verbatim.

bun start                                          # default: Claude Code
bun start -- claude --model opus                   # Claude with a model alias (sonnet / haiku / full id also work)
bun start -- claude --resume hollow-king-antonidus # Claude with extra CLI flags
bun start -- opencode                              # OpenCode (passes through the OpenCode SDK)
bun start -- opencode --model anthropic/claude-... # OpenCode wants a provider/model id
bun start -- codex --model gpt-5                   # Codex (supported but untested locally)

Debug logging

The Bun server emits structured HH:MM:SS.mmm LEVEL [tag] message lines for agent lifecycle, WebSocket activity, and errors. Set VELLUM_LOG to widen or narrow what's printed:

VELLUM_LOG=debug bun start             # verbose: includes per-agent debug events
VELLUM_LOG=warn  bun start             # quiet: warnings and errors only

Default is info. Info-and-above goes to stdout; warn/error go to stderr, so you can pipe them separately if needed.

Agents and SDKs

The default agent is Claude Code via the Claude Agent SDK. OpenCode (via the @opencode-ai/sdk) is fully supported. Codex (via @openai/codex-sdk) is wired up against the SDK but currently untested — the maintainer doesn't run Codex day-to-day, so adapter regressions may slip through.

Skipping the frontend

If you'd rather drive the DM straight from the terminal — no browser, no Bun server — the original CLI path still works:

mise run roll -- 1d20     # smoke test the dice
claude                    # (or `opencode`, etc.) launched in this directory

The agent reads the same AGENTS.md and skills either way; the campaigns directory and dice script don't care which front end you use.

Then, inside the agent, invoke a skill:

  • /campaign-creation — start a new campaign (setting, premise, factions, house rules)
  • /character-creation — roll up a level-1 PC, Baldur's Gate-style
  • /session-start — open or resume a play session
  • /session-end — close the session, snapshot state for next time
  • /encounter-build — DMG-balanced combat or skill encounter
  • /combat — run an encounter end-to-end with live initiative/HP tracking
  • /quest — scaffold a quest hook for the active campaign
  • /level-up — advance the active character one level
  • /long-rest, /short-rest — apply 5e rest mechanics
  • /inventory — manage equipment, currency, attunement

Skills

Skill Purpose
campaign-creation Interview-driven setup: name, setting, tone, themes, starting region, factions, house rules. Creates the campaigns/<slug>/ tree.
character-creation Level-1 PC creation. Baldur's Gate-style stat rolling (4d6 drop lowest, unlimited rerolls, free reallocation), then race / class / background / equipment / spells / personality. All official 2014 sourcebooks in scope.
session-start Loads campaign + character + prior-session context. Detects fresh, interrupted-resume, or mid-combat-resume mode. Creates the next sessions/NNN.md with a recap.
session-end Audits live state on disk, runs a close-out interview, appends an ## End-of-Session block with cliffhanger / open threads / XP, marks the session closed.
encounter-build Builds combat using the DMG XP-threshold table (levels 1–20), encounter multiplier, and the solo "fewer than three PCs" adjustment. Picks monsters from any official 5e source.
combat Runs a combat encounter end-to-end: initiative, turns, attacks, saves, damage, conditions, recharges, death saves, XP, loot. Updates the encounter's ## Live State block after every event for full mid-combat resume.
quest Lightweight five-field spine (hook, objective, complication, stakes, reward) plus NPC / location / faction connections.
level-up Advances the active character one level: HP roll/average, hit dice, class & subclass features, ASI / feat, spell slots, proficiency bonus, derived stats. Appends to the level history.
long-rest Restores HP, all spell slots, half hit dice (rounded down, min 1), reduces exhaustion by 1, resets long-rest abilities. Honors PHB / gritty / heroic resting variants.
short-rest Optional hit-dice spending (rolled live), recharges short-rest abilities (Warlock slots, Action Surge, Second Wind, ki, Channel Divinity, etc.).
inventory Add / remove / buy / sell / equip / list / transfer items. Recomputes total weight, carrying capacity, encumbrance, currency, and attunement slots.

Dice

All randomness flows through mise run roll — the DM never invents results.

mise run roll -- 4d6dl1       # 4d6 drop lowest (stat rolling)
mise run roll -- 1d20+5       # d20 with +5 modifier
mise run roll -- 2d8+3        # 2d8 with +3 modifier
mise run roll -- 4d6kh3       # 4d6 keep highest 3
mise run roll -- 3d6kh2-1     # 3d6 keep highest 2, then -1
mise run roll -- 1d20+3+1d4   # attack + Guidance (compound)
mise run roll -- 1d8+3+3d6    # weapon + Sneak Attack (compound)
mise run roll -- 1d20-1d4     # Bane penalty die (compound)

Operators: dlN drop-lowest, dhN drop-highest, khN keep-highest, klN keep-lowest, +N / -N flat modifier. Chain dice and constants with + or - for compound rolls (Guidance, Bless, Sneak Attack, Bane, Bardic Inspiration).

The script (scripts/roll.ts) uses crypto.getRandomValues with rejection sampling, so the distribution is unbiased.

Project layout

.
├── AGENTS.md             # DM onboarding doc (the canonical file)
├── CLAUDE.md             # symlink → AGENTS.md
├── .agents/              # canonical skill home
│   └── skills/
│       ├── campaign-creation/SKILL.md
│       ├── character-creation/SKILL.md
│       ├── session-start/SKILL.md
│       ├── session-end/SKILL.md
│       ├── encounter-build/SKILL.md
│       ├── combat/SKILL.md
│       ├── quest/SKILL.md
│       ├── level-up/SKILL.md
│       ├── long-rest/SKILL.md
│       ├── short-rest/SKILL.md
│       └── inventory/SKILL.md
├── .claude               # symlink → .agents
├── scripts/
│   └── roll.ts           # Bun dice script
├── server/               # Bun web server: WS hub, agent adapters, character parser
│   ├── cli.ts            #   entry point — `bun start` runs this
│   ├── server.ts         #   Bun.serve (static + /api + /ws)
│   ├── character.ts      #   markdown -> Character JSON
│   └── agents/           #   claude / opencode / codex SDK adapters
├── src/                  # Astro + React frontend (themed chat + character panel)
├── public/               # static assets served as-is (default portrait, etc.)
├── mise.toml             # tool versions + task definitions
├── campaigns/            # gitignored — your playthroughs live here
│   └── <slug>/
│       ├── campaign.md           # premise, setting, factions, house rules
│       ├── state.md              # active character, location, where-we-left-off
│       ├── characters/<slug>.md  # PC sheets
│       ├── sessions/NNN.md       # session logs (live, append-only during play)
│       ├── encounters/<slug>.md  # prepared / paused encounters
│       ├── quests/<slug>.md      # quest entries
│       └── world/
│           ├── regions.md        # locations
│           └── factions.md       # faction roster
├── .editorconfig
├── .zed/settings.json
└── .gitignore

Campaign data

Your campaigns are personal user content, not part of the harness. campaigns/ is .gitignored, so characters, sessions, and DM-only plot notes never get pushed. If you want backup or history of your campaigns, keep them in a separate private repo or your usual backup setup.

Conventions

All generated files conform to .editorconfig and .zed/settings.json:

  • UTF-8, LF line endings
  • Trailing newline at end of file
  • No trailing whitespace
  • 2-space indentation
  • Markdown wraps around 100–120 columns (code blocks and tables exempt)

The repo follows the AGENTS.md convention with CLAUDE.md as a symlink, applied both at the file level (AGENTS.md / CLAUDE.md) and directory level (.agents/ / .claude). Edit the AGENTS.md / .agents/ side; the symlinks just keep Claude Code's native conventions working.

Status

Working: campaign, character, session, encounter, combat, quest, level-up, rest, and inventory skills, plus dice, live persistence, and the Bun + Astro web frontend with the Claude / OpenCode adapters. Codex adapter ships unverified — see Choosing an agent.

The structured-state operations are fully covered. Anything outside that scope (free-form roleplay, exploration, social encounters, downtime activities) is handled conversationally by the DM with the same live-persistence discipline.

About

Agent-driven D&D 5e text RPG. The DM is an AI; the campaign is markdown on disk.

Resources

License

Stars

Watchers

Forks

Contributors

Languages