holonovel 2026.8.22 → 2026.8.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -42,7 +42,7 @@ state.buildFingerprint.lastSpecReview = new Date().toISOString();
42
42
  // ── Server ─────────────────────────────────────────────────────────
43
43
  const server = new McpServer({
44
44
  name: "inform-holonovel",
45
- version: "2026.08.22",
45
+ version: "2026.08.23",
46
46
  });
47
47
  // ── Helpers ────────────────────────────────────────────────────────
48
48
  // Badge gating and snapshot helpers provided by core/server.ts
@@ -542,9 +542,33 @@ server.registerTool("respond", {
542
542
  return ok("End novel cancelled.");
543
543
  }
544
544
  if (decision.toLowerCase().includes("present_choices")) {
545
+ const chosenId = String(option).trim();
546
+ // REQ-235b: record the player's choice in the audit log.
547
+ audit("respond", { decision, option: chosenId }, "[choice]");
548
+ // REQ-235b: advance countdowns whose scope equals the chosen id.
549
+ for (const [name, cd] of novel.countdowns) {
550
+ if (cd.scope && cd.scope === chosenId) {
551
+ cd.ticks--;
552
+ if (cd.ticks <= 0) {
553
+ novel.countdowns.delete(name);
554
+ audit("countdown_expired", { name });
555
+ }
556
+ else {
557
+ audit("advance_countdown", { name, remaining: cd.ticks });
558
+ }
559
+ }
560
+ }
561
+ // REQ-235b/c: advance faction clocks whose goals match the chosen id.
562
+ for (const f of novel.factions) {
563
+ const tokens = f.goals.join(" ").toLowerCase().split(/\W+/).filter(Boolean);
564
+ if (tokens.includes(chosenId.toLowerCase())) {
565
+ f.clock = Math.min(f.clock + 1, f.clock_max);
566
+ audit("faction_clock", { faction: f.id, clock: f.clock });
567
+ }
568
+ }
545
569
  novel.pending_workflow = null;
546
570
  state.saveNovel(novel);
547
- return ok(`Choice '${option}' selected.`);
571
+ return ok(`Choice '${chosenId}' selected.`);
548
572
  }
549
573
  if (decision.toLowerCase().startsWith("safety_escalation:")) {
550
574
  const target = decision.slice("safety_escalation:".length);
@@ -649,6 +673,9 @@ server.registerTool("undo", {
649
673
  }, async () => {
650
674
  requireNotObserver();
651
675
  const novel = requireNovel();
676
+ if (novel.pending_workflow) {
677
+ return err("STATE_CONFLICT", "A workflow decision is pending. Resolve it with respond before undoing.");
678
+ }
652
679
  state.undo(novel, getBadge());
653
680
  return ok("Undo successful.");
654
681
  });
@@ -659,6 +686,9 @@ server.registerTool("redo", {
659
686
  }, async () => {
660
687
  requireNotObserver();
661
688
  const novel = requireNovel();
689
+ if (novel.pending_workflow) {
690
+ return err("STATE_CONFLICT", "A workflow decision is pending. Resolve it with respond before redoing.");
691
+ }
662
692
  state.redo(novel, getBadge());
663
693
  return ok("Redo successful.");
664
694
  });
@@ -2848,7 +2878,9 @@ server.registerTool("create_novel", {
2848
2878
  return err("INVALID_INPUT", e.message);
2849
2879
  }
2850
2880
  }
2851
- return ok(`Novel created: ${novel.slug} (novel://current)${ruleset ? `, ruleset: ${ruleset}` : ""}${genre ? `, genre: ${genre}` : ""}`);
2881
+ return ok(`Novel created: ${novel.slug} (novel://current)${ruleset ? `, ruleset: ${ruleset}` : ""}${genre ? `, genre: ${genre}` : ""}
2882
+
2883
+ Next step: run the novel_setup guide to add characters, choose a story source, and hold a session zero before play.`);
2852
2884
  });
2853
2885
  server.registerTool("resume_novel", {
2854
2886
  title: "Resume Novel",
@@ -3865,37 +3897,32 @@ server.registerTool("toggle_synthesis_module", {
3865
3897
  });
3866
3898
  // ── Prompts ────────────────────────────────────────────────────────
3867
3899
  server.prompt("intro", "Introduction and Getting Started", async () => {
3868
- const novel = state.activeNovel;
3869
- const worldRooms = novel?.world.rooms.size ?? 0;
3870
- const hasWorld = worldRooms > 0;
3900
+ const novels = [...state.novels.entries()].map(([slug, n]) => ({ slug, name: n.name, description: n.description, modified: n.metadata.modified }));
3901
+ const libraryLines = novels.length > 0
3902
+ ? novels.map((n) => `- ${n.name} — ${n.description || "no description yet"} (last played ${n.modified})`).join("\n")
3903
+ : "";
3904
+ const library = novels.length > 0
3905
+ ? `\n## Your worlds\n${libraryLines}\n\nYou have ${novels.length} ${novels.length === 1 ? "world" : "worlds"}. Which would you like to resume, or create a new one?`
3906
+ : `\n## Your worlds\nThere are no worlds yet. Create your first one — a named, persistent campaign that holds your scenes, characters, and world.`;
3871
3907
  return {
3872
3908
  messages: [{
3873
3909
  role: "user",
3874
3910
  content: {
3875
3911
  type: "text",
3876
- text: `# Inform MCP Server — World-Model Interactive Fiction
3912
+ text: `# Welcome to Holonovel
3877
3913
 
3878
- This server provides a ruleset-free world-model layer for interactive fiction.
3879
- Parser commands let you navigate, examine objects, and interact with a spatial
3880
- world.
3914
+ This server runs tabletop roleplay with a real, persistent world. Every scene,
3915
+ character, and object lives on the server — not in a chat window — so your
3916
+ campaign survives restarts, breaks, and even rebuilds. Your campaign is the
3917
+ program you step into; the rules are whatever you bring to the table.
3918
+ ${library}
3881
3919
 
3882
- ${hasWorld
3883
- ? `**World model populated:** ${worldRooms} rooms, ${novel.world.things.size} things.
3884
-
3885
- ### Getting Started
3886
- 1. \`set_badge("player")\` — switch to player badge
3887
- 2. \`command("look")\` — describe the current room
3888
- 3. \`command("go north")\` — move through exits
3889
- 4. \`command("examine <thing>")\` — inspect objects`
3890
- : `**No world model populated.**
3891
-
3892
- ### Getting Started
3893
- 1. \`set_badge("game_master")\` — switch to GM badge
3894
- 2. \`create_novel({ name: "My World" })\` — create a new novel
3895
- 3. \`load_adventure({ slug: "<adventure>" })\` — load an adventure module
3896
- 4. \`convert_source({ source: "<world assertions>" })\` — parse room/thing declarations`}
3897
-
3898
- Use \`help\` to see all tools, or \`badge_briefing\` for current badge guidance.`,
3920
+ ## Getting started
3921
+ 1. Create a world for your campaign, or resume one from the list above.
3922
+ 2. Run the setup guide to add characters, choose a story source, and agree on
3923
+ tone before play.
3924
+ 3. Choose your role — player, game master, or observer.
3925
+ 4. Enter the story and begin your first scene.`,
3899
3926
  },
3900
3927
  }],
3901
3928
  };
@@ -3928,6 +3955,43 @@ Close each narrated turn by inviting the player's next action — ask what they
3928
3955
  else if (badge === "game_master") {
3929
3956
  briefing += `\n\n### Turn handoff
3930
3957
  You inhabit a player character. Close each in-character turn with an offer that hands initiative back to the human Game Master to describe the outcome or advance the scene.`;
3958
+ }
3959
+ // REQ-109 — boundaries (GM only): persisted persistent player boundaries,
3960
+ // never trimmed. Each is an absolute do-not-narrate directive.
3961
+ if (badge === "game_master") {
3962
+ const boundary = novel.player_signals.boundary;
3963
+ if (boundary) {
3964
+ briefing += `\n\n### Boundaries
3965
+ The player has set these topics as off-limits for the entire story:
3966
+ - ${boundary}
3967
+
3968
+ Do not narrate, imply, or introduce content that evokes these topics. They are
3969
+ kept out of every scene, every scene description, and every choice.`;
3970
+ }
3971
+ }
3972
+ // REQ-109 — novel-setup progress markers (surface wizard completion state).
3973
+ const setupMark = (done) => (done ? "[✓]" : "[ ]");
3974
+ briefing += `\n\n### Novel Setup
3975
+ Characters: ${setupMark(novel.characters_present)} | Story source: ${setupMark(novel.adventure_set)} | Session zero: ${setupMark(novel.session_zero_completed)}`;
3976
+ if (!novel.session_zero_completed) {
3977
+ briefing += `\nA session zero has not been completed yet — run the session zero guide before play begins.`;
3978
+ }
3979
+ // REQ-109 — POV directive (character-perspective anchor, GM + Player).
3980
+ if (badge === "game_master" || badge === "player") {
3981
+ if (novel.pov_mode === "character" && entity) {
3982
+ briefing += `\n\n### Point of view
3983
+ Describe the scene through ${entity.name}'s eyes — what they see, hear, and feel.`;
3984
+ }
3985
+ else {
3986
+ briefing += `\n\n### Point of view
3987
+ No character point of view is set — narrate in the third person until one is chosen.`;
3988
+ }
3989
+ }
3990
+ // REQ-109 — autonomy state (GM only): AI decision delegation sliders.
3991
+ if (badge === "game_master") {
3992
+ const a = novel.autonomy;
3993
+ briefing += `\n\n### Autonomy
3994
+ Level: ${a.level} | Confirmation: ${a.confirmation} | Safety: ${a.safety} | Creativity: ${a.creativity}`;
3931
3995
  }
3932
3996
  if (badge === "player") {
3933
3997
  briefing += `\n\n### Player Tools
@@ -3963,7 +4027,31 @@ You are both Game Master and Player. The human is observing. Narrate scenes, mak
3963
4027
  if (triggered.length > 0) {
3964
4028
  briefing += `\n\n### Triggered Lore\n${triggered.join("\n")}`;
3965
4029
  }
4030
+ // REQ-109 — narrative threads (GM): unresolved decisions, active
4031
+ // countdowns, non-neutral NPC dispositions, and active vows.
4032
+ const threadLines = [];
4033
+ for (const v of novel.vows) {
4034
+ if (v.state === "active")
4035
+ threadLines.push(`Vow: ${v.name} (${v.difficulty}, ${v.milestones} milestones)`);
4036
+ }
4037
+ for (const [name, cd] of novel.countdowns) {
4038
+ if (cd.ticks > 0)
4039
+ threadLines.push(`Countdown: ${name} (${cd.ticks}/${cd.total})`);
4040
+ }
4041
+ if (threadLines.length > 0) {
4042
+ briefing += `\n\n### Narrative threads\n${threadLines.join("\n")}`;
4043
+ }
4044
+ else {
4045
+ briefing += `\n\n### Narrative threads\nNo open threads — no active vows or running countdowns.`;
4046
+ }
4047
+ // REQ-109 — story journal (GM): recent decision/bond/moment entries.
4048
+ const storyItems = novel.story_journal.slice(-3).map((s) => `[${s.type}] ${s.entry}`).join("\n");
4049
+ if (storyItems) {
4050
+ briefing += `\n\n### Story journal\n${storyItems}`;
4051
+ }
3966
4052
  }
4053
+ // REQ-109 — intro pointer: always surface the intro prompt as an entry point.
4054
+ briefing += `\n\nUse the intro prompt to get started, or badge_briefing for current badge guidance.`;
3967
4055
  return { messages: [{ role: "user", content: { type: "text", text: briefing } }] };
3968
4056
  });
3969
4057
  server.prompt("session_zero", "Session Zero Setup", async () => {
@@ -3974,24 +4062,95 @@ server.prompt("session_zero", "Session Zero Setup", async () => {
3974
4062
  type: "text",
3975
4063
  text: `# Session Zero
3976
4064
 
3977
- ## World-Model Interactive Fiction
4065
+ Before the story begins, this guide helps you and the game master agree on the
4066
+ shape of the adventure. It is a creative check and a safety check — the choices
4067
+ you make here shape the narration for the whole story. Nothing needs to be final.
4068
+
4069
+ ## 1. Welcome and safety
4070
+ This is where you set expectations. You can change any of this later. If a topic
4071
+ is off-limits, say so now — the server remembers and keeps it out of the story.
4072
+
4073
+ ## 2. Tone, difficulty, pace, focus, and boundaries
4074
+ Each of these is a dial you can set. Here is what each one does.
4075
+
4076
+ Tone — the mood of the story.
4077
+ - Bright heroics: clear victories, warm allies, hope at the end of every scene.
4078
+ - Gritty realism: hard choices, scarce resources, consequences that stick.
4079
+ - Dark and ominous: creeping dread, fragile safety, victory at a cost.
4080
+ - Whimsical: playful logic, absurd coincidences, charm over threat.
4081
+ - Melancholic: quiet loss, bittersweet resolutions, emotional weight.
4082
+
4083
+ Difficulty — how much the world pushes back.
4084
+ - Gentle: the world gives you room to succeed; stakes are low.
4085
+ - Balanced: fair odds, real risk, meaningful choices.
4086
+ - Relentless: the world is hostile; every step costs something.
4087
+ - Brutal: survival itself is an achievement; loss is common.
4088
+
4089
+ Pace — how fast the story moves.
4090
+ - Slow: rich description, long scenes, time to linger.
4091
+ - Balanced: scenes move when they should.
4092
+ - Fast: cut to the action, little downtime, quick scene changes.
4093
+
4094
+ Focus — what the story centers on.
4095
+ - Exploration: discovery, maps, unknown places.
4096
+ - Combat: tactics, battles, escalating threats.
4097
+ - Social: dialogue, intrigue, relationships.
4098
+ - Mystery: clues, hidden truths, revelation.
4099
+ - Mixed: all of the above, in balance.
3978
4100
 
3979
- Before starting play:
3980
- 1. Create a Novel: \`create_novel({ name: "My Adventure" })\`
3981
- 2. Create characters: \`create_character({ name: "Hero", description: "..." })\`
3982
- 3. Populate the world model with \`convert_source\` or \`load_adventure\`
3983
- 4. The GM sets the opening scene with \`set_scene_state\`
3984
- 5. Players use \`set_badge("player")\` and start with parser commands
4101
+ Boundaries — what stays out of the story.
4102
+ Name any topics that should never be narrated, implied, or introduced. They are
4103
+ kept out of every scene, every scene description, and every choice.
3985
4104
 
3986
- ## Player Signals
3987
- - \`player_signal({ signal: "pace", value: "faster/slower" })\`
3988
- - \`player_signal({ signal: "difficulty", value: "harder/easier" })\`
3989
- - \`player_signal({ signal: "boundary", value: "<topic>" })\``,
4105
+ ## 3. Character introductions
4106
+ Three ways to introduce a character, from quickest to most detailed.
4107
+ - Quick: "A grizzled ranger who trusts no one." A single line is enough to play.
4108
+ - Full: three paragraphs — first appearance and mannerisms, then personality and
4109
+ voice, then backstory and motivation.
4110
+ - Media short-hand: "Like Samwise, but hardened by a decade in the arena — loyal,
4111
+ kind, and quietly ruthless when pushed." Name a known character, then say what
4112
+ to change or emphasize.
4113
+
4114
+ ## 4. Character creation
4115
+ Your world's rules define how characters are built. Every mechanical choice —
4116
+ ancestry, class, background, ability scores, equipment — is described in plain
4117
+ English, and each option's meaning for your character's abilities is explained.
4118
+ If the world has a roster, those characters are available to import directly.
4119
+
4120
+ ## 5. Adventure confirmation
4121
+ If an adventure is loaded, here is its premise, its factions and their tensions,
4122
+ its key characters, and its opening scene. You can accept it as-is or describe
4123
+ what to change. If you are building from scratch, describe the setting and the
4124
+ game master will shape it around you.
4125
+
4126
+ ## 6. What the game master can do
4127
+ During play the game master can set scenes, introduce characters, run world
4128
+ events, offer you choices, and pace tension. Each is described in plain English
4129
+ with examples of what you would say — no commands, no jargon.
4130
+
4131
+ ## 7. Quick start
4132
+ Everything is ready. The first scene begins when the game master sets it, and
4133
+ you describe what your character does. You can refine anything here at any time.
4134
+
4135
+ ## 8. Between stories
4136
+ Characters can grow between sessions — refine personality, voice, dialogue, and
4137
+ advancement when your rules provide it. The world you build is kept for the next
4138
+ session.`,
3990
4139
  },
3991
4140
  }],
3992
4141
  };
3993
4142
  });
3994
4143
  server.prompt("novel_setup", "Novel Setup Guidance", async () => {
4144
+ const novel = state.activeNovel;
4145
+ const marc = (done) => (done ? "[✓]" : "[→]");
4146
+ const steps = novel
4147
+ ? {
4148
+ characters: marc(novel.characters_present),
4149
+ adventure: marc(novel.adventure_set),
4150
+ sessionZero: marc(novel.session_zero_completed),
4151
+ rosterCount: state.roster.size,
4152
+ }
4153
+ : { characters: "[→]", adventure: "[→]", sessionZero: "[→]", rosterCount: state.roster.size };
3995
4154
  return {
3996
4155
  messages: [{
3997
4156
  role: "user",
@@ -3999,19 +4158,29 @@ server.prompt("novel_setup", "Novel Setup Guidance", async () => {
3999
4158
  type: "text",
4000
4159
  text: `# Novel Setup
4001
4160
 
4002
- ## Creating a World Model
4003
- Use \`convert_source\` with declarative assertions to populate the world:
4004
- \`\`\`
4005
- The Entrance Chamber is a room. "A dusty hall with torches."
4006
- North of the Entrance Chamber is the Hall of Statues.
4007
- The Hall of Statues is a room. "Tall statues line both walls."
4008
- A rusty sword is in the Entrance Chamber. "An old iron sword."
4009
- The Obsidian Door is north of the Hall of Statues and south of the Throne Room.
4010
- It is closed and locked.
4011
- The Serpent Crown is in the Throne Room. "A golden crown with emerald eyes."
4012
- \`\`\`
4161
+ A guided setup for your world. Complete each step in order; the markers show
4162
+ where you are.
4163
+
4164
+ ## Step 1 — Characters ${steps.characters}
4165
+ You have ${steps.rosterCount} ${steps.rosterCount === 1 ? "character" : "characters"} in your roster.
4166
+ Would you like to import one, create a new one, or move on? Character creation
4167
+ choices are described in plain English.
4168
+
4169
+ ## Step 2 — Story source ${steps.adventure}
4170
+ Choose where the story begins: load a written adventure, generate one from a
4171
+ premise, generate a random encounter, or build from scratch. Each option is
4172
+ explained in terms of what you get narratively.
4173
+
4174
+ Community-sourced play advice tailored to your adventure's themes is available
4175
+ for this world — voice examples, lore ideas, and scene advice. You can run
4176
+ synthesis now or proceed without it.
4177
+
4178
+ ## Step 3 — Session zero ${steps.sessionZero}
4179
+ Run the session zero guide to agree on tone, difficulty, pace, focus, and
4180
+ boundaries, and to confirm the opening scene.
4013
4181
 
4014
- Or use \`load_adventure\` to load a pre-written adventure module with a ## World section.`,
4182
+ When all three steps show [✓], the world is ready. A summary follows describing
4183
+ what is ready and how to begin your first scene.`,
4015
4184
  },
4016
4185
  }],
4017
4186
  };