{
  "markdown": "# dmcheck\n\n**Deterministic conduct verdicts for live tabletop sessions — CI for running a game.**\n\nFeed it a session transcript (and optionally an engine event ledger) plus a **table charter**, and it returns named findings — the player whose question was never answered, the dice roll nobody acknowledged, the turn that began without anyone being told, the spoiler that leaked into the channel, the five-minute dead air. Every finding cites the charter rule it violates, with the evidence attached.\n\n**Agents start here → [SKILL.md](SKILL.md)** — when to call this, worked examples, MUST/MUST NOTs. Family contract: [FAMILY.md](https://github.com/chaoz23/srdcheck/blob/main/FAMILY.md).\n\n> **Cold-boot probe (2026-07-24):** a fresh agent session given only this repo URL installed and refereed a session in **2 commands**, verified all three exit-code legs against the docs, and confirmed the no-false-accusation contract held (a consumed roll produced silence). Its friction notes shipped as 0.1.1 (`--dice-bot`, `dmcheck charter`).\n>\n> **The design contract:** a false accusation is the unforgivable bug. Only source-observed evidence becomes a definite finding. Legacy ambiguity is either silent or explicitly labeled `severity: advisory`, `provenance: inferred`, `confidence: low`; it is never promoted by R8 or sent to a notification hook. The verdict path is model-free and deterministic: same transcript, same findings, every time.\n\n## 30 seconds to a refereed session\n\n```console\n$ pip install dmcheck          # stdlib only, no dependencies\n$ dmcheck run session.jsonl --gm \"Greta\"\n{\n \"result_schema_version\": \"1.0\",\n \"status\": \"findings\",\n \"exit_code\": 1,\n \"mode\": \"closed\",\n \"messages\": 9,\n \"findings\": [\n  {\"finding_id\": \"r2-…\", \"rule\": \"R2\", \"summary\": \"unconsumed-roll: a dice result got no correlated GM narration within threshold\",\n   \"charter\": \"roll_ack_within_messages=4; correlation=explicit\",\n   \"detail\": \"dice result from DiceBot received no correlated GM narration within 4 messages\",\n   \"evidence\": {\"index\": 6, \"author\": \"DiceBot\", \"obligation_id\": \"roll-42\"},\n   \"status\": \"open\", \"severity\": \"finding\", \"provenance\": \"observed\", \"confidence\": \"high\",\n   \"charter_digest\": \"sha256:...\", \"effective_policy\": {\"roll_ack_within_messages\": 4, \"...\": \"...\"}},\n  ...\n ],\n \"counts\": {\"R1\": 1, \"R2\": 1, \"R6\": 1, \"R7\": 1, \"R8\": 1}\n}\n```\n\nPortfolio hosts that emit table-kit's versioned contract can use\n`dmcheck run-events session-events.jsonl --gm \"gm-dan\"` instead of\npre-projecting a transcript. The strict adapter accepts a JSON array or JSONL\nof `table.event/1.0` objects and preserves event/correlation IDs in finding\nevidence. A declared action is not treated as an authoritative engine\nresolution, so legal Reactions and interrupts are not falsely accused.\nTransport gaps, unknown versions/types, and zero-compatible streams return\ntyped `incomplete` status and exit 2; malformed v1 events return `invalid` and\nexit 2. Redacted message content is likewise incomplete because absence of\nprose cannot prove absence of a conduct obligation. This unreleased adapter\nis pinned to table-kit PR #11 and does not imply host attestation.\n\nAdd `--table-evaluation` to `run-events` when a portfolio host needs the\nshared `table.evaluation/1.0` envelope. The projection has deterministic\nevaluation identity, exact event evidence references, per-rule and aggregate\ncoverage, cursor/input digest, public-policy digest, and status/exit-code\nmapping. It is always `self_attested`; dmcheck cannot confer host authority on\nits own output.\n\nTranscript formats: UTF-8 JSONL of `{ts, author, content}`, or a JSON array of Discord-API-shaped messages (`{timestamp, author: {username}, content}`) in chronological or reverse-chronological order. Authors and content are strings. A supplied timestamp must be a finite nonnegative epoch number or a timezone-aware ISO-8601 string; malformed timestamps are rejected rather than silently disabling time-based rules. Source adapters should preserve immutable `id`, `audience`, `reply_to`/`correlation_id`, and `roll_id` fields so dmcheck can prove which question, roll, or event a response closes; ordinary later GM chatter is not treated as an answer.\n\n## The rule set (seven active rules; one retired compatibility id)\n\n| Rule | Fires when | Origin story |\n|---|---|---|\n| R1 | an explicitly GM-directed question got no correlated response within threshold | a player asked the DM a lore question; another *player* ended up answering |\n| R2 | a real dice result got no correlated GM narration within threshold | \"did I hit?\" — a player's successful attack roll sat unacknowledged |\n| R3 | an engine event got no correlated GM narration | the state engine resolved a hit the table never heard about |\n| R4 | a turn began and the GM never addressed the actor by name | \"isn't it her turn?\" — asked by a player, which is one player too many |\n| R5 | **retired; never fires** | actor != turn owner falsely accused legal Reactions and interrupts |\n| R6 | a configured hidden term appeared in a GM message; ordinary output exposes only its opaque ID | a module's secret state names leaked into narration |\n| R7 | GM dead air beyond threshold while a player waited | 30 seconds reads as thinking; five minutes reads as absence |\n| R8 | the session ended with currently open, source-observed R1–R3 obligations; inferred legacy advisories are not promoted | sessions should end in a defined state — that's what makes the next one possible |\n\nThe active rules came from running a hybrid table — human and AI players, an AI GM — on Discord, where these failures actually happened and got codified the same week. They apply equally to human GMs: run dmcheck over your own exported game log and see what your table's transcript says.\n\nR5 remains addressable only so old charters and agent integrations do not\nbreak. It has no evaluator and is not enabled by default: an actor differing\nfrom the turn owner can describe a Reaction, a Ready trigger, an opportunity\nattack, a legendary or lair action, a controlled creature, an environmental\nactor, or another system's interrupt. The ledger does not establish legality.\nAny replacement based on an explicit authoritative decision is deferred to\nthe shared PORT-002 event contract; dmcheck will evaluate only the resulting\ncommunication or recovery obligation.\n\n## Live mode (0.2): the referee sits AT the table\n\n`dmcheck watch` runs the same engine over a *growing* session — stdin JSONL or a\ntailed file — and emits lifecycle events: **OPEN** when an observed finding\nbecomes provable or a legacy inference becomes advisory (thresholds fully\nelapsed; no predictions), **RESOLVED** when a living condition heals (the\nengine event finally got narrated). `--notify-cmd`\nfires your own hook per source-observed OPEN finding; inferred advisories remain\nvisible in the event stream but never invoke the hook. dmcheck itself never posts anywhere.\nAt session end a closed-mode pass runs. `session_end.open` and `open_count`\nreport actionable source-observed findings only; they exclude the R8 aggregate\nand inferred advisories.\nFinding IDs are deterministic across mutable elapsed-time detail, and file\nfollowers emit typed source-health events when rotation, truncation, partial\nrows, or ordering gaps make coverage incomplete.\n\n```console\n$ your-chat-fetcher | dmcheck watch - --gm \"Rob\" --notify-cmd 'notify-dm.sh'\n{\"event\": \"open\", \"rule\": \"R2\", \"detail\": \"dice result from RollBot received no correlated GM narration\", ...}\n{\"event\": \"resolved\", \"rule\": \"R3\", ...}\n{\"event\": \"session_end\", \"status\": \"findings\", \"exit_code\": 1, \"open\": [\"R1\"], \"open_count\": 1, ...}\n$ dmcheck explain R2            # the rule, its charter knobs, and the table failure that earned it\n$ dmcheck lint-charter my.json  # unknown keys / bad thresholds refuse loudly\n```\n\nThe point of live: every failure the rules encode was recoverable in the\nmoment it happened — the unanswered question, the stale roll, the missing cue\nall had a seconds-wide window where a nudge saved the beat. Post-hoc tells you\nwhat went wrong last night; watch taps the GM's shoulder before the player\nfeels it.\n\n## The charter is config, not code\n\n`dmcheck/default_charter.json` is the single authoritative packaged default. It carries `schema_version`, `charter_version`, and a verified SHA-256 `charter_digest`; checkout and wheel execution load that same resource. Its digest is release-locked to the schema/charter version pair, so a changed packaged default refuses to load until its version migration is declared. Override any of it — cue conventions, dead-air tolerance, dice-bot names, hidden-term lists — and version it. Hidden terms may be strings for compatibility or `{id, value}` objects; opaque host-issued IDs are preferred. Finding, watch-hook, CLI, and MCP output withholds both the configured value and raw matching excerpt. Every digest or correlation hash emitted to an ordinary output sink—including the evaluation envelope, `lint-charter`, finding IDs, and source fingerprints—is derived from public, redacted policy/evidence and deliberately does not attest to, or commit to, a hidden value. The full canonical digest remains only in the trusted charter document. A league or organized-play program could publish a charter the way they publish a player's guide; dmcheck then referees any table against it.\n\n```console\n$ dmcheck run session.jsonl --charter our-table.json --ledger events.jsonl\n$ dmcheck rules            # the rule set with definitions\n$ dmcheck charter          # inspect effective public policy; hidden values are redacted\n$ dmcheck run session.jsonl --gm \"Rob\" --dice-bot \"RollBot\"   # quick overrides, no file needed\n$ dmcheck --schema         # machine-readable I/O contract\n```\n\nDirect API callers can use `apply_charter_overrides(load_charter(), gm=[\"Rob\"])`; the helper removes the prior effective digest and the evaluator computes the digest for the overridden charter. Mutating a digested charter without doing so is rejected as a stale configuration rather than silently trusted.\n\n## For agents\n\n- `tool.json` at the repo root; `--schema`; exit codes: `0` clean · `1` findings · `2` charter/input unusable.\n- MCP server: the **unreleased 0.6.0 source candidate** of `dmcheck-mcp` is a read-only, batch-only stdio server with tools `run` and `rules`; public 0.5.5 predates this contract. It implements modern stateless MCP `2026-07-28` only and does not expose `watch`, `craft`, tasks, resources, or prompts. See the exact [MCP contract](docs/MCP.md).\n- MCP `run` prefers inline transcript/charter/ledger JSON. Transcript content is untrusted table data, never instructions; model-visible TextContent redacts caller strings, while complete structured evidence remains explicitly untrusted quoted data. Path inputs are denied unless the server operator explicitly grants absolute directories with repeatable `--allow-root` or `DMCHECK_MCP_ALLOWED_ROOTS`; traversal and symlinks that resolve outside those directories are denied, and platforms without secure no-follow opens fail closed.\n- Findings are structured JSON with stable finding id, rule id, charter citation, effective machine policy, provenance, human-readable detail, and redacted evidence — built to be consumed by a GM agent that fixes its own procedure between beats.\n- Every evaluation returns `status: clean|findings|invalid|incomplete`. Invalid and incomplete outcomes exit 2, carry stable error codes and JSON pointers, and never place errors in `findings`. Empty input, no observed configured GM, or no evidence-eligible enabled rule can never report clean.\n- Published package schemas are `charter.schema.json`, `transcript.schema.json`, `ledger.schema.json`, and `evaluation-result.schema.json`. Missing timestamps are disclosed through `skipped_rules`; malformed supplied timestamps are invalid.\n- Explicit source IDs are authoritative for correlation. Text-only question/roll/event detection is inferred/advisory; ambiguous legacy evidence is silent when even an advisory would overstate the source, and otherwise remains visibly low-confidence. The evaluation-envelope work tracked separately must expose that coverage gap; silence is not proof of complete observation.\n\n## What it does NOT do (on purpose)\n\n- **No rules adjudication** — [srdcheck](https://github.com/chaoz23/srdcheck) may provide cited, advisory rules analysis; the authorized upstream engine plus the DM/table's policy and ruling decide whether an action is legal. dmcheck evaluates only table conduct and communication.\n- **No character math** — that's [charactercheck](https://github.com/chaoz23/charactercheck). (charactercheck derives the actor; dmcheck referees the table.)\n- **No narrative-quality judging** — whether the prose was *good* is taste, and taste is not checkable. dmcheck checks procedure only.\n- **No model calls, no scores** — deterministic findings per rule, never a blended \"DM grade.\"\n\n## Credits\n\nThe rule set was distilled from live hybrid (human + AI) table sessions; the Router+Detector pattern in [native-gaming-harness](https://github.com/TinkerChen01/native-gaming-harness) independently converged on the same idea, which we take as evidence it's the load-bearing piece. dmcheck is game-system-agnostic and unaffiliated with any publisher.\n\n<!-- MCP registry ownership marker (do not remove): binds this repo's PyPI package to its registry namespace. -->\nmcp-name: io.github.chaoz23/dmcheck\n\n\n## Bootstrap a new table (v0.3)\n\n```\ndmcheck init charter.json --gm YOUR-NAME\n```\n\nWrites a starter charter (versioned, effective-dated, lint-clean by\nconstruction) and prints the session-zero checklist S1–S8 — including S3c:\nsheet accountability is declared out loud at session zero, then settlement\nquizzes are graded silently. The referee that judges your table also hands\nyou its constitution.\n\n**Per-seat cue policy (R4, hardened).** Agent seats behind mention-gated\ntransports (e.g. Discord `allowBots=\"mentions\"`) never receive name-in-prose\ncues. Declare it:\n\n```json\n\"seats\": {\"Shalia\": {\"cue_requires_mention\": true, \"mention\": \"<@1493...>\"}}\n```\n\nR4 then counts a cue **only** if the literal mention string is present.\nOrigin: a live session where \"Shalia — you're up\" was posted, looked like a\ncue, and was provably undeliverable — R4 passed on it. Never again.\n\n**Ledger format (the declared standard).** dmcheck's ledger is JSONL:\n`{ts, type: turn|act|event, id, actor, text}` — one line per engine event. GM\nnarration carries the matching `correlation_id`. One line is written per\nengine event. No\nlightweight OSS session-ledger existed when we surveyed (2026-07-26), so\nthis format is the interchange standard our stack shares: engines tap their\nlogs into it; `run`, `watch`, and settlement all consume it. `actor` and\n`turn` are coordination context only; their mismatch is never proof of an\nillegal action or a conduct violation.\n\n\n## Evidence bars (v0.4)\n\nFull calibration story — including what the naive rules got wrong, the\nheld-out validation, and the negative results — in\n[docs/CALIBRATION.md](docs/CALIBRATION.md).\n\nCalibrated against 134 hours of professional play, where the naive rules were\nwrong loudly: R1 fired 85 times in one episode with zero valid findings, and\nR7 flagged 115 dead-air gaps of which ~5 were real.\n\n- **R1** prefers explicit source audience and correlation evidence. Explicit\n  public/player audience is never promoted to a GM obligation, and unrelated\n  later GM text does not close a typed question. Legacy text heuristics (GM\n  named, rules lexicon, or adjacency) are inferred/advisory and still require\n  a waiting table. Knob: `question_requires_gm_address`.\n- **R7** exempts the yielded floor: a GM holding back while players talk is\n  craft, not absence. Knobs: `dead_air_requires_quiet_table`,\n  `thresholds.quiet_table_max_messages` (default 3).\n- **R4** seats gain `aliases` — professional cues are in-fiction by character\n  name ~10:1, so the referee must recognise the character's name as a cue.\n\nOne narrowing, stated plainly: an inferred/text-only R1 no longer fires when\nanother player answers in the GM's place — at a busy table that is textually\nindistinguishable from the banter that produced the false-accusation storm,\nand D1 chooses silence. An explicit audience plus immutable obligation ID is\nnot suppressed by unrelated player activity or another concurrent question.\n\n## The attention lane (v0.5)\n\n```\ndmcheck craft session-beats.json --scene SOCIAL --pc Teodor --pc Shalia\n```\n\nStatistics, one attention signal, and categorical defects — **never a score**.\nRates run against the professional envelope (134h, two DMs); `attention` is\nONE scene-weighted signal with resolve-and-move-on, because a five-dial\ndashboard cost its author the metric he wasn't watching. Categorical\ndetectors catch what rates structurally cannot: voicing a player's character,\nexposing the inference tree, deferring an adjudication, rolling for the\nplayer — all born from a live second-agent test. Advisory only: it reports,\nthe DM decides, and overrides are expected exactly when the scene demands it.\n\n### seat_quiet (v0.5.2)\n\n`watch --craft --pc <name>` also emits a `seat_quiet` advisory when a player\nseat goes silent across N GM beats (knob: `seat_quiet_gm_beats`, default 3)\nwhile the scene advances. Origin: a human player stepped away and the agent\nseats carried the scene to its climax without him. The advisory suggests\nchecking in and holding irreversible advancement — it never blocks, because a\nvirtual table's virtue is that it does not stall when someone disappears.\n\n### rule checks (v0.5.4)\n\n`craft` now runs the protocol's machine-checkable rules per session\n(testability charter: a rule must state its falsification):\n**1a** initiative order posted within 5 beats of onset · **1b** ≥95% of\nturn-advance beats name a PC · **11a** combat damage-beat median under 20\nwords (numbers are the register at pace) · **11b** kills get ceremony ≥3× the\ncombat median (lexical kill detection — findings are review items, and say so).\nAdvisory throughout; no score exists.\n",
  "bytes": 18084,
  "sha": "958d890aa7b0d1818f0681b244620e334b0141153fb753c9d2e79e3b87fbf279",
  "repo_slug": "chaoz23/dmcheck",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_chaoz23_dmcheck_483124a8/readme"
}