M3 showrunner for world-building
M3 makes Reelwright something you talk to:
"Create a new show: a comedic starship crew, Saturday-morning 80s style."
In reply you get a theme, a style, a crew and a bridge, each as a proposed change set with previews. You approve, tweak, lock, undo or ask for variations ("give me 4 options", "the second one, but taller"). It works from Claude Code (MCP) and from Reelwright Studio.
Read with ARCHITECTURE §10–§11 and ADRs 0003, 0005, 0007 and 0011, and
schemas/v1/changeset.schema.json.
Layers
Studio / editor dock (Godot UI) Claude Code / Desktop (MCP client) reelwright chat (terminal)
│ session protocol (JSON-RPC 2.0, WebSocket) │ MCP (stdio) │ in-process
└──────────────────────────────┬───────────────┴────────────────────────┘
core: Showrunner agent (LLM) ← not used over MCP (the client is the LLM)
│ tool calls
core: ToolRegistry ── one registry for every surface
│
ChangeStore · DependencyGraph · PreviewService · CostMeter
│
Show Pack on disk (+ git) engine jobs (previews)
ToolRegistry (core/src/studio/tools/)
A tool is a name, a description written as a prompt, an input schema, and a handler.
- Input schemas are
$defsinschemas/v1/tools.schema.json(e.g.ReviseInput: an id plus a list of{pointer, value}edits).ToolRegistryinregistry.tsvalidates every input before a handler runs.bundleSchemainlines cross-file refs for Claude and MCP. - Strict tools have a budget. Claude compiles all the strict schemas in one request
together, within at most 20 tools, 24 optional parameters and 16 union parameters.
- Only small inputs are
strict: true: ids, pointer edits, generation options. - Tools that author whole documents (
create_theme,create_style,create_*_pattern,create_props) are non-strict. The full schema is still sent; validation and the repair loop catch mistakes. - The strict flavor (
strictToolSchema) moves constraints Claude does not enforce into descriptions, the way the SDKs do. - The limits count every strict schema the request carries, a tool found by search
included (the grammar is built from the whole toolset). So only the tools in view
(
coreTools, below) are sent strict: 10 tools and 3 optional parameters on the demo. A tool found by search goes non-strict whatever its registry flag, andToolRegistry.callchecks its input, with repairs. What a turn sends on the demo is tested againststrictLimits(showrunner-agent.test.ts).
- Only small inputs are
- Tools found on demand (
core/src/showrunner/tool-search.ts). Every request carries every tool's definition, but only a dozen are in Claude's view from the start (coreTools):show_summary,get_document,list_documents,search_canon,show_memory,estimate_costand the everyday revisions (revise_character,revise_set,revise_theme,revise_style,write_bible_entry,vary). The rest carrydefer_loading: true, and Claude finds one with the API's tool search (tool_search_tool_bm25_20251119, a plain-words query), which loads its definition then. A system block between the rules and the show context names every searchable tool, so Claude knows what there is. A first message on the demo is about 9,100 tokens; with every tool in view it was about 128,500. Deferred tools stay out of the cached prefix, so a search does not break the cache; the search's blocks (server_tool_use,tool_search_tool_result) go back to Claude as they came.- Over MCP (
reelwright mcp, the web service's endpoint) the same tools, withstudio_contextandlist_shows, carry_meta: {"anthropic/alwaysLoad": true}: Claude Code keeps an MCP server's other tools behind its own tool search.
- Over MCP (
- Handlers are pure over (show state, input) → result. They never write specs. A
proposing tool returns a ChangeSet (status
proposed) plus preview references. - The same registry backs every surface: MCP, the showrunner agent, the session protocol and the CLI.
| Kind | Tools |
|---|---|
| Read | show_summary, get_document, list_documents, search_canon, impact (what re-derives if X changes), find_uses (every document that names a document or sound by id, and every screenplay line whose cue or scene heading calls a character or set), render_preview (model sheet, set views, style swatch, storyboard stills), estimate_cost |
| Propose | create_theme, revise_theme, create_style, revise_style, create_character_pattern, generate_characters (count, archetypes or roles, ensemble), vary (N options over pointers, one variation group), revise_character (pointer edits), create_set_pattern, build_set (sample a SetPattern), revise_set, revise_pattern (pointer edits to any pattern), create_props (LLM-authored PropInstances with recipes), revise_prop (pointer edits to one prop: its recipe, palette, how it is held and its states; non-strict, past the strict-tool budget), create_graphic (in-world signs, screens and badges), create_sound (synthesized sound recipes), create_fx (particle, glow and flash effects), restyle_show (switch or edit the active style; design transforms on locked characters are flagged), write_bible_entry, write_screenplay (a Fountain draft, noting what the show lacks), edit_episode (typed re-direction edits, compiled before proposing), revise_fx, revise_graphic, revise_outline and revise_sound (pointer edits to an effect, a graphic, an outline, a sound recipe synthesized before it is proposed), remove_document, rename_document and duplicate_document (any document or sound; see "Removing, renaming and copying"), import_sound (a WAV, Ogg, MP3 or FLAC kept as the show's FLAC) |
| Decide | approve_change, reject_change, lock, unlock, undo, redo |
Removing, renaming and copying. Any document or sound below the show can be removed,
renamed or copied (core/src/studio/tools/documents.ts, core/src/studio/references.ts), one
change set each, undone like any other, in the person's hands and the showrunner's:
- A rename takes every reference with it: strings that are the id anywhere in another document
(an episode's cast and cues, a set's props and links, the bible's
about, memory), the files that move (an episode's folder, screenplay and outline, a sound's file), and the screenplays' character cues (a cue that called a character by its name, first name, short name or id slug says the new one; one through the episode's castMap stays) and scene headings that named a set by its name, in scripts, episodes and outlines alike. A character's short name that was only its name or first name ("PIP ROOK" or "PIP" for Pip Rook) follows a new name, in its own case; one of its own ("SKIPPER") stays. Action lines and parentheticals say the new names too ("Ibden looks up." → "Wren looks up.", an introduction in capitals in capitals), whole words only; what is spoken stays as recorded, so no take goes stale. A recorded sound's name is the title its FLAC carries (its TITLE comment):sounds.importandimport_soundname it from the file it came in as ("Fog_Horn.wav" is "Fog Horn"), and a rename rewrites the title, the audio byte for byte as it was. - A remove is refused with every use (
document.in_use, each use a diagnostic naming the file), unlessreplaceWithputs another of its kind in its place everywhere (cues and headings follow), oranywaydrops it from the lists that name it and leaves the rest for validation. An episode goes with its folder. The active style is switched first. - A copy gets
<id>_copy(numbered), "copy", no locks and fresh provenance. - Locks hold: a locked document is neither moved nor removed, and a locked reference is never rewritten.
- Change sets carry files of bytes (
Contenttypebinary, by SHA-256 in the objects store), so a recorded sound is added, moved and taken back byte for byte.
Decide tools belong to the human.
- In Studio they are buttons, and the agent is never given them.
- Over MCP they are exposed with
destructiveHint, and their descriptions say they must only be called after the user explicitly asks. - The CLI has
reelwright changes approve|reject|undo|redo.
ChangeStore (core/src/studio/changes/)
Proposals live in
<show>/.reelwright/changes/<id>.json, validated againstchangeset.schema.json. Each operation carries the file's base hash. A proposal whose base no longer matches the file on disk becomesstaleand is never applied.- A tool reads one snapshot of the show (
readShow) and proposes against it: base hashes are those of the bytes the tool read, however long it worked (agenerate_characterswaits seconds for the engine). If a file it writes changed on disk meanwhile, nothing is proposed (change.stale), since the proposal would be stale at birth.
- A tool reads one snapshot of the show (
Approve applies every operation atomically: write temp files, then rename, and roll back on failure. Every written document goes through the canonical formatter and must validate (schema + semantic) as a whole show before anything is written. The status becomes
applied.- Each target is compared with its base again as it is set aside, just before the new
file is renamed in, and once more after every rename. A save that lands after the stale
check (while the show validates, say) is put back, and the change becomes
stale. A backup is deleted only while it still holds the base; one that changed is kept, and the applied change's history names it. - A change file is checked against the schema whenever it is acted on, and every path
again when it is written: inside the show, no
.., no dot-folder (.git,.reelwright), and never through or onto a symbolic link. - If the Show Pack is a git repository, approve also commits the change as the git user
configured in that repo. The message is the change's title and rationale, with a
trailer
Reelwright-Change: <id>. - Only a show that is the top of its own repository is committed. A show that sits inside
another repository is not committed there; the change's history says why.
reelwright initgives a new show its own repository, even inside another one, and never touches the outer one. - Git runs with no
GIT_*variable and noEMAILfrom the environment, and withuser.useConfigOnly, so the identity is always one configured in git. Without one, nothing is staged or committed, and the history says so.
- Each target is compared with its base again as it is set aside, just before the new
file is renamed in, and once more after every rename. A save that lands after the stale
check (while the show validates, say) is put back, and the change becomes
Undo proposes and applies the inverse change set (
inverseOf). History is never rewritten. Redo re-applies an undone change if its bases still match.- The exact bytes of every file a change replaces are kept in
.reelwright/changes/objects/, named by their SHA-256 (baseBytes) and checked against it whenever they are written or read; a damaged copy is refused, never restored. Undo writes them back byte for byte (bytes), so a hand-formatted file comes back as it was, and removes the folders the change created (createdDirs) if they are empty.
- The exact bytes of every file a change replaces are kept in
Statuses say what is on disk. A change and its undo changes form a family, and at most one member is in effect: whenever a member is applied, any other member that was applied becomes
reverted.approvedis transient: it is recorded in history between the decision and the write, never kept as a status. Every sequence of up to four approvals, undos and redos of two changes is tested against disk (core/test/change-store.test.ts, "ChangeStore statuses").Status On disk approve reject undo redo proposednothing written writes it: applied(variation siblingsrejected); bases moved:stalerejectedrefused its rejection was undone: rejectedagain; otherwise refusedstalenothing written refused rejectedrefused its rejection was undone: rejectedagain; otherwise refusedrejectednothing written refused refused the person's own rejection: it waits again, proposed, orstalewhen its files moved since (notedrejection undone); one rejected by another decision (an option chosen, a revision approved): refused, since undoing that decision is what takes it backrefused appliedits edit is in effect refused refused a change: a new undo change is applied and it becomes reverted; an undo change: the change it undid is re-applied (applied) and it becomesreverted. Refused if its files changed since.refused revertedits edit is not in effect refused refused refused re-applied if its bases still match: applied, and the family member that was applied becomesreverted; otherwise refused and stillrevertedVariation groups:
varyandgenerate_characters --options Ncreate N proposals sharing avariationGroup. Approving one rejects its siblings.Revisions (M7): a proposal may name one still waiting that it
supersedes(only a proposed or stale one). Approving it rejects that one and whatever it superseded in turn, each notedsuperseded by <id>; until then the person can choose any of them. A proposal that names none supersedes the newest one still waiting by the same author whose files it all writes (a second screenplay draft, a set built again), since approving it would leave that one stale; a variation, an undo or a lock never does.direct_episodeandedit_episodename the newest proposal still waiting for the same episode file, so re-directing an episode leaves one live proposal, not a pile.Locks:
lockandunlockare change sets too, editingmeta.locksormeta.status, so they are undoable. Any proposal that would modify a locked pointer is refused at proposal time with the lock named. They are the only changes that may lock or unlock: any other proposal that would add, remove or editmeta.locks, or makemeta.statusbecome or stop beinglocked, is refused (change.lock_edit), so no tool can unlock a pointer in one proposal and change it in the next.
Versions (core/src/studio/versions.ts)
The product audit's #20: a show had no named versions, no restore points and no show-wide history, and Ctrl+Z covered one session.
- A version is what every document and script of the show held at a moment: their bytes,
kept by SHA-256 in
.reelwright/changes/objects/(the copies undo writes back, so a file kept by both is kept once), and a record in.reelwright/versions/<id>.jsonwith a name, a note, who saved it (the web service's person) and when. Comments are talk about the show: no version keeps or restores them. Recorded takes and assets are named by their own hash and never written over, so the documents that name them are enough. Nothing needs git: a show with no git identity, or a machine with no git, has versions too. The version also keeps the newest still the show's previews hold, as a picture of it then (no render is made for it). - Comparing two versions, or one and now (
versions.diff), lists every document or script added, removed or changed, with its field count; asked for one document, its fields pointer by pointer (as a proposal's are) and a script's lines in stretches. Formatting alone is no difference: JSON documents compare by what they hold. - Restoring is a change set like any other (
restore_version): the files that differ are written back byte for byte, and restoring the whole show removes documents made since. The person's own (Studio's tools.call) is applied as made, in History and undone with Ctrl+Z; the showrunner's, or an assistant's through MCP, waits as a proposal. Locks hold: a restored document keeps the locks it has now, and a locked field that would change refuses the restore with the lock named. Nothing to restore is refused (version.same). - Restore points are versions the ChangeStore takes by itself just before it writes a
risky change: a restyle, an OTIO import, one that removes a file, or a bulk edit (three
files or more, or twenty fields of one). An undo or a restore puts things back and takes
none. The change's history says
restore point <id> taken first. The newest 20 are kept; named versions stay. A store that cannot take one says so in the history and the change goes ahead. - Roles:
versions.listandversions.diffread, so a viewer or a reviewer browses and compares;versions.saveandrestore_versionwrite, for the owner and editors.version.eventtells everyone in the show of a version saved, with who saved it. - Tools:
list_versionsandcompare_versions(read) andrestore_version(propose) serve the showrunner (Studio's chat andreelwright chat) and MCP. - Ctrl+Z across sessions is Studio's: its stack of the person's own changes is kept with
the show's place (
RwProjectState, per person on the web service, in the browser), the newest 100 applied and undone, so it outlasts a restart. Ctrl+Z goes past a change that can no longer be undone (undone by someone else since, or overtaken) to the one before it.
Dependency graph (core/src/studio/graph.ts)
Nodes are documents; edges come from references and meta.derivation: pattern → instance,
style → everything rendered, set → episodes, character → episodes.
impact(id)lists what re-derives or re-renders ifidchanges.statuslists instances whose pattern hash drifted, and locked characters whose model sheet no longer matches the approved one (canon drift, ADR-0007). Approved model sheets are stored by hash in.reelwright/approved/.
Previews (core/src/studio/previews.ts)
- Previews run through engine jobs and are cached by (the jobs as the engine draws them,
engine fingerprint, kind) in
<show>/.reelwright/previews/<key>/. A job embeds only the documents it draws, so editing another one keeps the preview; stills key on the compiled Timeline, so a compiler change that moves a frame renders again. Captions and document ids come from the request. One engine job runs at a time per service; the rest queue, and an AbortSignal drops a queued request or kills its job. - Kinds:
- model sheet (existing task);
- line-up: the model sheet's line-up panel with the whole cast (existing task);
- set views (existing);
- storyboard stills (existing);
- style swatch: a new engine task,
style_swatch, that renders a fixed sample (a character bust, a set corner, sparks) in a given style, so a style can be judged before any show uses it.
- Previews are files. Surfaces reference them by path over the session protocol, or return them as MCP image content (downscaled JPEG).
Showrunner agent (core/src/showrunner/)
- Provider interface (
LlmProvider): the Anthropic implementation uses the official@anthropic-ai/sdk.- Defaults:
claude-opus-5-5;- adaptive thinking;
- streaming;
- per-stage
effortfromshow.json; - no forced
tool_choice(current models reject it): tools arestrict: trueand the prompt names the tool; - server-side refusal fallbacks enabled.
- The key comes from
ANTHROPIC_API_KEY, then a key file ($REELWRIGHT_ANTHROPIC_KEY_FILE, else${XDG_CONFIG_HOME:-~/.config}/reelwright/anthropic-api-key, refused unless only its owner can read it), then the macOS Keychain (security find-generic-password -s reelwright -a anthropic -w), then the SDK's own chain. It is never logged or written, except byreelwright key set, which saves a key to the key file (folder 0700, file 0600, written whole) after asking Anthropic whether it works with a free models.list call;key statussays where the key in use comes from andkey forgetremoves the file. Output names a key only assk-ant-…and its last four characters. Studio's "Connect Claude" dialog runskey set --check --jsonwith the key on stdin, never on a command line, then starts the show's daemon again. Its other way, the person's own Claude plan through the Claude Code on their computer, is Using a Claude plan. A Claude subscription's sign-in token (sk-ant-oat…) is refused on this path wherever it turns up, with the way to use a plan. - A daemon whose showrunner has no credentials keeps it for dry runs, but leaves
chatout ofsession.hello's capabilities (Showrunner.canTalk), so a UI offers to connect Claude instead of letting a first turn fail. - A stream that breaks partway (malformed tool input, a dropped connection) is sent again at most twice. Each attempt is planned against the budget and billed, and the surface never sees the same text twice.
- Defaults:
- Loop: user turn → model → tool calls (run in parallel when read-only) → tool results → … until the model ends its turn. Proposals stream to the surface as they appear.
- Repair. Tool input that fails schema or semantic validation returns an
is_errortool result with the diagnostics (pointer + hint). Schema failures are repairs: at most 3 per tool, counted once per model call however many of its calls failed; then the tool fails visibly. A handler's own failure (an unknown id) is an ordinary error result. - Unfinished turns. A turn that is cancelled, refused or cut off at
max_tokensis taken back out of the conversation, so the next turn neither inherits the abandoned request nor shows a declined one to the classifiers again. Proposals it made reach the next turn as show updates. Nothing before it is edited. - Context and caching.
- Cached prefix, in this order: the tools in view, then the system prompt (role, rules,
tone), the names of the tools found by search, then the show context block. Both
breakpoints (the rules, the show context) are kept an hour (
cache_control.ttl: "1h",SHOWRUNNER_CACHE_TTL): writing the prefix for an hour costs 2x its input price instead of 1.25x, once, and a person's next message seldom comes within five minutes. On the demo the first message writes its ~9,100 tokens for $0.07 and each one within the hour reads them for $0.002 (with every tool in view, $1.03 and $0.03). The show context holds the theme, a style summary, the bible, a cast digest (ids, names, roles, one line each) andshowrunner/NOTES.md. - Everything volatile (the conversation, fresh tool results) comes after the last
cache_controlbreakpoint. - Long context plus caching replaces retrieval until a show outgrows it.
- Cached prefix, in this order: the tools in view, then the system prompt (role, rules,
tone), the names of the tools found by search, then the show context block. Both
breakpoints (the rules, the show context) are kept an hour (
- Where the person is (
core/src/showrunner/page.ts,brief.ts,suggestions.ts). Studio says where the person is with each message and as they move (ChatContext): a page path (StudioPage:world/theme.marigold,cast/char.otto,sets/set.marigold_bridge/setups/wide,timeline/ep.x,script/<path>,memory/gags,change/<id>), its title ("World › Theme") and what is open, selected and in view there.resolvePageturns it into one of twenty page kinds against the show's documents (the Show, its settings, World, the theme, a style, the bible, Cast, a character, a pattern, Sets, a set, a prop, an effect, a graphic, the timeline, Episodes, a script, Memory, a proposal, the booth); an old context (a document, the timeline's episode, a script) still resolves.- The page brief is said before the person's message as a
<studio-focus>block: the page and what is open, what Studio says is in view, facts about the page drawn compactly from the show (a theme's premise, tone, rules and catalog; a style's palette and settings; a character's card; a set's marks, setups, lighting and elements, the part in view first and whole; the episode at the playhead with its shot and lines; a memory entry; a proposal's diff), the tools that act there (only those the registry has), and how to link a place ([Open the theme](reelwright:world/theme.marigold)). It is deterministic (core/test/fixtures/page-briefs/snapshots each kind on the demo show) and within 1,200 tokens: facts are cut last first, ending with how to read the rest; the person's selected lines are never cut. It is part of the turn, not the cached prefix, so the system prompt, the show context and the recorded fixtures are unchanged, and the API's Showrunner and the plan's PlanChat say it alike. - Suggestions for each page, three to five, in the show's own words ("Give Otto a winter costume.", "Tighten sh020.", "Try a warmer variant of Saturday Morning.", "Build the show's first set." on an empty Sets), with no model; where no page applies, the show-level ones Studio always offered.
- Answers that take you there: after a turn's proposal the daemon sends
studio.navigatewith the place it changes (pageOfChange: the document's page, a new document's section, a script, Memory), and after the reply each place it linked that the show has. Studio shows a proposal's as a button on its card, and a linked place as a link in the words.
- Memory.
showrunner/NOTES.mdin the Show Pack holds durable preferences. The agent proposes edits to it like any other document. Transcripts go to.reelwright/sessions/<id>.jsonl. - Budgets.
show.jsonbudgetcaps spend per request, per episode and per month.- Before every call the agent counts tokens and prices the worst case: the whole prompt
as cache writes (at the rate of how long it is kept) and all of
max_tokens. With the refusal fallback on, each model of the chain may bill both in turn, so a call goes withfallbacks: "default"only while the whole chain's worst case leaves room for 4,096 output tokens. Otherwise it goes to its one model without the fallback and is priced on that model alone: a refusal stays a refusal, never a bill past the cap. It cutsmax_tokensto fit, or stops with a clear message. - The demo caps a request at $3 (
examples/shows/marigold/show.json): a first turn of three calls costs about $0.13, and the whole fallback chain fits. Its $2 cap once stopped the first message before it was sent, when every tool was in view: the chain's worst case was $2.54. - The monthly ledger (
.reelwright/spend/<YYYY-MM>.jsonl) is shared by every process on the show. Each check re-reads it, and a call in flight holds its worst case there until its real cost replaces it. --dry-runshows the plan and the estimate without calling the model: the tools in view and those found by search, the strict schemas against the limits, the prompt's tokens (counting only what is in view, as Claude sees it) and how many are the cached prefix, what turn 1 costs to write it, what a turn within the hour costs to read it, and whether the first call fits the caps.- The CostMeter records usage (input, cache read and write, output) from each billed attempt, including cancelled and broken streams.
- Recorded fixtures (tests never need a key).
RecordingProviderandReplayProviderkey each call by SHA-256 of the canonical request, excluding volatile fields. Fixtures live incore/test/fixtures/llm/.- CI uses replay only, and a missing fixture fails with the command to record it.
- Recording is explicit (
reelwright fixtures record <scenario>) and spends real money, so it needs a key and a budget.
Session protocol (schemas/v1/session.schema.json)
JSON-RPC 2.0 over a localhost WebSocket. The daemon (reelwright serve --show <dir>) prints
{"port":…,"token":…} on stdout. Clients send the token in session.hello; before it
nothing else is answered and batches are refused. Upgrades whose Origin is not a loopback
one (a web page) are refused with 403.
| Method | Purpose |
|---|---|
session.hello |
Authenticate; returns show summary, capabilities |
session.place |
Where the person is in Studio (section, episode, document, screenplay and caret line, timeline frame and shot, editing: the script open has unsaved changes), for the others in a shared show: session.people carries each person's place and colour, at most every 250 ms; screenplays.get answers editing, the others with unsaved changes in that script. shared: false on the desktop (docs/design/m13-shared-shows.md) |
chat.send |
User message, with where the person is in Studio (context, a ChatContext: the page, its title, the document and part in view, the episode, frame, shot and what is selected on the timeline, the script and its selected lines, a memory group or entry, a proposal, a view of the stage's camera, and a short summary of what is in view), said to the model as the page brief (see "Where the person is") → streams chat.event notifications (text deltas, tool calls, proposals, errors, done) |
chat.focus |
Where the person is now (a ChatContext), sent by Studio as they move, once a place has held for 0.4 s. Answers 3 to 5 things to ask there (suggestions, drawn from the show without a model; none to a viewer), and keeps the place in .reelwright/studio/focus.json (focus-<person>.json on the web service) for Claude Code's studio_context; the daemon takes it away when it stops. Read class |
chat.cancel |
Cancel the running turn |
chat.ask / chat.unask |
With no Claude for the chat itself, what the person asks goes to their own Claude elsewhere (protocol 1.19): the ask waits in .reelwright/studio/chat-asks/ with where they were, their newest in place of one still waiting, until their Claude takes it (studio_context) and answers (answer_in_studio, an agent.event). Answers the ask's id and the line Studio opens claude.ai with (and copies). chat.unask takes it back, the asker's own only. Write class |
agent.activity |
An AI's own link (the web service's, for claude.ai's connector or Claude Code) says what it did: a call, its result, a change it proposed, a resource it read (AgentActivity, protocol 1.19). Refused on any other connection (session.not_agent). The daemon tells the person's Studio as agent.event (docs/design/showrunner-connected.md) |
changes.list / changes.get |
Proposals and history, with diffs and previews |
changes.approve / changes.reject / changes.undo / changes.redo |
The human's decisions |
changes.lock / changes.unlock |
Lock or unlock pointers (as change sets) |
versions.list / versions.diff |
The show's versions, newest first, and what differs between two (or one and now), a document field by field (see "Versions"). Read class |
versions.save |
Keeps the show as it is now under a name and a note; version.event tells everyone in the show. Write class |
docs.list / docs.get |
Read documents |
docs.form |
Read class: a document as a form, from its JSON schema (core/src/studio/form.ts): every field in schema order with the widget that edits it (number or slider with its range and the unit its name says, vector, choice from an enum, switch, color or palette slot, text, long text, a list of words, a pick of the show's ids its pattern takes), a heading for each object and list item, an add for each list and each missing field with a template that validates, which fields are read only (ids, provenance, derivation), and the tool its edits go through (revise_*, write_bible_entry; none for show.json and episodes, changed in Settings and on the timeline) |
sounds.list / sounds.preview / sounds.import |
Read, read and write class: the show's sounds, recipes and recorded files, each with how many documents and scripts use it; a 16-bit WAV of one to play (a recipe synthesized, a file decoded) and its picture; a file sent with takes.upload purpose: "sound" (up to 256 MiB) kept as `audio/<sfx |
previews.get |
Request a preview (returns a path when ready; progress via preview.event) |
graph.impact / graph.status |
Dependency answers |
episode.timeline |
An episode's compiled Timeline and EpisodeIR, or a proposal's (M7) |
episode.playback |
An episode as Studio plays it live: a frames job file at 540 px and its sound as WAV (M7) |
waveforms.get |
The waveforms the timeline draws on its clips: each sound's peak level a hundredth of a second, one byte a peak, made from its samples once and kept in the show's cache by hash |
episode.voice |
Voices an episode's lines that have no take, with the show's voices (Kokoro on this computer, free) into the take store; no document changes; one at a time |
takes.list / takes.upload / takes.add / takes.choose / takes.delete / takes.edit |
The booth (docs/design/m6-recording.md): an episode's lines and whether actors recorded them; a line's prompter and takes (recordings and synthetic, the chosen one, length, peak, a WAV and peaks to draw); a recording sent in base64 pieces of at most 700,000 characters and kept as the line's take in audio/takes/ (refused with why when it is no 16-bit PCM WAV at 44.1 or 48 kHz, 0.3 to 120 s, or silent); the take a line plays (a change set, applied as made); a recording deleted; a trimmed, gained, normalised or cleaned version made from the recording. episode.playback take plays one not chosen; episode.timeline recorded names the recorded lines |
screenplays.list / screenplays.get |
The show's .fountain files, each with the episode directed from it (or none yet), its title and scene count; one's text, for the script editor |
episodes.status |
Every episode and how far it has got, for Studio's Show section ((implementation) core/src/studio/episodes.ts): directed ones (an EpisodeIR) first, then outlines and scripts not directed yet. Each says its stage (outline, script, directed), title, outline and logline, script, scenes; once directed, its lines, how many have a take (voiced: an actor's recording or a voice made for it) and how many an actor's recording, its running time, frames and fps, and stillFrame, late in its first shot, past the fade in, for a thumbnail. needs: what it lacks from the show, each with the one tools.call that makes it when there is one (make): before directing, the missing-asset pass's characters, sets and props (episode_needs; a sound a script names, create_sound from the word); after, the sounds its cues play that the show has neither a recording nor a recipe for (create_sound), and problem when it does not compile. Directed episodes compile without lip sync and without writing their timeline (about 0.3 s each warm); the answer is kept until a file of the show or the take store changes. Nothing renders |
screenplays.read |
A screenplay's text, saved or not, read by the heuristic director without shots or a compile (the show loaded once, again only when one of its files changes; about 75 ms for a 70 s episode): each scene's heading, set name and opening, each action paragraph and each line with business under it (its first and last line), what is staged there in the show's own words ("Otto walks up to the movie screen"), and what it could not act on, by line, with the rest of the notes apart. Studio's script editor shows it beside the text. With it come the show's words (screenplayWords): each character's cue in capitals, each set as a heading names it with the times its lighting answers to, each prop as an action names it, and the noise each sound effect makes; the editor completes them as the person types. A note can carry fixes (fixesFor, core/src/studio/fixes.ts): a cue or heading that names nobody becomes the cast member or set it most likely meant ("Use CHIEF", a change to the line's words), and a sound word the show lacks becomes the nearest sound it has with the word's ending ("Use CLANKS") or a sound made from the word itself (soundRecipeFor, core/src/audio/word-sounds.ts: its family, a ring, beep, metal, thud, air, hum, something wet, a zap or a pop, picks the recipe and the word seeds its pitch and noise), run through create_sound as the person's own change. |
screenplays.import |
A script file the person chose, as its name and its bytes (base64, up to 2 MB; never a path, so a browser's file picker and a desktop dialog send the same): Final Draft (.fdx, or any file of Final Draft's XML) converted to Fountain (core/src/screenplay/fdx.ts: scene headings with their numbers, action, cues with their extensions, parentheticals, dialogue, lyrics, transitions; Shot, General and Cast List as action; New Act and End of Act centered; outline elements as sections and synopses; dual dialogue as a ^ pair; bold, italic and underlined runs as emphasis however the runs were split; StartsNewPage as ===; capitals where the file's element settings say; the title page read by where its paragraphs sit), or Fountain (.fountain, .txt) as written with its line endings made plain. UTF-8, UTF-16 with its mark or Windows-1252. Returns the Fountain, a title (the title page's, else from the file's name), its scene count and what did not come across ("1 “Montage” paragraph became action."). Writes nothing: Studio opens it as a new episode's script, as New episode does, and Save writes it as the person's own change. screenplay.empty, screenplay.not_text, screenplay.not_fdx, screenplay.unsupported (a .pdf, a .docx) and screenplay.too_large say why in words for the person |
storyboards.export |
An episode's storyboard as a PDF's bytes (base64), the same result as screenplays.export (lighthouse_cold_open_storyboard.pdf): US Letter on its side, six panels a page in cut order, each the shot's middle frame (previews kind panels, drawn first when they are not cached) with "sc01 · sh020 · MCU", the lines that start in it and the business, in Courier (core/src/studio/storyboard.ts: the panels from the compiled cut as episode.timeline gives it, and a writer for pictures, each scaled to its panel and compressed with Node's zlib; the same panels and pictures give the same bytes). A shot with no still is an empty box, and warnings say so; without an engine every box is empty. Pictures are drawn smaller when the file would pass about 5.5 MB |
screenplays.export |
A screenplay's text as the editor has it (saved or not) as a file's bytes (base64), with a name from the script's (lighthouse.pdf): pdf, an industry-format screenplay (core/src/screenplay/pages.ts over a small writer, pdf.ts: US Letter, base-14 Courier 12 with nothing embedded, margins 1.5" left and 1" right, top and bottom, cues at 3.7", parentheticals at 3.1", dialogue at 2.5" 3.5" wide, transitions against the right margin, scene numbers both sides, dual dialogue side by side, page numbers from page 2, a speech broken at a sentence with (MORE) and the speaker's (CONT'D), never a scene heading alone at a page's foot, a title page from the script's own fields or else the show's and the episode's titles; no clock in it, so the same text gives the same bytes); fdx, Final Draft, which screenplays.import reads back to the same Fountain (its title page names its Fountain keys in a comment Final Draft ignores); fountain, the text itself. pages counts a PDF's script pages; warnings say what the format has no place for (notes and boneyard in Final Draft) |
episode.timeline actions |
With the compiled timeline, what each character does and what happens on the set, with its frames from the compile and in the show's own words (episodeActions): Studio's timeline draws them as Action lanes. |
episode.timeline screenplay |
For an episode written from a screenplay: each action paragraph and line with its lines in the script, its frames, and the cues it became (episodeScript), a paragraph found by its words in order so one rewritten since is left out. Studio links the script and the timeline both ways through it: the script editor's Timeline goes to where the caret's paragraph plays, and a double-click on a line, a sound or someone's business opens the script at its words. |
assist.write |
The script editor's writing assistant (M5, "Writing with Claude" in docs/design/m5-writers-room.md): the editor's text as before + selection + after (so no offsets), an action (continue, rewrite, punch_up, shorten, business, line for a character, scene), instructions and settings (tone, length, boldness, house voice). One streamed model call with the show as context, its text as assist.event; the suggestion read in place by the heuristic director and revised once when it could not stage it. Returns the Fountain to put in (joined with blank lines), what the director stages for each of its paragraphs and lines and its notes (lines counted from the suggestion's first), revised, usd and model. assist.no_model without a model or key, assist.budget when show.json's budget has no room |
assist.cancel |
Stops an assist.write by its client-chosen id: the model call is aborted and the write answers assist.cancelled |
tools.call |
Run a read or propose tool directly, as the timeline does (M7). The script editor's Save and Direct are write_screenplay and direct_episode with base (and baseText, the text it started from): a save over a newer script is merged with it line by line and answers merged (the text written, whose changes came in), or, where both changed the same lines, is refused with screenplay.conflict and error.data.conflict (docs/design/m13-shared-shows.md). The person's own edit, applied as made; with author: "showrunner" it is an assistant's call instead (Claude through the web service's MCP endpoint, docs/design/m12-remote-mcp.md), whose proposals wait for review |
tools.list |
Every showrunner tool as an assistant is shown it: name, title, kind (read, propose, decide), description and whole input schema (JSON text). The web service's MCP endpoint lists these |
show.get / show.update |
Read class and write class: Studio's Show settings (core/src/studio/show-settings.ts): the show's title, logline (show.json's meta.description), cover, active style and the show's styles, its theme's name, premise, genres and setting, the active style's shared colors, the format full renders are made at, and its voices; and the person's edits to any of them, applied at once as their own change set (show_settings, author user) that Ctrl+Z undoes. Renaming renames the theme too while it still goes by the show's old name. Nothing that is so already makes a change. A size that is not even is refused (H.264). |
show.cover |
Write class: the cover picture, from an image sent with takes.upload (PNG or JPEG, up to 12 MiB) or one of the show's own preview files (an episode's still): frozen in assets/<sha256>.<ext> with its provenance (license NOASSERTION), and named by show.json's cover, as the person's change |
show.restyle |
Write class: a proposal (not applied) to switch to another of the show's styles (restyle_show), so the person looks at its restyle preview (previews.get kind restyle) before approving it |
show.storage / show.clean |
Read class and write class: the room the show takes by part (its files, takes, assets, history, renders, previews, caches), and making room: always what reelwright clean removes, renders and previews when asked; never documents, takes, assets or history; not renders while one is being made |
renders.list / renders.start / renders.cancel |
Whole-episode renders: those on disk (and the one under way), each with its version (renders/<slug>/v<N>/, docs/design/review-loop.md) and whether it is the approved one; add one to the show's render queue (preview 1280x720 or full; ahead says how many run or wait before it), stop one, running or waiting; progress via render.event |
renders.queue / renders.move / renders.pause / renders.resume / renders.clear |
The render queue (docs/design/render-queue.md): every render asked for, in order, one at a time (read); move one, pause one or the whole queue (the running render keeps its frames), resume one (a failed or cancelled one tried again, from the frames it kept) or the queue, take one or every ended one off the list (write). Each answers with the queue; render.queue says it to everyone |
renders.approve / renders.changes / renders.review |
The review loop (docs/design/review-loop.md): mark a version the episode's approved one (write); what changed between two versions, the script line by line, the director's scenes, every other document (read); a render's review copy for Studio's player, its frames as JPEGs read by byte range, their levels and its sound (read) |
comments.list |
The show's comment threads (comments/<slug>.thread.json, docs/design/m13-shared-shows.md), the most recently active first, filtered by status, mine, mentioning (me) and anchor (type, and target: a document id, a script's path, an episode); each script thread with its place now (here, moved, edited, lost); and people, whom a comment may @mention. Read class |
comments.create / comments.reply / comments.edit / comments.delete / comments.resolve / comments.reopen |
Comment class (a reviewer may): a thread on a script's lines (from, to, or the editor's own quote), a timeline frame or shot (a note on a render: its version, a range to, marks drawing), a document or the show; replies; edits (your own); deletes (your own, or any as the owner; a whole thread when all its comments are yours); resolve and reopen. Each says comment.event to everyone in the show; mentions and replies are notices for the web service (DaemonOptions.notify) |
comments.export |
An episode's notes on its renders, as a ZIP of a CSV with timecodes and thumbnails, or a PDF with each note's frame and marks beside its thread, in renders/<slug>/notes/ (read class) |
Server notifications: chat.event, change.event (proposed, applied, rejected, stale; once
applied, with note, who did what in a sentence: "Ada edited the Lighthouse's script."),
doc.event (changed on disk, including edits made outside Reelwright, detected by
watching files), preview.event, render.event (a render's stage as it starts, the frames
drawn every second while it draws them with the speed and the time left, then done with its
video and how long it took, failed with why in words and what to do, cancelled, or paused),
render.queue (the render queue whenever it changes), agent.event (what an AI working on the
show through Reelwright did, live: from its link's agent.activity, or the recent of a
reelwright mcp's presence file, to the Studios of the person it works for that say 1.19), chat.asks (what the person asked their own Claude from the chat, waiting or taken and by which client, to their Studios that say 1.19, when it changes), assist.event (an assist.write's text as it is written, revising when it starts
again, done when it ends), comment.event (a thread created, replied to, edited, deleted,
resolved or reopened, by whom, and the thread as it is now), studio.navigate (a place a
chat turn's answer points at: label, page, frame or line, its turnId, and change
when it is where a proposal lives), request.working (the id of a request the daemon still
works on, every 20 seconds until its answer goes, to a Studio that says 1.23: long work, such as
the style lab's directions from Claude Code on the person's plan, waits as long as it is said,
RwSessionClient.request_long). A file that cannot be read (its permissions, say) is
still a doc.event, and a diagnostic (show.unreadable_file) when the show is read;
nothing the watcher meets ever stops it or the process.
Every message has a schema. The core validates both directions. The Godot client uses the
generated RwSpecSession* classes.
UI (ADR-0011): engine/addons/reelwright/ui/
Plain Control scenes, with no editor APIs, talking only through RwSessionClient
(WebSocketPeer + JSON-RPC):
RwChatPanel: the conversation, streaming text, tool activity, inline proposal cards. (implementation) Its header says where the person is ("Looking at: World › Theme"), with "Ask about the whole show" to leave the place out of what is sent (and "Use this page" to put it back; moving elsewhere puts it back too). It asks its host where the person is four times a second and tells the daemon (chat.focus) once a place has held for 0.4 s, the playhead's frame and what is in view aside, so a playhead running through shots is told once it stops; the chips are the answer, in the empty chat and, the first three, over the message box under a conversation. Astudio.navigatebecomes a button that goes there (on a proposal's card when it is the proposal's place), a[label](reelwright:<page>)in a reply a link, and both are kept with the thread.RwPlace(implementation): where the person is has one source, Studio's route (RwStudioRoute, what Back and Forward go through): the page path is its parts after the show (RwStudioRoute.page_of). Every view that fills the main area adds what it alone knows (chat_place(): the title, what is open, selected and in view, anddeeperparts below the route: a document's part at the top of its scroll, a memory group or entry), and finds them again (show_place(deeper, frame, line)).RwStudio.chat_contextputs the route and the view's words together;RwStudio.go_to_placegoes to the route a page path starts with (RwStudioRoute.of_page, throughgo_to_route, so Back returns from it) and hands the view the rest. The views: the Show section, World, Cast and Sets (the browser and its document), the timeline, Episodes, the script editor, Memory, Settings, a proposal and the booth. A new view (a set explorer, a character studio) adds its places, a camera included, by answering the two, with no switch to change.RwProposalView: title, rationale, a per-document diff (readable field-level, not raw JSON), previews, approve/reject, lock toggles. Variation groups show side by side. Each card reads as what changes for a person: the document by name, ids in values by name (RwValueFormat.id_words), no JSON pointer under a field (in its name's tooltip); what a document keeps for itself (kind, schema version, id, its meta but for description, tags and notes) folds under the card's Details with its id and file (RwProposalView.is_bookkeeping), and a commit's hash is only in the status line's tooltip.RwPreviewGallery: model sheets, set views, style swatches; zoom.RwShowOverview(implementation): Studio's Show section, the first thing a showrunner sees. The show's poster (its cast as a line-up,previews.get lineupon the show; with no cast, a frame that says the cast lines up there), title and premise (the theme's: "Borrowed from the … theme" with Write your own when the theme is the kit's), its counts, and New episode, Write with Claude, Render… and Record lines. Next steps: what the show lacks, each with the one action that deals with it, in order: a first character and a first set, the showrunner's proposals waiting (a variation group as one), each decided on its row without opening it: what it changes in a line (changes.get: the document and the field, its new value when short), who proposed it and when, then Approve, Reject and Review. Approve is held, the row saying why, for options to choose between (Reject rejects them all), for one gone stale (its drift folded into its row) and for one proposed with warnings ("2 warnings: look first":diagnosticsonchanges.get, session protocol 1.16, which Review lists under Before you decide, a kind of note as one card saying how many more are like it, warnings first, past the first two kinds folded behind "N more notes" so the change stays on the first screen; the chat's proposal card holds its Approve the same way); Ctrl+Enter and Ctrl+Backspace (changes.approve,changes.rejectin the key map) decide the row with the focus, which then goes on to the next one waiting. A decided row stays where it was, as tall as it was, until the page is left ("Approved · …" or "Rejected · …" with Undo, "Taken back · …" with Redo, followingchange.event), so nothing under the pointer moves up into its place. Each decision is the person's own, as in Review: its row's Undo is Studio's Ctrl+Z for that change (RwStudio.undo_change,redo_change), and Ctrl+Z takes an approval back and brings a rejection back. Then what an episode needs (Make it runs itsmakecall as the person's own change), an episode that does not play, drift, each episode's next step (write the outline's script, direct the script, voice its silent lines withepisode.voice, finish recording, render a preview), memory updates to review and episodes not in memory (both open Memory); six, then Show N more; All caught up when nothing waits. Episodes as cards: a still atstillFrame(previews.get still), the running time, scenes and lines, and how far it has got (Outline, Script, Directed, Voiced, Recorded N/M, Rendering, Rendered, Does not play); a card opens the timeline, the script, or starts the outline's script. The latest decisions on the showrunner's proposals, each with its picture (changes.get). Every part is asked at once and drawn as its answer comes, a placeholder until then; it refreshes onchange.event,doc.event,render.eventandmemory.event, and asks a still again only for an episode a change touched. With no episodes, its Episodes part offers Write your first episode.RwEmptyState(implementation): what a place holds while it is empty and the action that fills it: Cast (Create your first character, or with no pattern Ask the showrunner), Sets (Build your first set), Episodes (Write your first episode), the Timeline with no episode (Go to Episodes, New episode), the booth on an episode with no lines (Open the script, Back). "Ask the showrunner" puts words in the chat's message box for the person to send (RwChatPanel.prefill), or offers Connect Claude with no model; on the web with no showrunner there is no such button. A host that does not answer a view's button gets none.RwCoachMarks(implementation): Studio's first-run tour, five marks shown one at a time (the rail, writing a script with its read-along and Write with Claude, the timeline's inspector, the showrunner, Ctrl+K and Ctrl+/), each outlining what it is about with its card beside it; Next, Done, Skip tour. It takes no clicks but its card's and never the focus. Shown the first time Studio opens a show (RwUiSettings.tour_seen,[tour] seen, kept when it starts), again from the palette ("Show the tour again") or the foot of the shortcut sheet. Only the app turns it on by itself (RwStudio.tour_on_first_open): tests and review tools host Studio without it.RwShowBrowser: theme, style, cast, sets, props, bible, with lock state and drift badges. Opening a document reads it withdocs.formintoRwDocumentView, where every field is edited by hand (RwFieldForm, through the document's revise tool) and its locks are set. Each row's ⋯ renames, duplicates or deletes it (RwDocActions); "More" makes a prop, an effect, a bible entry of any kind, or a copy of a pattern, fromlibrary/templates/. (implementation) Cast has "New character" and Sets "New set" (RwCreateDialog): pick one of the show's patterns, how many (characters) or what scene headings call it (a set), andgenerate_charactersorbuild_setruns throughtools.callwith the seed one more than how many the show has, so each is new, and no model; the person's own change, applied as made, Ctrl+Z to undo, and the first one made opens.RwEpisodesView: each episode with its latest render and preview, to watch in the system's player or find in its folder; "Render preview" and "Render" start one (renders.start), whose stage, frames, time left and Cancel show on the episode while it runs. Renders wait their turn in the show's render queue, shown over the list (RwRenderQueue, docs/design/render-queue.md), and run one at a time in the daemon (core/src/studio/renders.ts), each into a version of its own (docs/design/review-loop.md). (implementation) "Script" opens an episode's screenplay inRwScriptEditor; screenplays no episode was directed from yet are listed too, "not directed yet" (the demo's Salvage Claim had never shown anywhere); "New episode" asks for a title and startsepisodes/<slug>/<slug>.fountainwith the show's first set and two of its cast. An outline from the writers' room whose episode's folder has no script yet is listed as well, with "Write the script", which starts<folder>/<folder name>.fountain.RwScriptEditor(ui/script/): the screenplay in a code editor, Fountain colored as it is typed byRwFountainHighlighter, which reads lines as the core's parser does. Save (Ctrl+S) iswrite_screenplay; Direct (Ctrl+Enter) isdirect_episodewithheuristicand the text: the heuristic director, free and with no model. Both go throughtools.call, so they are the person's own changes, applied as made and undone with Ctrl+Z; then "Timeline" shows the episode, to play. A Studio audit found no way to write a script in Studio: only the showrunner could, and only with an API key.RwBooth(ui/booth/, docs/design/m6-recording.md): actors record an episode's lines. A prompter, a count-in, Record and Stop (Space) with a live input meter, the line's takes with waveforms to play alone or against the picture, choose, trim, gain, normalise, clean or delete, and Previous and Next through a character's lines. Opened from a line in the timeline's inspector ("Record a take", "Takes…") or an episode's "Record lines…".RwUiTheme: the one Theme (dark, Inter, a 2 px focus ring on everything focusable).RwShowSession: what a host owns. It runs the daemon (RwDaemonSupervisor: reads the endpoint line, restarts with backoff, stops only its own child) and holds the oneRwSessionClientevery component binds to.RwSessionStatussays how it is doing.switch_tomoves it to another show and keeps the one left running in itsRwDaemonPool: the daemon, and the open socket itself (RwSessionClient.hand_over), so what that show says while away (a render done, a chat turn's events, proposals) is heard, and switching back hands the same socket back with no new start and no new hello. When the socket comes back for the same show (after a drop, the daemon started again, or a first hello that came after a view had asked), the client saysreconnectedand every open view reads its document again (itsreload): a document, a set, a character, a style, a script (unsaved text kept), a proposal, the booth's line; the timeline, memory and settings read the show again on every hello. Another show's hello, or a socket handed over, reloads nothing.
Hosts:
- Reelwright Studio (
engine/app/studio.tscn): a standalone window that startsreelwright serveand supervises it.- Launch:
tools/studio.sh [--show <dir>] [--daemon "<command>"], which runs the scene in the engine project (godot --path engine res://app/studio.tscn -- …). The project's main scene stays the engine CLI, which renders depend on. An exported Studio will set its own main scene per export preset once export templates join the toolchain. - Daemon command:
node <repo>/core/src/cli/main.ts servein a checkout, elsereelwright serve;--daemonor the welcome screen changes it, and Studio appends--show <dir>. Recent shows and the command live inuser://reelwright-ui.cfg, shared with the editor plugin. - Switching shows ((implementation)
RwProjectSwitcher,RwProjectState,RwDaemonPool): Ctrl+Shift+O, the show at the top of the rail, or "Switch show…" in the palette lists the recent shows, the open one first and then the most recent, each with its picture (the timeline's still where it was left, else its style swatch, drawn while it was away, else its initial), its folder (its account on the web), when it was last opened and what changed since (proposals waiting, renders finished, an unsaved script). Typing filters, Enter opens, Shift+Delete takes one off the list; then Open another folder…, New show…, Try the demo show. The show left stays running: the last two warm (RwDaemonPool.CAPACITY), put away after 10 idle minutes, the least recently used first when a third is left, never one that works (a render, voicing or a chat turn). What finishes in one while away is said in a toast that stays ("Render of “The Lighthouse” finished in The Night Watch." with Open), by its episode's name: the pool reads the show's names first, and what finishes before they come waits for them, in order, and keeps the show from being put away as idle. A daemon that stops first is asked again once started again; one that stops twice before saying them, a show put away, or one that refuses has what waited said by its ids. Each show comes back where the person left it, on the next launch too: the section, the document, change or script open, the script's unsaved text (kept as its draft, marked unsaved, written only by Save), caret and scroll, the writing assistant open beside it, the booth's line (a take being counted in or recorded when the person switches is dropped: it was the other show's), the timeline's episode, playhead, zoom and scroll, the chat and the message being written, all inuser://reelwright-projects.json. The shortcut sheet lists the switcher's keys. A show that cannot open (its folder gone, its daemon failing before it answers) says why and leaves the person where they were. - Layout: a rail (Show, World, Cast, Sets, Timeline, Episodes, Memory, Settings), the
Show section's dashboard (
RwShowOverview), the browser, the timeline, the episodes, the show's settings ((implementation)RwShowSettings: its title, logline, cover, style and theme, render format, director, memory and writing settings, storage, and Duplicate, Export as Show Pack, Archive and Delete, the same on the web; read-only for a reviewer or a viewer of a shared show, and Delete its owner's alone) or a proposal in the main area, and the chat on the right. Both side columns fold for room, sliding over 0.15 s: the rail to its icons (Ctrl+B, the chevron at its foot; each icon's tooltip names its place and key, the show keeps its picture and the status its dot), the chat to a strip at the window's edge that brings it back on a click (Ctrl+Alt+B or Ctrl+Shift+L, the chevron in its header). The chat's left edge drags it between 340 and 720 px (a double-click gives it its 400 px); Studio keeps both folds and the width, and the web edition keeps them for each person in their browser. Keys: Ctrl+O, Ctrl+Shift+O (switch show), Ctrl+1–8, Ctrl+B (rail), Ctrl+Alt+B (chat), Ctrl+L (message box), Ctrl+K ((implementation)RwCommandPalette: every document, script, section and command, each word typed starting a word of its name or kind; Enter goes there: an episode to the timeline, a document to its section, open), Esc (back), Ctrl+Enter (approve, only with the focus in the proposal), Ctrl+= / Ctrl+- (UI size).
- Launch:
- Editor plugin: the same components in EditorDocks. The Showrunner dock is the chat,
the Show dock the browser, and a Reelwright main-screen tab shows the proposal under
review.
plugin.gdonly places them; their content is plain Controls ineditor/. - Screenshots for review:
tools/studio-screenshots.shdrives Studio from the recorded protocol messages and writesdocs/images/m3/. (implementation) What a new user meets, against the real daemon:godot --path engine -s res://tests/support/rw_first_look_tour.gd -- --show <dir> --out <dir>(each tour mark, the Show section once its pictures have come, and Cast, Sets, Episodes and the Timeline as that show has them).
MCP server (core/src/mcp/)
reelwright mcp --show <dir> runs over stdio, using the official
@modelcontextprotocol/sdk.
- Tools: the ToolRegistry, with annotations
readOnlyHintanddestructiveHint. (implementation) Andstudio_context, the MCP server's own (read only): where the person is in Studio on this show now, from the place the daemon keeps (.reelwright/studio/focus.json,core/src/session/studio-focus.ts), with that page's brief, the tools that act there and Studio's suggestions; "Studio has not said where the person is" when Studio is not open on the show. Claude Code (and the plan's chat, whose tools are this server's) can ask what "this" is. The web service's MCP server answers it for the person signed in to the session (createMcpServerperson; theirfocus-<person>.json), reading the show through its daemon. Given acontext(a chat message's, checked againstChatContext), it answers that place's brief as a turn carries it: the web edition's plan bridge asks it for each message, so a chat on a plan through the person's computer hears the same brief as the daemon's showrunner. - Resources:
reelwright://show,reelwright://doc/<id>,reelwright://changes/<id>. - Proposal results carry the change id, a readable diff summary and preview images.
- A
.mcp.jsonexample for Claude Code ships in the docs.
To use it from Claude Code, put a .mcp.json in the directory you start Claude Code in
(paths are relative to it). The client is the LLM, so no API key is needed:
{
"mcpServers": {
"reelwright": {
"type": "stdio",
"command": "node",
"args": ["core/src/cli/main.ts", "mcp", "--show", "shows/my-show"]
}
}
}
- Or add it once with
reelwright claude connect --show shows/my-show, which runsclaude mcp addin the show's folder with this Reelwright's absolute command (an installed release or a checkout alike). Claude Code then works on the show with your own Claude plan: see Using a Claude plan, which also covers Claude Desktop and Studio's opt-in chat on a plan. - The everyday tools (
coreTools) andstudio_contextcarry_meta: {"anthropic/alwaysLoad": true}, so Claude Code keeps them in view; it finds the others with its tool search. - Decide tools carry
_meta: {"anthropic/requiresUserInteraction": true}, so Claude Code asks the person before each one. Proposals the server writes reach a running Studio aschange.events: the studio watches.reelwright/changes/. - Point it at your own Show Pack, not
examples/shows/marigold: approving a proposal writes to the show, and the demo show is canon for tests. - Previews arrive as JPEGs at most 768 px on the long edge (ffmpeg downscales them); without ffmpeg the result names the image file instead.
Tests
- Registry: every tool's input schema passes the structured-output lint, and every handler is pure: the same state and input give the same ChangeSet bytes.
- ChangeStore: atomic apply with rollback; stale detection; undo/redo round trips to byte-identical files; variation groups; locks refused with the lock named; git commits authored by the repo's configured user, carrying the trailer.
- Graph: impact and drift on the demo show.
- Agent: replayed fixtures for the watch-it conversation. The repair loop (a fixture whose first tool input is invalid) and the budget stop.
- Session protocol: schema-validated in both directions; a scripted client drives chat, approval and undo against a daemon using the replay provider.
- MCP: an in-process client lists tools, calls
generate_characters, gets a change id and images, and approves it. - UI (gdUnit): components render proposal cards and diffs from recorded protocol
messages, and Studio starts a fake daemon and completes
session.hello.
Watch it (M3 done when)
- From Claude Code, with the MCP server configured, one conversation creates a new Show Pack. Theme, style, a crew of four and a bridge are each proposed with previews, approved, and committed. "Make the engineer older and grumpier" becomes a proposal with before/after model sheets. Undo works.
- In Reelwright Studio, the same flow runs through the chat panel with the showrunner agent, with proposal cards, previews, "give me 4 options" as variation cards, and lock/unlock.
- CI replays the recorded conversation and the protocol tests. No key is needed.