Troupe authoring brief
One page for people and for LLMs asked to write Troupe files. Give it to a model whole, with the request. It is informative; Troupe Format is normative, and the authoring guide has patterns for bigger stories.
A complete card:
{ "troupe": "0.12", "kind": "bundle", "characters": [{ "id": "mira", "name": "Mira", "description": "A sarcastic starship engineer.", "entries": [ { "id": "ship", "keys": ["Kestrel"], "text": "The Kestrel is Mira's patched-up freighter." }, { "id": "debt", "knownBy": ["mira"], "text": "Mira owes the Corvane syndicate money." } ] }], "scenarios": [{ "id": "kestrel", "openings": [{ "id": "docked", "content": [{ "speaker": "mira", "text": "*She doesn't look up.* It's supposed to sound like that." }] }] }]}Shapes. IDs start with a lowercase letter and use only a–z, 0–9 and _, at most 64 characters. Openings belong to scenarios and are {"id", "content"}, or {"id", "title"} for one the model writes; content and event message are arrays of parts, [{"speaker": "mira", "text": "…"}], never strings. examples is an array of exchanges, each an array of {"speaker": "char" | "user" | id, "text"}. Text is Markdown, never HTML; put a character’s actions in italics in the speaker’s own part.
Macros. {{char}} is the character the text belongs to and {{user}} the player’s character. {{state.bell}} and {{own.nerve}} show values; {{@kai}} shows a name. Name other characters with {{@kai}} rather than writing “Kai”, so renames and recasts keep working.
Entries: {"id", "text"} is always on. Add keys to activate on words, position: "system" for rules, knownBy: ["id"] for secrets or one character’s instructions, when for conditions, priority (0–100, 100 never left out).
Scenarios hold openings and, for stories, cast (an object without character is a player seat; the default is one seat and every character), premise, entries, variables and events.
Variables need id, type (bool, int, number, string, enum with values, or set) and description. Give by: ["model"] for values the story should update, and of: "characters" for one value per character. Read a scenario’s variable as state.bell, a character’s own as own.nerve, and another character’s as state.kai.nerve.
Events need id and when. set takes plain values; compute takes CEL strings. Durations count rounds: one player message and the replies to it. An event on: "player" fires only after a player’s message, so it can roll for what the player attempted and direct the next reply.
Traps:
settakes plain values:"set": {"hp": 10}. Expressions go incompute:"compute": {"hp": "state.hp - 1"}.- Everyone knows a character’s entries unless
knownByis set. Put secrets inknownByentries. - Inside CEL, strings use single quotes:
"when": "state.phase == 'escape'". Look up a computed key with brackets:state.trust[char]. - Numbers in CEL arithmetic match the variable’s type:
state.trust.kai - 0.1for anumber,state.hp - 1for anint. chanceis 0–1, not a percentage.weightandpriorityare integers.- A numeric
positionis a depth in history, not an order; write4, not"4". - Without
by, only openings and events can change a variable, and players can change one withask. - An entry, a variable and an event of the same owner can’t share an ID.
knownBy: ["narrator"]is for the narrator alone; an explicitknownByhides an entry from the narrator unless it listsnarrator. Narrator-only clues belong to the scenario, because a character’s entries only count while they are present.- Characters, worlds and scenarios can’t be called
char,user,narrator,session,state,scene,playersorown, and a variable can’t be calledsceneorown; no ID can be a CEL word such asvar,loop,functionorpackage. - Leave the player’s character out unless the story defines it; then mark its cast member
player. - Don’t write
null. Leave fields out.
Validate the result against bundle.schema.json and fix every error before returning it.