Make the character toolkit dumber so the DM runs the rules
An architectural review flagged that we'd quietly grown a partial 5e
2024 engine inside the character compute/operations layer — automatic
resistance halving, concentration save DC hints, condition-based
disadvantage markers on the sheet, exhaustion folded into the displayed
skill and save totals. That's the opposite of what we want: the DM is
supposed to be in charge of rules so homebrew and variant rulesets just
work without fighting the tools.
This rips the rule-implementation bits out and tightens the invariant
("tools support, they don't implement") across the code, tests, docs,
and the DM system prompt.
- `character/compute.py`: no more `has_disadvantage_on_checks`,
`auto_fails_save`, or exhaustion folded into `skill_modifier` /
`save_modifier`. Passive perception no longer auto-subtracts 5 for
disadvantage.
- `character/operations.py`: `damage()` applies raw amounts; the `type`
field is metadata for narration and the log, not a rule trigger.
`add_effect(concentration=True)` is a metadata flag, not
uniqueness enforcement. `break_concentration` removed entirely —
`remove_effect` covers it. `use_resource` + `restore_resource` are
consolidated into a single `adjust_resource(name, delta)` where
negative spends and positive restores.
- `character/display.py`: the `✗` auto-fail and `◂` disadvantage
markers are gone. Exhaustion surfaces as an informational reminder
line rather than a folded numeric penalty — the DM applies the
effect at roll time per whatever ruleset they're running.
- `design/architecture.md` rewritten to match the actual CLI + FastMCP
composition + background-agents reality (the old doc was still
talking about Textual) and to enshrine "tools support, they don't
implement" as a first-class architectural invariant.
- `prompts/dm-system.md`: deleted the hand-maintained tool tables (the
LLM has MCP schemas for that already), added a "Tools Track State,
You Track Rules" section that enumerates what the tools do and don't
do, rewrote "never do arithmetic" as "two kinds of arithmetic" so
the DM understands tool-managed bookkeeping vs rule-effect math it
has to do at roll time, and expanded the "when to look things up"
guidance to explicitly cover condition rules now that the tools
don't apply them.
On the side, some follow-up work on DM timekeeping (which we'd been
talking about separately):
- Added an ambient `## ⏰ Current Time` header that renders at the top
of the DM's context every turn. The clock is the one thing the LLM
actually forgets about; putting it front-and-center is the cheapest
fix for "wait, is it still morning or did I advance?".
- Consolidated the three scattered timekeeping sections in the DM
prompt into one forceful "Read This Every Turn" section that points
at the ambient header and restates that the clock freezes if
`set_scene` is skipped.
- Silenced the `[Setting scene...]` chatter after every turn — it
fires on every response and was just noise now that the clock
update is visible in the ambient header instead. Added a small
`_SILENT_TOOLS` set in the engine for this.
And a small pytest ergonomic fix: `call_tool` moved out of
`tests/conftest.py` and into `src/storied/testing.py`, since `tests/`
isn't on the import path and the old `from tests.conftest import
call_tool` only worked with PYTHONPATH gymnastics. Plain `pytest` now
works.
693 passing, coverage 95.7%.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>