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 +219 -50
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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.
|
|
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 '${
|
|
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
|
|
3869
|
-
const
|
|
3870
|
-
|
|
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: `#
|
|
3912
|
+
text: `# Welcome to Holonovel
|
|
3877
3913
|
|
|
3878
|
-
This server
|
|
3879
|
-
|
|
3880
|
-
|
|
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
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
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
|
-
##
|
|
3987
|
-
|
|
3988
|
-
-
|
|
3989
|
-
-
|
|
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
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
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
|
-
|
|
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
|
};
|