Docs Design

Contents

M11 Gamewright Studio: the plan

Status: approved by the owner, 2026-10-03, as written. On the core the owner asked for whatever gives the best end result; that is the bundled helper (below, "Why the core is a helper"). Evidence: research/studio-architecture.md. The milestone: ROADMAP M11.

What it is

An installable desktop app for macOS, Windows and Linux in which a person builds a game with this engine: start from a template, ask the Director for changes, read and approve its proposals, play the game as it changes, see the checks, export it. Built in Godot 4.7.2 the way Godot's editor is, from Godot's own Control nodes. Not a player or a launcher. Not Electron, Tauri or a web shell.

Shape

Gamewright Studio (one Godot 4.7.2 executable per system)
├─ the Studio UI, in Godot: documents, forms built from the schemas, the Director's chat and
│  proposals, the checks' results, the play view
├─ the play view: the game running inside the Studio, in a SubViewport, on the same engine
├─ the same executable, started by the Studio for checks and "play in a window"
└─ the core (validate, bake, proposals, the ladder, the Director's loop) as a helper program
   bundled inside the app, spoken to over stdio; no window of its own
  • The UI and the game are Godot. The game plays in-process and keeps its state hash: Tideline inside a Studio UI ended on the standalone run's hash at 300 and 600 ticks (measured).
  • The core stays TypeScript, compiled to one bundled binary (Node). It is ~all of today's validation, baking, Director tools and verification, and its tests. Compiled builds gave output byte-identical to the repo CLI and served all 29 Director tools to Godot over stdio (measured). Porting it to GDScript would mean rewriting and re-proving all of that; Godot's JSON also rewrites numbers and key order, so GDScript must never write pack documents.
  • The Director runs on the Messages API with the person's own Anthropic key, kept in the OS keychain. Someone with a Claude subscription can instead point their own Claude Code at the Studio over MCP. Anthropic's terms forbid offering Claude.ai login in a third-party app and paying for users' usage in products that run Claude Code, so: no login, no Agent SDK.
  • Exporting a person's game needs no Godot editor: our player for each system plus the game's bundle as data, and the web player.

Why the core is a helper

The owner's rule is that the Studio is built in Godot: everything a person sees, edits and plays is Godot. The core is not seen; it is the one source of truth for what a pack is.

  • One core, every caller. The CLI, CI, the web build's checks and the MCP server a subscriber's Claude Code connects to all run it. A GDScript port would be a second core to keep in step with the first, or the end of those callers.
  • The official SDK. The Director's loop runs on @anthropic-ai/sdk (streaming, retries, tool use). GDScript has no official SDK; its own loop would trail it.
  • Pack files stay exact. Godot's JSON turns every number into a float and sorts keys, so a GDScript writer would change files nobody touched. Only the core writes pack documents.
  • A fault stays in its process. The core and the Studio can each fail without taking the other's work with it.
  • Proven, not promised. The compiled core matched the repo CLI byte for byte and served all 29 Director tools to Godot over stdio.

Steps, each ending in something watched

  1. The player can be hosted (done 2026-10-03: GwPlayer.hosted, proved by gw_hosted_player_test.gd and watched with tools/engine-script.sh res://tests/player/gw_hosted_shot.gd; the longest bake frame in the host is 6 ms, from 236). Before it, it leaked into whatever ran it: keys typed in a text box move the hero, starting a game changes the global physics rate, its audio is all on Master, and the music bake stalls a frame for up to 298 ms. A hosted mode takes its input only from its view, keeps its own clock, plays on a Game bus and writes nothing global. Proof: a test types into a text box beside a playing game and the hero stays put, and the state hash matches a standalone run.

  2. The core leaves the repo. No bash, no Godot editor on the engine's source, no repo-root paths: the core is told where its resources are and which executable is the engine, and engine jobs take their arguments after -- (release builds refuse --path). Proof: validate, bake and an L2 Playtest run from the compiled core against an exported engine on Linux, then on macOS and Windows CI runners. In order, each with its own proof: Done 2026-10-03: 2a to 2e. The compiled core (124 MB) and the exported Linux engine (72 MB) run L0 to L2 from a folder of their own with nothing on the PATH, on the editor build's state hash, and bake the source's bytes.

    • 2a, a resource root. The core reads schemas, the library, the kits' kit.json and the playbook from a root it is told (GAMEWRIGHT_ROOT), laid out as the repo lays them out; unset, the repo. Proof: validate and bake from a directory holding only those resources.
    • 2b, the engine without bash. The core spawns the engine executable itself (GAMEWRIGHT_ENGINE), with a timeout, the paths made absolute and the display chosen in TypeScript; tools/heavy.sh wraps it on this box only. Proof: L1 and L2 with no tools/*.sh on the path.
    • 2c, one executable. The engine's CLI scene starts the player on -- play …, so a release build, which takes no scene argument, serves both. Proof: -- play --tour from the CLI scene ends on the same state hash as play.sh.
    • 2d, an exported engine. The engine exported on the Linux release template runs L0 to L2 for the core. Proof: Tideline's L2 green through it.
    • 2e, the compiled core. The core as one Node executable, run outside the repo against the exported engine. Proof: 2d's run from a temp directory with neither repo nor Node installed on the path.
    • 2f, Windows and macOS. CI runners on both: the compiled core and exported engine run L0 to L2; a timeout kills the engine's whole process tree on Windows too. Proof: the runs green. First run (2026-10-03, .github/workflows/studio.yml): macOS green, on Linux's L1 state hash (sha256:273e8ecc…); the tree kill passed on all three. Windows stopped compiling the core: the SEA config held Git Bash's /d/a/… paths, which Node cannot read inside a file. It now names its files bare from their own folder; Windows to be run again.
    • 2g, frames off Linux. L3 renders on macOS and Windows without Xvfb: a small window, or the Studio drawing the shots itself. Proof: an L3 frame read back on each. macOS: L3 green from the compiled core in a window on the runner's own display, the frames the same scene as Linux's (read back 2026-10-03). Windows: with 2f's rerun.
  3. The Studio opens and plays a game (done 2026-10-03: engine/studio/; one run opens Tideline, validates, bakes, plays it while the Director's box takes typing, and verifies to L2 green: tools/engine-script.sh res://tests/studio/gw_studio_shot.gd. Exported for Linux with the compiled core beside it and driven by clicks, it does the same: tools/studio/drive-linux.sh). A Godot app: open a pack, browse its documents, edit one through a schema form, play it in the play view or a window, run the checks and read the results. Proof: on Linux, open Tideline, play it while typing in the chat, run L2. Editing through a schema form was not done: it is S6 below.

  4. The Director in the Studio (done 2026-10-03 against a fake Anthropic API: tools/studio/director-scenario.sh; proposals made elsewhere, over MCP, reviewed there too: tools/studio/review-scenario.sh; the paid proof ran 2026-10-06 with tools/studio/director-paid.sh: on Tideline, after "I've played it", one real turn read the game, drew the beach before and after at 23:00 and 14:00, proposed "Bleach the driftwood so it catches the moon at night", was approved, rebuilt and played, for $0.59. The first tries found three things, now fixed: the answer's Markdown shown raw, the Director not knowing it was in the Studio, and no prompt caching, which spent the $1 budget a turn re-sending the playbook before it could propose). Chat with streaming answers, proposals shown as what changes, before-and-after previews and the checks; approve, adjust or undo. Proof: with a test key and a $1 budget, ask for a change, see the proposal, approve it, play the result.

  5. New games and export. Start from any template; export for macOS, Windows, Linux and the web. Proof: a new game from a template, exported, runs on each system. New games are S5 below. 5b, export a game (designed 2026-10-05):

    • What an exported game is. The player alone (no Studio, no core, no samples) and the game's baked bundle beside it, named for the game: Ember Shore/Ember Shore.x86_64 and a game/ folder on Linux, Ember Shore.exe and game/ on Windows, Ember Shore.app with the bundle in Contents/Resources/game/ on macOS, and on the web a folder any static host serves (index.html, the web player, game/), which is also what itch.io takes as a zip. Each comes out as one .zip a person can hand on. The bundle is documents and media only, as everywhere (ADR-0002): nothing from it is loaded as a script or a Godot resource.
    • The player finds its game. With no --bundle, the desktop player plays the game/ beside its executable (in Contents/Resources/ inside an app) before res://game, and the web page plays the game/ its own <meta name="gamewright-bundle"> names when the address has no ?bundle=. It opens on the game's title screen in a window titled with the game's name; what it saves is the game's own, never shared with another exported game or the Studio.
    • Players are release assets, the way Godot's export templates are. Each release adds Gamewright-Player-<v>-linux-x86_64.zip, -windows-x86_64.zip, -macos-universal.zip and -web.zip (from the player presets, built and smoke-tested where the Studio's packages are), listed in releases/index.json with their SHA-256 as kind: "player"; the download page offers only the Studio. Exporting for a system downloads that system's player for the Studio's own version once, checks its SHA-256 and keeps it in the person's cache. A player from another version is never used: the bundle and the runtime go together. In a checkout (and in tests) GAMEWRIGHT_PLAYERS names a folder of player zips instead.
    • The core exports; the Studio asks. gamewright export --to linux|windows|macos|web [--out DIR] bakes the game, validates the bundle against its manifest (sizes and SHA-256, as the play origin does), lays the player and the bundle out, and writes the zip. The session's pack.export runs it as a job with progress events. The Studio's Export (Ctrl+E, and a next step once the game is built) asks which system and where, shows the download and the packing, and ends on Show in folder.
    • macOS signing. Adding the bundle to an app breaks its seal, and a broken seal reads as "damaged" on a Mac, which is worse than unsigned. On a Mac the core signs the app again, ad hoc (codesign --force --sign -). Elsewhere, exporting for macOS waits for an ad hoc signer in TypeScript (the CodeResources plist and each slice's resource hash, after tools/release/macho.ts); until then the Studio says to export for macOS on a Mac.
    • Proof. A starter exported for Linux runs from its unpacked zip in a clean home with nothing on the PATH and plays its first Playtest to the same state hash as the Studio's build; the web export served by a static server boots in Chromium; the release workflow exports a starter on each system's runner and plays it there.

    Built 2026-10-05 (core/src/export/, GwStudioExport, tools/release/players.ts). Watched on Linux by tools/studio/export-scenario.sh: Ember Shore from the starter, exported, ran its look tour from the unzipped folder to the Studio build's hash (sha256:677e3a6c…), opened on its title in a window named for it with no arguments, saved under Gamewright Games; its web zip reached ready in Chromium from a plain static server; the Export dialog exported it at 1280x720. The Linux package's smoke exported Windfall with its own core and played it to the core's hash. Not yet: Windows and macOS (the release workflow's first run, the owner's call), and exporting for macOS off a Mac.

  6. Packaged for three systems. CI builds an installer per system, with updates. Signed once the accounts exist (below). Proof: a fresh install on each system does steps 3 to 5. Started 2026-10-05: .github/workflows/release.yml and tools/release/ build a Linux AppImage and .tar.xz, a Windows .zip and a universal, ad-hoc signed macOS .dmg, each with the compiled core beside the Studio and the nine samples, and smoke-test each on its own system (.claude/skills/verify/features/release.md). Linux is proven on this machine; Windows and macOS wait for the workflow's first run, which is the owner's call. No installer yet. The update check is in (2026-10-06, GwStudioUpdate): the Studio reads web/public/releases/latest.json from gamewright.diy when it opens, if the person leaves it on, and the welcome says when a later version is out; it never installs.

Steps 1 and 2 are engine and core work the Studio stands on; the Studio app starts at step 3. The web version of the Studio comes after the desktop one.

The Studio as a product (2026-10-05)

After step 4 the Studio worked but was not a product: raw JSON, ids on screen, one core command at a time, nothing to start a new game with. Reelwright Studio shows what finished looks like (research/reelwright-studio.md); ADR-0012 sets the bar. These steps come before 5 and 6 are called done, each proved by tests and a sized tour whose frames are Read:

  • S1, the kit (done 2026-10-05: GwStudio* in engine/studio/ui/, tools/studio/check.sh at 1280x720 and 1920x1080 and exported). engine/studio/ui/: GwStudioTheme (tokens, type and spacing scales, variations, check boxes, switches, popups, tooltips, focus ring), GwStudioIcons, GwStudioUi and its widgets (badge, empty state, spinner), Inter and Space Grotesk; the Studio rebuilt on it. Proof: the theme tests (contrast, focus ring, icons) and the Studio's tour at 1280×720 and 1920×1080.

  • S2, plain words (done 2026-10-05: GwStudioWords; every scenario asserts no id, pointer, path or hash on screen). GwStudioWords: kinds, pointers, ids, paths and proposals in a person's words, ids in tooltips. Proof: a test that the tour's visible text holds no id or temp path.

  • S3, the core as a session (done 2026-10-05: the daemon, the client and the supervisor, gamewright serve and engine/studio/net/; the window runs on them, GwStudioSession; tools/studio/resilience-scenario.sh watches a Director turn answer during a playtest and a killed core come back. Not yet: a build is not a job; it holds the core for the seconds it takes, so a turn asked during one starts when it ends). gamewright serve: JSON-RPC 2.0 over a localhost WebSocket with a per-launch token, its methods and events a schema generated into TypeScript and GDScript (Reelwright's session/, ported). The Studio starts and supervises it; jobs run side by side; proposals, document changes and the Director's stream arrive as events. Proof: a proposal made over MCP shows without the window refocusing; a bake and a Director turn at once; a killed daemon comes back.

  • S4, the layout (done 2026-10-05: GwStudioShell, the rail, folds, handles, settings, the shortcut registry, sheet and palette; a pre-commit hook and tools/stop-matching.sh came out of it). A section rail (the game, its world, characters, items, levels, rules, screens, sound, playtests, checks), panels that fold, resize and are remembered, the shortcut registry with its sheet, Ctrl+K, a UI-size setting, the window placed and remembered. Proof: tests for each habit; at 1280×720 nothing pushes the Director off the window.

  • S5, a new game (done 2026-10-05: the starters and gamewright new; the welcome and the New game dialog, GwStudioStart; a game's overview with its next steps, GwStudioOverview; tools/studio/welcome-scenario.sh watches all of it). A welcome screen: open a game, new game, try a sample. A new game starts from a sample as it is, or from its starter (the sample minus its content, keeping its look and sound), or from an empty folder with the Director. A game's dashboard leads with next steps that work without the AI; every empty view offers its action. Starters for every template, playable at once (today platformer_2d and puzzle_grid give an empty game). Proof: a first-look tour on a sample and on each starter; each starter L2 green.

    Starters (the core's half, 2026-10-05). Each starter is a pack of its own in library/starters/<template>/, written for a first look, not a sample minus a list of what to drop when gamewright new runs. Reelwright can subtract because its starter keeps no content: a show without its cast, sets and episodes is still a show. A game cut down from a sample has to stay playable, so what is left (one level, one scene, a small region) must still fit its rules, Playtests, screens and brief, and most of those need rewriting for it anyway; a removal list can't promise that, and nobody would see the result until a person ran new. A folder is checked like any game: validated, verified to L2 and shot as it ships, and reviewed as a diff. What subtraction gave for free, the sample's look, a test keeps instead: core/test/starters.test.ts fails when a starter's Style, Models, Sprites, Sounds, Score or Theme are not exactly its sample's, so a sample that changes how it looks is copied across on purpose. The six: platformer_2d Meadow Run (Burrow Run's meadow), puzzle_grid Harbour Push (one of Pier Pressure's levels, where the order of pushes is the puzzle), visual_novel Before the Fog (one evening in Fog Signal's look: a choice, a flag, two endings), adventure Windfall (Thornwick's village: fetch the fallen bell), action_survival Ember Shore (Tideline's coast at half the size: gather, make the fire, light the lamp as the night comes), twin_stick Salt Pan (Saltwell's well held through three short waves of scuttlers and spitters). Each keeps its sample's rootSeed, since its Playtests and shots were proven on it. gamewright new copies a starter or a sample as the person's game (ids from its name, made by them, now) or makes an empty folder for the Director; new --list feeds the New game dialog; the installed Studio carries both (tools/core-app.sh). Proof: tools/studio/starters-check.sh (every starter through new, then L2).

  • S6, editing (done 2026-10-05: docs.form and docs.edit in the core, the inspector and undo in the Studio, GwStudioInspector and GwStudioEditing; tools/studio/edit-scenario.sh watches an edit, an undo, a redo, a refusal, a tag added and a build. Lists of words, numbers, choices and references are edited in place, and a list of objects item by item. A pending proposal shows under each field it changes, with Approve and Reject there (tools/studio/review-scenario.sh approves one from the field). Since 2026-10-06 the form is a pro tool's (design/studio-layout.md, "The Inspector's form"): an unset group is added with "Add" and a new object item with "Add an item", each the core's blank; a set optional group is removed with "Remove"). A schema-driven inspector: fields with plain labels, choices as dropdowns, references as pickers, numbers with their ranges; every edit a change set through the core, undone with Ctrl+Z; proposals shown as plain field changes. Proof: change Driftwood's description and a number, undo, redo, bake, play.

    The core's half (2026-10-05). docs.form works a document's fields out of its kind's schema (core/src/session/form.ts): in the schema's order, labelled by fieldLabel; a string whose schema is another kind's id $def is a reference, offered the game's ids of that kind; objects are groups with their fields one deeper (an unset one is set as a whole); lists are a count, edited as JSON; meta is a group whose provenance, derivation and locks are shown, never edited, and left out when unset; a union of strings is text (a choice when every branch is fixed), a union of objects the fields of the branch the value takes. docs.edit applies the person's edits (revise's applyEdits) at once as their own change set, through the store's applyOwn, as lock and undo do (core/src/session/edit.ts). A refusal writes nothing and its data.pointer is the field edited: change.invalid (the schema or the game's rules, said as the explainer says it), change.locked, change.lock_fields, edit.invalid, edit.read_only, change.empty, doc.unknown. Proof: core/test/session/edit.test.ts.

    Drafts (2026-10-06, for the level editor: design/studio-level-editor.md). A person paints a level one stroke at a time, and most strokes on the way leave it unfinished: no way to win yet, no start, an exit not yet reachable. An edit refused for that could never be made, so docs.edit takes draft: true. A draft lands when every document it writes fits its schema, even if it breaks those documents' game rules; the problems it keeps are noted on its change set (draft: true, kept errors as warnings) and show as problems: the level's tab turns red, the checks and the problems list them, and bake and play refuse the game, saying why, until they are fixed. A draft is still refused for a schema error (or a broken id, kind or file) in a file it writes, and for any problem it newly causes in another document (a legend that leaves a reference elsewhere dangling). Only the person's own edit is a draft: the Director's proposals, and their approval, keep the whole rule whatever they ask. Undo and redo work across drafts: undoing the person's own change puts back what was there before it, a kept problem included, so it is held to the draft rule; redo holds a change to the rule it was made under. An edit without draft is refused exactly as before. Proof: core/test/session/draft-edit.test.ts.

  • S7, releases. Merged into step 6: a tag builds each system's package, smoke-tests it in a clean home with nothing on the PATH, and publishes SHA256SUMS; an update check that never installs; Report a problem; a CHANGELOG.

Risks

  1. The core's ties to the repo are the biggest piece of work (1–2 weeks), and how L3 renders frames on macOS and Windows without a visible window is not yet tested.
  2. The play view hosts a whole engine. A crash in a kit takes the Studio down with it, so "play in a window" must always work.
  3. UI work Godot doesn't hand us: docking, schema forms, Markdown, HiDPI on Windows and X11. Godot-built tools (Pixelorama, Material Maker) show it can be done.
  4. First-run trust. Unsigned, macOS needs Open Anyway and Windows' Smart App Control can block the app outright.
  5. Keys. Some Linux desktops have no keychain; a person without an API key or Claude Code can't use the Director.

Decisions

  • This plan: approved, 2026-10-03.
  • The core: a bundled helper program (the owner: "whatever is absolute best practice and will result in the absolute best end result").
  • Still the owner's, before the first public download, not now: an Apple Developer account ($99 a year), a Windows signing service (Azure Artifact Signing, $9.99 a month, open to individuals in the US and Canada), and a licence for the code (today: all rights reserved).

From docs/design/m11-studio.md in the Gamewright repository.