M5 Writers' room and missing-asset pass
M5 takes a show from a premise to a script it can shoot:
"Write us an episode where the crew salvages a buoy that sings" → an outline you approve in steps → the cast and sets the script needs, proposed → the screenplay →
direct_episode(M4) → a voiced, rendered episode.
Read with ARCHITECTURE §4 and §7, docs/design/m3-showrunner.md (change sets, tools) and
docs/design/m4-director.md (screenplay → EpisodeIR). What the room knows of earlier
episodes (the story so far, the characters' voices, the gags and which are resting, the open
threads) is the show's memory: m10-show-memory.md.
What the model does and doesn't do
As in M4, the model writes the story and code holds it to the show.
| Decided by code (deterministic) | Decided by the writers' room (the showrunner model) |
|---|---|
| The outline's shape: beats land in scenes, ids are unique, every scene has a place | Premise, logline, beats, scenes, the screenplay's words |
| Canon references resolve: cast, sets, props, bible gags, threads and arcs | Which gags, threads and arc beats an episode serves |
| Bible bookkeeping: a gag's usage, an arc beat landing after the one before it, a thread raised before it is resolved | Who a new character is; what a new place is |
| What the script needs that the show lacks, and the proposal that makes each | Writing a set or character pattern when the catalog has none |
| Running time against the target, from estimated takes |
Theme never rules and hard world rules are prose. Code cannot judge them, so they go to the
model with every writers' room tool (tool descriptions and the show context), and the human
judges them at each checkpoint.
The outline (EpisodeOutline)
episodes/<slug>/writers-room/<slug>.outline.json, kind EpisodeOutline, schema
schemas/v1/outline.schema.json. One document grows through the checkpoints.
idoutline.<slug>,episodeep.<slug>,title,premise(two to five sentences),logline(one sentence),targetSec.cast: the characters, each an existingcharacterid, or a new one:{name, role (an archetype catalog id), description}.beats:{id, text, kind}where kind issetup | inciting | complication | turn | climax | resolution | button, and optionallyserves: a bible gag, thread (raise/resolve) or arc beat (arc+beat), andactBreak(EpisodeOutline 1.1): an act ends after the beat.scenes:{id, heading, set, cast, beats, summary, props?}.headingis the Fountain heading the screenplay will use (INT. MARIGOLD - BRIDGE - DAY).setis an existing set id, or a new set{id, name, setType (catalog id), description}.propsare existing prop ids or new{name, family}.
Checkpoints are approvals. write_outline proposes the outline with premise and logline
first. Revising it adds the beats, then the scenes. Each is its own proposal, and nothing
later is written until the human approves. In "just make it" mode the human approves as it
goes, or tells the showrunner it may.
Without a model: the season and the room on cards
The product audit's #14 and #15. A showrunner runs a season, and writes outline first; both used to need a model, or did not exist.
- The season (
core/src/studio/season.ts). show.json'sseason(ShowManifest 1.1) is the running order:{number, episodes: [{episode, title?, logline?}]}. An entry names an episode by id: directed, outlined, or a script (its id isep.<its file's name>, the id directing it gives, so it keeps its place from script to directed), or an idea with nothing written yet, which carries a working title and a logline. Episodes the season does not list follow it, as made.reelwright validaterefuses an episode listed twice, and an entry with no files and no title. - episodes.status follows the running order: each episode's
numberis its place, itsstatushow far it has got (idea, outline, script, directed, voiced when every line has a take, rendered when it has a render, delivered when a render has its deliverables), worked out from its files and never kept;arcsandthreadsare the bible's arc beats and threads it moves, from its outline's beats'servesand from the bible's own (an arc beat naming the episode, a thread introduced or resolved in it). Studio's Episodes and Show page read it. - Tools.
order_episodes {order}puts the season in a new order; it keeps every entry the season lists (an idea is never dropped by an order that forgot it) and may place episodes it did not list.add_episode_idea {title, logline?, after?}adds an idea, its id from its title. Both write show.json as one change, the person's own from Studio (undone with Ctrl+Z), a proposal from the showrunner or MCP. - The script from the outline (
core/src/writers/draft.ts):screenplays.draft {episode}(read) anddraft_screenplay(propose, never over a script the episode has) draft its Fountain with no model: the title page, then for each scene its heading, its summary as a synopsis (=), who is in it as a note ([[In the scene: OTTO and CHIEF.]]) and the beats it lands as action, in order; an act break is a section (# Act Two) where its act's first beat starts, inside a scene too. An outline with beats and no scenes yet plays them on the show's first set. The script editor opens it unsaved; the model, when there is one, writes over it. - Studio (both editions). Episodes opens on the season board (
RwSeasonBoard): a card per episode in the running order, its number, its status as a word and seven steps, and the bible's arcs and threads it moves, each in its own colour, named in a legend. A card dragged onto another goes before it (its left half) or after it, Alt+Left and Alt+Right move the one focused; the board shows the order at once and asks for it (order_episodes). Add an idea… asks for a title and a logline. The list below and the Show page follow the order. An idea's or an outline's Outline opens the writers' room in its place (RwOutlineBoard,#/<show>/episodes/<episode>): title, logline and premise; beats as cards with their kind, what they serve in the bible and an act break, moved earlier and later or taken out; scenes as cards on the show's sets (the heading follows the set), with their cast and the beats they land picked from menus and a summary. Save sends the whole outline (write_outline) as the person's own change; Write the script drafts it. A viewer reads both and moves nothing.
Checks (core/src/validate/outline.ts)
These run when the outline is proposed and in reelwright validate:
- Shape: unique beat and scene ids; a scene's beats exist; once there are scenes, every beat lands in one; a scene's cast is in the outline's cast; a heading resolves (by the M4 SetIndex rules) to the scene's set, existing or new.
- Canon: existing cast, set and prop ids exist; a new character's role is an archetype in the catalog; a new set's setType is a set type in the catalog; a new character's name does not resolve to someone already in the show (use them instead).
- Bible: a served gag exists, and its
usageholds (araregag at most once per episode). A served arc beat is the arc's next undone beat. A thread is raised only if it is not open yet, and resolved only if it is open. - Runtime (once the screenplay exists): its estimated running time against
targetSec, as a warning beyond ±25%.
write_screenplay checks the script against the episode's outline when there is one: every
outline scene appears with its heading, and every speaker is in the outline's cast.
(implementation) Its answer gives the dialogue's length and the whole running time as the
heuristic director would cut it; when that director cannot cut it yet, it says why instead
("no total, since the heuristic director cannot cut it yet (no set is called …)").
Trying action lines
(implementation) While writing, try_action {set, text} (read) stages a few paragraphs of
Fountain on one of the show's sets with the heuristic director and says, paragraph by
paragraph, what it made of them ("char.otto walks to helm, and sits"), and "nothing staged"
for any it did not act on, with the props and sounds it could not find. Wording it cannot
stage is found and changed before the draft, not in the cut.
The missing-asset pass
episode_needs {episode} (read) reports what the outline and the screenplay need, from the
outline when there is one and the script otherwise (screenplay_needs folds into it). Each
need is resolved to an existing document, or comes with the one proposal that makes it:
| Need | Proposal |
|---|---|
| A new character with a role | generate_characters {pattern, roles: [role], names: [name]}, using the archetype's catalog pattern, else the theme's default character pattern |
| A new set with a setType | build_set {pattern, setType, id}, using the pattern of that type its name and description say most about, else one no set of that type in the show looks like yet, else the set type's catalog pattern |
| A set type with no pattern | create_set_pattern first (model work), then build_set |
| A new prop with a family | create_props from the family's pattern, else a hand-written prop |
fill_needs {episode, rationale} (propose) makes every deterministic proposal in one call,
one change set per need, so each can be approved or rejected on its own. It returns what is
left for the model, such as a set type that needs a pattern.
Two small changes make the made things resolve by the names the script uses:
generate_characters takes names (one per role), and the example theme's catalog links its
patterns (settype.ship_bridge → pattern.set.marigold_ship, and so on).
(implementation) A set type can have more than one pattern, and a second room of a kind is
usually another place: the thirty-ninth watch-it's Boxwood, the Licensing Board's patrol
cutter, came out of the Marigold's pattern as her bridge, kettle shelf and all. So the pass
picks among the patterns that build the type. First, of two or more, by words: the one whose
name and description share the most telling words with the set's name and description,
counting only words no other candidate has ("the Licensing Board's spotless patrol cutter" is the cutter's,
"as cramped and patched-up as the Marigold's" the Marigold's). Else, when the show already
has a set of the type, a pattern none of them looks like: a set looks like the pattern it was
drawn from (meta.derivation.pattern), and a hand-built one like its type's catalog pattern.
Else the catalog's. The demo show has two bridge patterns, pattern.set.marigold_ship and
pattern.set.patrol_cutter. build_set says so too: a set drawn from the pattern another
set of its kind looks like warns set.same_kind, naming the patterns of that type no set
looks like yet before create_set_pattern. A screenplay heading may name a set the outline
plans before anyone builds it; the pass reads that scene's action with no set.
(implementation) The rooms the pass counts are the show's sets and the sets the same
outline plans before this one (Room, setPatternFor in core/src/writers/needs.ts). Two
new bridges in one outline both took the cutter's pattern; now the second takes another that
no room looks like, else the catalog's. build_set with no pattern chooses one the same
way from its setType, name and description, counting the show's sets and those waiting
in proposals, and its proposal says which and why (set.pattern_chosen, info: "built from
pattern.set.patrol_cutter: no settype.ship_bridge of the show's looks like it yet"). Its
set.same_kind warning counts the sets waiting in proposals too, naming the proposal, and
when every other pattern of the type has a room like it already it says whose each is
("every other pattern that builds settype.ship_bridge has a room like it already
(pattern.set.patrol_cutter: set.boxwood_bridge (waiting in chg.…))").
Sound recipes
A screenplay names sounds ("the buoy SINGS", "the comm CHIMES") that a show may not have.
Change sets carry JSON and text, and a file in audio/sfx/ is live the moment it exists, so
a sound is proposed as a recipe, not as audio.
- The document. A
SoundRecipe(schemas/v1/sound.schema.json) lives ataudio/sfx/<slug>.sound.jsonand is the soundsfx.<slug>, the same id a recordedaudio/sfx/<slug>.flacwould have; a slug with both is an error. (implementation) Music is the same underaudio/music/:music.<slug>, a tune a music cue starts and loops, made a whole number of bars so it loops cleanly. A recipe whose id is not its folder's family and slug issound.id_mismatch. It has a length, a target peak, an optional echo and up to 16 layers:tone: a pitch, an optional glide to another (even in pitch), vibrato, a wave (sine, triangle, square, saw) and partials for bells and metal;sequence: notes back to back, each a tone with the layer's wave and envelope;noise: white, pink or brown, band-limited by a highpass and a lowpass, seeded. Every layer has an envelope: a linear attack, an exponential release, and a 5 ms fade at its end so nothing clicks.
- Synthesis (
core/src/audio/synth.ts). The core computes every sample itself, with no ffmpeg (synth 2.0; 1.x built an ffmpeg graph, and a release that carried its own ffmpeg compiled the demo to another Timeline than the checkout). A tone is its wave's Fourier series, stopped below 21.6 kHz at the highest pitch a glide or vibrato reaches, so no harmonic folds back past Nyquist. A glide is even in pitch; a vibrato swings its depth in cents either way of the pitch it rides, the same all along a glide (synth 1.1; 1.0 swung a fixed number of hertz, so a falling mew's 30 cents became 59). Noise is PCG32 seeded with the layer's seed, coloured pink or brown as ffmpeg'sanoisesrccolours it and filtered by its highpass and lowpass as ffmpeg's are (two-pole, Q 0.707), then shaped by its envelope. The layers are summed, echoed (each repeat delayMs later and decay times quieter), cut to the exact sample count, normalized to the recipe's peak and written as a 48 kHz 24-bit mono WAV with a fixed 44-byte header. Only IEEE arithmetic in a fixed order, and the sine, exponential and logarithm ofcore/src/audio/exact-math.ts, built from it:Math.sin,Math.expand the rest are left to each platform's build and may differ in a last bit, which can move a sample. Against 1.2, over all 95 recipes Reelwright and its examples have: tones within 1e-6 dB; with ffmpeg's generator in place of PCG32, noise too, so the only change is the noise's draw, which moves a sound's level (peak-normalized) as far as another seed does, a median 0.7 dB. - Derived audio. The same recipe gives the same bytes on every machine (golden hashes in
core/test/synth.test.ts). The WAV is cached at.reelwright/cache/sfx/<sha256>.wav, keyed on what the audio depends on (its length, peak, echo and layers, and the synth's version), never its name or bookkeeping. The director, the compiler's media and the render all find a sound the same way (core/src/adapters/sounds.ts): the recorded file, else the recipe synthesized. - The tool.
create_soundsynthesizes the recipe first (one that comes out silent is refused) and proposes the JSON; its answer gives the length and peak, and a picture of the sound: its waveform over its spectrogram, in Hz against seconds, so a showrunner that cannot listen can still check pitch, envelope and noise against what was asked (the twenty-third watch-it's showrunner read the synthesized files' waveforms by hand).render_previewkindsoundpictures any sound, recorded or a recipe, without the engine. The missing-asset pass points sound needs at it. (implementation) It asks for no sound a character makes ("Otto GIGGLES", "The Chief SNEEZES"): the director voices those in the character's own voice; a thing's sound ("The buoy WHIMPERS") it still asks for.
This is procedural groundwork for M9, whose generative SFX adapters will add sounds a recipe cannot describe.
Writing with Claude in the script editor
(implementation) Studio's script editor has a writing assistant (assist.write,
core/src/studio/assist.ts; the panel is RwScriptAssist). Claude works as a staff writer in
the room: it continues the script at the caret, rewrites, punches up or shortens the selection,
adds physical business, writes the next line for one of the cast, or writes a whole scene from
what the writer says happens. What it writes is in the show's voice and is what the heuristic
director can stage, and the director reads it before the writer sees it.
The request. The editor sends its text as before + selection + after, so no offsets
cross between Godot and JavaScript. It also sends the action, the writer's instructions, and
settings: a free-text tone, a length (short, medium or long), a boldness (faithful, balanced
or bold) and the show's house voice. Studio keeps the settings per show, by its id, in the user
folder; the web edition keeps them in the browser.
One call. The call goes through the LlmProvider seam and the director's ModelCaller. It
is priced against show.json's budget before it is sent, billed to .reelwright/spend/ under
assist.<episode or path>, and sent again when the stream breaks. It uses the stage
writers_room when show.json configures one, else the showrunner's settings. Thinking is
adaptive, effort is the stage's (default medium), and max_tokens follows the length. There
are no tools, so the reply's text streams as assist.event deltas.
The prompt has two cached blocks:
- The system prompt (
assistSystemPrompt, never interpolated): the role, Fountain, what the director stages, and original work only. The director part comes from whataction.tsandheuristic-perform.tsread: one physical action per sentence, present tense and singular; the director's moves, postures, gestures, looks and faces; props by their names; noises as the show's capitalized sound words; parentheticals for delivery only. - The show context (
assistContext): the theme's premise, tone and never rules; the bible's tone rules, relationships and lore; what the show remembers; the cast with persona, speech and voice; each set with its heading name, times of day, places, pieces and ways in and out; the props; the sound words; the writer's house voice; and the screenplay, with<caret/>or<selection>…</selection>where the suggestion goes. A long script is cut around the caret: the earlier scenes become their headings, the caret's scene is kept from its own heading, and a little of what follows stays.
"What the show remembers" (remembers) is one input to that builder: the show's memory
(m10-show-memory.md) as every writer reads it, within memory's token budget, for the scene
at the caret: the characters it names (their designed voice, the voice memory heard, their
catchphrases), the running gags it touches and which are resting, the story so far without
the episode being written, open threads and world facts. The theme's and the bible's rules
are in the context already, so memory does not repeat them. A show with no memory yet gets
the bible's running gags and threads and its continuity logs' facts in its place.
Read before it is shown. The reply is cleaned of code fences, preamble and echoed markers.
It is then joined to before and after. A selection keeps its own whitespace at either end,
so a sentence replaces a sentence. At the caret the suggestion is its own paragraphs, one
blank line from what is around it. The whole text goes through readScreenplay, and the
suggestion keeps the items and notes on its own lines, counted from its first line. A note's
word fix names the suggestion's line.
When the suggestion has something a writer would have to fix, it goes back to the model once, with the notes in plain words ("Line 3 (…): the director stages nothing for it…"). Those are:
- a cue the show does not have;
- a heading on no set;
- a sound word the show lacks, unless the writer's instructions or the script already use it;
- an action paragraph with nothing staged after the scene has begun. A scene's first paragraphs only place everyone, and the director reads them for that.
The second pass is kept unless it has more problems than the first, and then revised is
true. There are never more than two calls.
Failures say what to do.
assist.no_model: no provider or no key. The message tells the writer to connect Claude with an Anthropic API key, or to write with Claude Code connected to Reelwright on a Claude Pro or Max plan. The panel offers Connect Claude, except on the web.assist.budget: the cap's own words.assist.rate_limited.assist.cancelled:assist.cancelaborts the call.assist.busy,assist.no_selection,assist.no_instructions,assist.unknown_characterandassist.too_long(a selection over 40,000 characters).
The panel. "Write with Claude" or Ctrl+I opens the panel beside the script; below 1280 px it takes the reading's place until it closes. It opens on Rewrite with a selection and on Continue at a caret, and says what it acts on ("4 lines selected", "At line 9"). The suggestion streams into a Fountain-colored preview. Under it, the director's staging and notes are drawn as the reading draws them. A word fix changes the suggestion; a tool fix runs as the writer's own change. Then come the cost and Accept, Try again and Discard (Stop while it writes). Accept puts the text in as one undo step. It refuses, and says why, when the script changed since the writer asked.
The script editor as a writer's tool
(implementation; product audit 2, findings 9, 10 and 23) A writers' room lives in the script for weeks, so the editor does what Final Draft and WriterDuet do. Everything here is GDScript in Studio, the same in the installed app and in the browser, and the script stays Fountain.
The page's face. The script draws in Courier Prime (OFL, bundled under
engine/addons/reelwright/ui/theme/fonts/) at 12 point: ten characters and six lines an inch,
the same letters on every computer and in the browser. Ids and codes draw in the bundled Noto
Sans Mono, never a font the computer happens to have.
Find and replace (rw_script_find.gd). Ctrl+F opens a bar over the script, Ctrl+H the
same with Replace; a focused script keeps both keys, so Ctrl+H is History only elsewhere. Match
case and whole word; Enter and Shift+Enter in the box, F3 and Shift+F3 in the script, go to the
next and the previous match, round past the end; the bar says "3 of 12" and every match is lit.
Replace takes the selected match and finds the next; Replace all (Ctrl+Alt+Enter) is one change
Ctrl+Z takes back. Without Match case each match keeps its case, so renaming Chief to Captain
makes the cue CHIEF into CAPTAIN and it is still a cue. A viewer finds but does not replace.
Elements (rw_script_elements.gd). The bar under the header shows the element the caret is
in, and choosing one makes the line it. Enter after a scene heading starts the action, after a
name the dialogue, after dialogue and a transition what comes next; Tab on an empty line goes
round action, character, transition and scene heading (Shift+Tab back), under a line of
dialogue opens a parenthetical, and on a name typed as action makes it the character. Each step
writes the Fountain a writer would otherwise type (the blank lines, the capitals, the brackets,
a forcing . ! or >) as one undo step, and a letter typed in a heading, a name or a
transition comes out a capital. Completion offers the show's names and places and the script's
own, so a name typed once completes the next time; an empty heading offers INT. and EXT.
Spelling (rw_spelling.gd). A wavy line under each word the dictionary does not know, with
suggestions, "Add to the dictionary" and "Ignore" on a right-click. The word list is the
English Speller Database's size-60 US list (Hunspell en_US's own source, under its permissive
licence, engine/addons/reelwright/ui/script/spelling/), bundled, so it works offline and in
the browser. The show's dictionary already knows its cast, sets, props and sounds and every
name and place the script itself uses; the words a person adds are kept for that show.
Pages (rw_fountain.gd, rw_script_pages.gd, rw_script_page_view.gd). The core's own
reading and page layout (core/src/screenplay/parse.ts, pages.ts), ported to GDScript and
held to it by a shared fixture (engine/tests/fixtures/screenplay_pages.json, checked by
core/test/screenplay-pages-fixture.test.ts and rw_script_pages_test.gd): what the editor
counts is what the PDF export prints. The script shows where each page begins, and the bar says
the caret's page and scene and the page count. Pages shows the script on industry pages, with
page numbers, scene numbers in both margins and (MORE) and (CONT'D); a click on a line goes
back to it in the script.
Revisions (rw_script_revisions.gd). Lock this draft saves the show as a version
(versions.save, its note "Locked draft of versions.diff's line stretches put back, and what changed since is found
line by line (Myers). The marks can be hidden.
Notes (rw_script_notes.gd). A comment thread on the script's lines (comments.*, a
script anchor quoting them), in a panel beside the script that folds away and resizes like
the reading. Ctrl+Alt+M (or right-click › Note) writes one on the lines selected, or the
caret's line, saving the script first when those lines are not saved yet; people are
@mentioned on the web service. Notes are replied to, resolved and reopened, a resolved note
folds away under the filter, the gutter marks each open note's first line, and a note follows
its lines as the text changes (found in the editor's text, else where the daemon last found
them: moved, edited or gone). Everyone in the show sees a note as it is written. Viewers read
them; a reviewer, who cannot edit the script, can still give notes.
Watch it
One prompt in Claude Code with the MCP server, on a copy of Marigold: "Write and make a new
episode: the crew salvages a buoy that sings." Expected: an outline proposed in three
checkpoints, a new character (the buoy's owner) and a new set proposed by fill_needs, the
screenplay, direct_episode with its storyboard, then a render whose MP4 plays with voices.
Count the turns and the cost, and fix what it stumbles on, as M3's watch-its did.