Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion qa/lib_beat_driver.sh
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ clawdnd_dm_lean_args() {
• events / companion_arcs — any decisional that fired this beat, and any bond that just turned or betrayal_warning to foreshadow.
• recent_narration — the last $lean_tail player-facing beats' prose (the immediate story-so-far).
• state — the volatile current scene, party vitals, day/time, active quests, combat, pacing_mode, seed_params.
Do NOT contradict any of it, re-introduce an already-met NPC, reset the clock, or forget a prior choice. CRUCIAL — LOSSLESS RULE: this compact bundle is the always-pinned SPINE, not the whole world. For ANYTHING the moment reaches back to that is NOT in this bundle (a fact, NPC, place, event, or lore detail from earlier), you MUST retrieve it BEFORE you narrate — the entire world/lore/history is searchable on disk: call recall(campaign_id=\"$campaign_id\", query=\"…\") for past events/decisions/facts, lookup_lore(campaign_id=\"$campaign_id\", query=\"…\") for world/setting lore, or recall_npc(campaign_id=\"$campaign_id\", npc_id=\"…\") before voicing a returning NPC. NEVER guess and NEVER invent a detail that contradicts established canon — retrieve first. (You may also pass recall_query=\"…\" to scene_context to fold a recall into the same first call.) Then resolve the move and narrate, seamlessly continuing the established story.")
Do NOT contradict any of it, re-introduce an already-met NPC, reset the clock, or forget a prior choice. CRUCIAL — LOSSLESS RULE: this compact bundle is the always-pinned SPINE, not the whole world. For ANYTHING the moment reaches back to that is NOT in this bundle (a fact, NPC, place, event, or lore detail from earlier), you MUST retrieve it BEFORE you narrate — the entire world/lore/history is searchable on disk: call recall(campaign_id=\"$campaign_id\", query=\"…\") for past events/decisions/facts, lookup_lore(campaign_id=\"$campaign_id\", query=\"…\") for world/setting lore, or recall_npc(campaign_id=\"$campaign_id\", npc_id=\"…\") before voicing a returning NPC. NEVER guess and NEVER invent a detail that contradicts established canon — retrieve first. (You may also pass recall_query=\"…\" to scene_context to fold a recall into the same first call.) Then resolve the move and narrate, seamlessly continuing the established story. OUTPUT DISCIPLINE — the lean fresh-session re-grounds on a STATE digest and tends to shortcut into bookkeeping; do NOT. Your reply is 2nd-person in-fiction PROSE + quoted dialogue ONLY: NEVER a planning/intention note (\"I'll play that as a held beat…\"), NEVER a 3rd-person scene SUMMARY (\"The scene is resolved and persisted…\", \"the enforcer's threat now has a clock\", \"The floor is yours\"), NEVER a director/tool/stat leak. Any NPC the player ADDRESSED this move SPEAKS a real quoted in-voice line THIS beat — never summarized (\"she considers\", \"he weighs it\"). Narrate the beat ONCE: the prose you stream via log_event and the prose in your reply MUST be the SAME words (a reworded second version double-shows the beat in the player's chronicle).")
}

# DM-TURN EFFORT TIER (the ONE shared implementation of the cold-open-vs-routine effort split).
Expand Down
2 changes: 1 addition & 1 deletion qa/run_duo.sh
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ Begin the session. The player agent introduces their character and opening inten

$PMSG

Do the setup now: start_world(\"$WORLD\"), start_session, then seat THEIR character as the PLAYER CHARACTER (the PC). The player ALWAYS plays a REAL, LIVING CANON NPC — their persona names one (e.g. Aubree, a Flaming Fist ranger). Seat that exact figure via load_canon_character(their canon name, kind=\"player\", add_to_party=true) so they get a real backstory + ingested portrait — NEVER create_character / invent a custom PC, NEVER seat the player's own character as a companion or NPC, and NEVER a canon-DEAD figure (a corpse like Dal Lightspark is rejected as a PC; if the seat returns an error, pick a living canon NPC instead). A companion is a DIFFERENT character the player MEETS. Then OPEN the scene — human-scale and personal — grounded in the world's canon, responding to their stated intent. A companion should ENTER as part of that opening scene: someone the player MEETS on-screen (voiced, with a real wound and a reason they fall in together) — recruit_companion / load_canon_character(kind=\"companion\") as that meeting lands, NOT a silent name dropped into the party before the player has met anyone. End by handing the moment to the player.")"
Do the setup now: start_world(\"$WORLD\"), start_session, then seat THEIR character as the PLAYER CHARACTER (the PC). The player ALWAYS plays a REAL, LIVING CANON NPC — their persona names one (e.g. Aubree, a Flaming Fist ranger). Seat that exact figure via load_canon_character(their canon name, kind=\"player\", add_to_party=true) so they get a real backstory + ingested portrait — NEVER create_character / invent a custom PC, NEVER seat the player's own character as a companion or NPC, and NEVER a canon-DEAD figure (a corpse like Dal Lightspark is rejected as a PC; if the seat returns an error, pick a living canon NPC instead). A companion is a DIFFERENT character the player MEETS. Then OPEN the scene — human-scale and personal — grounded in the world's canon, responding to their stated intent. A companion should ENTER as part of that opening scene: someone the player MEETS on-screen (voiced, with a real wound and a reason they fall in together) — recruit_companion / load_canon_character(kind=\"companion\") as that meeting lands, NOT a silent name dropped into the party before the player has met anyone. End by handing the moment to the player. OUTPUT DISCIPLINE — your final reply IS the opening scene: write it as 2nd-person in-fiction PROSE + quoted dialogue ONLY. NEVER narrate your own setup/process — no \"State is grounded\", no \"the cold open is on the dashboard\", no \"Closing my turn on the scene\", no 3rd-person status line. The very first words the player reads must be INSIDE the fiction.")"
# #357: recover the engine's logged narration if the DM turn ended on a tool call / status
# line (empty final reply) — so a tool-final-but-narrated turn isn't mistaken for silence.
DMSG="$(clawdnd_dm_narration_or_fallback "$DMSG" "$STATE_DIR")"
Expand Down
31 changes: 31 additions & 0 deletions servers/engine/tests/test_lean_output_discipline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Guard: the lean re-ground directive + the duo cold-open carry OUTPUT DISCIPLINE (anti-leak/summary).

Opus lean routine beats intermittently shortcut into bookkeeping (VM 2026-06-06, run vm2-opushi-narr):
a planning-leak preamble ("I'll play that as a held beat...") and a 3rd-person stage-direction summary
("The scene is resolved and persisted...") instead of narration. The SKILL.md has the anti-scaffolding
rules, but the lean re-ground directive only re-grounded STATE and never restated the OUTPUT discipline,
so the fresh lean session narrated loosely. The duo cold-open prompt separately induced a process-
narration leak ("State is grounded... on the dashboard. Closing my turn..."). Both now carry an explicit
discipline clause. Validated: lean beats clean; the duo cold-open opens in-fiction.
"""
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[3]


class OutputDisciplineTests(unittest.TestCase):
def test_lean_reground_directive_has_output_discipline(self):
src = (ROOT / "qa" / "lib_beat_driver.sh").read_text(encoding="utf-8")
self.assertIn("OUTPUT DISCIPLINE", src, "the lean re-ground directive must restate output discipline")
self.assertIn("3rd-person scene SUMMARY", src, "must name the summary-not-narration failure mode")
self.assertIn("planning/intention note", src, "must name the planning-leak failure mode")

def test_duo_coldopen_has_output_discipline(self):
src = (ROOT / "qa" / "run_duo.sh").read_text(encoding="utf-8")
self.assertIn("OUTPUT DISCIPLINE", src, "the duo cold-open must restate output discipline")
self.assertIn("NEVER narrate your own setup", src, "must forbid the setup-process narration leak")


if __name__ == "__main__":
unittest.main()
Loading