This file is for code-modifying agents working inside the StemStudio Engine open-source repository. It is intentionally a router and an architecture map, not a tutorial. Treat the code as the source of truth; this guide tells you where to look and what rules to honour.
- Search before you write. Reuse existing code and patterns.
- Ask clarifying questions when scope or product intent is ambiguous.
- Build a short plan in
docs/planning/before non-trivial changes, then implement against it. - Keep changes small and local. Pause before large or breaking edits.
- Read the relevant code before editing it.
- Prefer the code as the source of truth — when docs disagree, fix the docs.
- Update affected docs after a change.
- Dispose Three.js resources you create: geometries, materials, textures, render targets, listeners.
- Run the right verification before declaring work done. Never use
eslint --fix.
A browser-based 3D editor and runtime built on Three.js with a React UI, a behavior system, an ECS-style lambda layer, Ammo.js / Rapier physics, a Colyseus multiplayer sidecar, and a Go AI proxy that forwards calls to the user's own provider keys (BYOK).
Projects persist locally — IndexedDB by default, or a real folder via the File System Access API. There is no hosted backend in this build: no authentication, no cloud project store, no telemetry. Every feature you see in the UI runs against local state or against a service the user explicitly configured.
client/packages/
editor-oss/ The editor: scene tree, viewport, behaviors,
lambdas, physics adapters, scheduler, rendering,
asset loading, serialization, the Copilot panel,
runtime UIKit/HUD.
copilot/ Provider-agnostic copilot interfaces and a basic
chat panel for forks. Editor-oss reaches the
provider through ICopilotProvider — no concrete
provider is bundled in this build by default.
network/ HTTP/WS adapters. The remote-go adapter in here is
the only thing that talks to the AI server. Other
paths read/write the local ProjectStore directly.
shared/ Cross-package types, build-mode flags, queryClient,
Sentry, AppContainer shell.
play/ The Player-only runtime — the entry point a built
game uses when it ships standalone.
marketing/ Marketing pages used by the dashboard shell.
server/ Go HTTP/WS server. The AI subset (`cmd/ai-server`)
is the only binary that ships here — it proxies AI
provider calls using the BYOK keys.
stemstudio-multiplayer/ Colyseus server. Auto-started by `bun run dev`.
stemstudio-copilot/ Optional ACP/MCP bridge for forks that want
Claude Code-style tool use.
docs/ Engine subsystem docs and planning.
scripts/ Build, export, and Playwright smokes.
__BUILD_MODE__ is fixed to oss in this build, and the IS_OSS /
IS_INTEGRATED flags in @web-shared/buildMode flow from that. Code that
checks IS_OSS is the seam where this build deliberately diverges from
features that would require a hosted backend — auth, cloud asset storage,
telemetry, hosted multiplayer. Keep those gates intact when refactoring.
Project bodies ({meta, sceneJson, ...}) flow through the
ProjectStore interface in client/packages/editor-oss/src/persistence/:
IndexedDBProjectStore— default; one row per project keyed by ID.FileSystemProjectStore— writes<name>.<id>.stemscript.jsonfiles into a folder the user picked viashowDirectoryPicker. The picked handle persists in IndexedDB (fsHandleStore.ts) so subsequent reloads reattach without re-prompting on Chromium.
projectStoreFactory.ts is the singleton boundary. The OSS first-run
modal (OSSBootstrapModal.tsx) and the dashboard banners
(OpenFolderBanner.tsx, ReconnectFolderBanner.tsx) are the only
surfaces that swap the active store at runtime; everything else reads
through getProjectStore().
When you add a new feature that needs to write data, route it through
ProjectStore — do not invent a parallel storage path.
- Base type and lifecycle live in
client/packages/editor-oss/src/behaviors/Behavior.ts. - Behaviors register through
BehaviorTypeRegistry. Saved scenes embed the per-instance config inscene.userData.behaviorConfigs. Built-in behaviors are referenced by id only; full configs hydrate from the in-process registry on load. - Reach the engine from inside a behavior via
this.erth.*andthis.gameObject. OldEventBusandthis.targetstyle code is deprecated. - Lifecycle docs:
docs/behaviors/.
client/packages/editor-oss/src/lambdas/ — archetype-driven systems on
top of behaviors. Use when you need batched, dependency-scheduled work.
See docs/lambdas/ for the architecture internals.
Frame orchestration is in client/packages/editor-oss/src/scheduler/,
post-processing and pipeline in render/, adaptive quality in
core/quality/. The scheduler drives lambdas every frame; do not call
lambda systems directly.
Two engines, one adapter surface:
client/packages/editor-oss/src/physics/ plus the per-behavior glue in
behaviors/erth/physics/. Memory management for Ammo and shape-system
conventions are non-obvious — read docs/physics/ before touching either.
React tree under editor-oss/src/editor/. The dashboard, scene tree,
viewport, asset panels, and copilot panel all live here. UI components
that need the engine read global.app?.editor?.*; defensive optional
chaining is the convention because the engine boots after the React
shell.
This build has an optional, provider-agnostic AI surface and a Go AI proxy that fronts the BYOK keys.
editor-oss/src/copilot/ICopilotProvider.tsis the seam. The build ships with no concrete provider; an OSS fork can register one throughsetCopilotProviderFactory. When no provider is registered the Copilot panel hides itself.- The Go AI server (
server/cmd/ai-server) is what the editor'snetwork/adapters/remote-gocalls. It accepts BYOK keys from env or the dashboard's BYOK panel and proxies to Anthropic / OpenAI / Meshy / ElevenLabs / Anything World. It is not a hosted service — it runs on the user's machine. - Inline
execand the script-tool import pipeline (editor-oss/src/agent/script-tool/) work without any AI provider. - The dashboard exposes "Import stemscript folder" which stages a folder
via sessionStorage, navigates to a fresh project, and runs the same
execflow to materialize a saved project.
stemstudio-multiplayer/ is a Colyseus server. bun run dev boots it as
a sidecar. Client code lives in
client/packages/editor-oss/src/multiplayer/. The room schema is shared
across the wire; touching either side without thinking about both will
diverge them.
This build is OSS-only. The original codebase ships in two flavours from
the same tree (integrated + OSS); only the OSS slice was exported here.
Code paths gated on IS_OSS === true are always-on in this build, and
the corresponding integrated-only files are absent from the tree.
When you add a feature that would require a hosted backend (account management, hosted scene library, telemetry), gate it behind a new local-config flag or surface it through the existing BYOK pattern. Do not add hosted-backend URLs to the source.
Pick the narrowest meaningful check; broaden if risk justifies it.
bun run typecheck
bun run lint
bun run test # Vitest (jsdom). NOT `bun test` — that runs Bun's
# native runner, which lacks the jsdom env and fails.
bun run vite-build
bun run build-server # builds the Go AI proxy
# End-to-end smokes — require `bun run dev` running on :5173
node scripts/playwright/oss-smoke.mjs
node scripts/playwright/oss-filesystem-roundtrip.mjs
node scripts/playwright/oss-open-folder-banner.mjs
node scripts/playwright/oss-import-3dchess.mjsThe smokes cover the engine round-trip:
- IndexedDB persistence: dashboard → save → reload → play.
- File System Access mode: pick folder → save → reload → list.
- Open-folder banner: bootstrap with IDB, swap to filesystem mid-session.
- Stemscript folder import: 3D chess folder → exec → saved project.
If you change anything that those smokes touch (the persistence layer, the dashboard shell, the AiCopilot panel mount, the script-tool import pipeline) re-run all four. They are fast.
Plans go under docs/planning/YYYY-MM-DD-short-topic.md. Keep them
short: goal, assumptions or open questions, affected files,
implementation steps, validation steps. Use Markdown checkboxes (- [ ])
so the plan doubles as a progress tracker. Always include a
Manual code review checkbox in the Validation section.
If during implementation you discover the plan is incomplete, extend the plan first (add the missing checkboxes), then continue. Don't silently drift.
Pause and ask before:
- changing the persistence layer or save/load semantics;
- changing scheduler, render, or multiplayer semantics without clear local precedent;
- removing or refactoring
IS_OSSgates; - changing build scripts or deployment paths;
- introducing a new external service dependency;
- making a change that spans unrelated subsystems.
| You're touching... | Read first |
|---|---|
| Behaviors / game logic | behaviors/Behavior.ts, then docs/behaviors/ |
| Lambdas / ECS | lambdas/, then docs/lambdas/ |
| Scheduler / frame loop / quality | scheduler/, core/quality/, docs/engine-core/ |
| Physics | physics/, behaviors/erth/physics/, docs/physics/ |
| Editor UI / import / camera | editor/, controls/, serialization/, docs/editor/ |
| Runtime UI / HUD / UIKit | behaviors/uikit/, behaviors/hud/, docs/ui/ |
| Multiplayer | multiplayer/, multiplayer/worker/, docs/multiplayer/ |
| AI integration | copilot/, agent/, server/server/controllers/tools/ai/, docs/ai/ |
| AI server | server/main.go, server/server/server.go, docs/infrastructure/ |
| Persistence | persistence/, docs/editor/ (asset-management.md) |
| Scene serialization | object/, serialization/, docs/ARCHITECTURE.md |
| Three.js conventions | EngineRuntime.ts, render/, docs/engine-core/threejs-conventions.md |
| Art budgets | docs/art-specs/ART_SPECS.md |
- World axes: Y up, +X right, +Z toward the default editor camera. Three.js standard.
- Engine forward (character runtime / generated controllers): -Z.
- Mixamo assets face +Z by default — the
characterbehavior'sinvertForwardDirectionattribute is the 180° fix for stock Mixamo. - BlazePose → Three.js conversion:
Vector3(lm.x, -lm.y, -lm.z)(only Y and Z flipped, X stays). Seeassets/js/animations/poseFit.ts.
This project uses a local dual-graph MCP server for efficient context retrieval.
-
Call
graph_continuefirst — before any file exploration, grep, or code reading. -
If
graph_continuereturnsneeds_project=true: callgraph_scanwith the current project directory (pwd). Do NOT ask the user. -
If
graph_continuereturnsskip=true: project has fewer than 5 files. Do NOT do broad or recursive exploration. Read only specific files if their names are mentioned, or ask the user what to work on. -
Read
recommended_filesusinggraph_read— one call per file.graph_readaccepts a singlefileparameter (string). Call it separately for each recommended file. Do NOT pass an array or batch multiple files into one call.recommended_filesmay containfile::symbolentries (e.g.src/auth.ts::handleLogin). Pass them verbatim tograph_read(file: "src/auth.ts::handleLogin")— it reads only that symbol's lines, not the full file.- Example: if
recommended_filesis["src/auth.ts::handleLogin", "src/db.ts"], callgraph_read(file: "src/auth.ts::handleLogin")andgraph_read(file: "src/db.ts")as two separate calls (they can be parallel).
-
Check
confidenceand obey the caps strictly:confidence=high-> Stop. Do NOT grep or explore further.confidence=medium-> If recommended files are insufficient, callfallback_rgat mostmax_supplementary_grepstime(s) with specific terms, thengraph_readat mostmax_supplementary_filesadditional file(s). Then stop.confidence=low-> Callfallback_rgat mostmax_supplementary_grepstime(s), thengraph_readat mostmax_supplementary_filesfile(s). Then stop.
A token-counter MCP is available for tracking live token usage.
- To check how many tokens a large file or text will cost before reading it:
count_tokens({text: "<content>"}) - To log actual usage after a task completes (if the user asks):
log_usage({input_tokens: <est>, output_tokens: <est>, description: "<task>"}) - To show the user their running session cost:
get_session_stats()
Live dashboard URL is printed at startup next to "Token usage".
- Do NOT use
rg,grep, or bash file exploration before callinggraph_continue. - Do NOT do broad/recursive exploration at any confidence level.
max_supplementary_grepsandmax_supplementary_filesare hard caps - never exceed them.- Do NOT dump full chat history.
- Do NOT call
graph_retrievemore than once per turn. - After edits, call
graph_register_editwith the changed files. Usefile::symbolnotation (e.g.src/auth.ts::handleLogin) when the edit targets a specific function, class, or hook.
Whenever you make a decision, identify a task, note a next step, fact, or blocker during a conversation, call graph_add_memory.
To add an entry:
graph_add_memory(type="decision|task|next|fact|blocker", content="one sentence max 15 words", tags=["topic"], files=["relevant/file.ts"])
Do NOT write context-store.json directly — always use graph_add_memory. It applies pruning and keeps the store healthy.
Rules:
- Only log things worth remembering across sessions (not every minor detail)
contentmust be under 15 wordsfileslists the files this decision/task relates to (can be empty)- Log immediately when the item arises — not at session end
When the user signals they are done (e.g. "bye", "done", "wrap up", "end session"), proactively update CONTEXT.md in the project root with:
- Current Task: one sentence on what was being worked on
- Key Decisions: bullet list, max 3 items
- Next Steps: bullet list, max 3 items
Keep CONTEXT.md under 20 lines total. Do NOT summarize the full conversation — only what's needed to resume next session.