Roblox — Run and Operate (house stack)
Overview
This skill maps every way code and assets leave your editor and land in Roblox on the firebit house stack: the local dev loop, Studio automation, GitHub CI/CD, and the two ID-minting CLIs (rblxsync, asphalt). The core principle: local builds and dry-runs are always safe; anything that touches origin main/production or runs a live sync publishes to a real Roblox universe (canonical rules in roblox-change-control).
All commands run from the current project's repo root unless stated otherwise. Every project on this stack shares the same skeleton (Rojo + Wally + npm scripts + boilerplate workflows), but names, IDs, and script lists vary per project — verify against the current repo before quoting them.
When to use / When NOT to use
Use when you need to run the game, build a place file, drive Roblox Studio, understand CI, or operate rblxsync/asphalt.
Do NOT use for:
- Bootstrap, npm script internals, sourcemap traps, wally → roblox-build-and-env
- Test/playtest/storybook mechanics and evidence standards → roblox-validation-and-qa (and plugin commands
/roblox-testing:test,/roblox-testing:playtest) - The four hard rules' canonical statement and change gates → roblox-change-control
- Adding a new config axis or flag → roblox-config-and-flags
- Runtime debugging in a running game → skill roblox-debugging
- General Rojo/rblxsync theory → global skills rojo-pro, rblxsync
1. Dev loop anatomy
Definitions: Rojo syncs a filesystem tree into Roblox Studio (a "project file" like level.project.json declares that tree); an .rbxl is a binary Roblox place file; Wally is the Luau package manager.
House-standard npm scripts (per-place names vary — read package.json for the actual <place> list, e.g. level, lobby):
| Command | What it does | Use for |
|---|---|---|
npm run dev:<place> |
build:<place> → opens the built .rbxl in Studio → rojo serve |
Day-to-day work on that place |
npm run build:<place> |
build:deps + rojo build -o <name>.rbxl <place>.project.json |
Build only, no Studio |
npm run build:<place>:open |
Build + open in Studio, no serve | One-shot inspection |
npm start |
build:deps + rojo serve (no build, no open) |
Reconnecting serve to an already-open place |
The .rbxl output filename is defined inside each build:<place> script. Built .rbxl outputs land in the repo root and are gitignored — check .gitignore before assuming.
The dev-loop caveat that bites everyone
rojo serve inside dev:<place> is invoked with no project argument, so it serves default.project.json — a code-only tree (Packages, DevPackages, src/Shared, src/Server, src/Client, and similar). The place you opened, however, was built from the per-place project file (level.project.json, lobby.project.json, ...), which additionally mounts Workspace geometry (assets/environment/* or equivalent), ReplicatedStorage.Assets, MaterialService, StarterGui, ServerStorage, and sometimes Lighting.
Practical consequences:
- After connecting the Rojo Studio plugin, Luau code edits live-sync; edits under
assets/do NOT. To see asset/environment changes, stop, rebuild (npm run dev:<place>again), reopen. - The served tree includes
DevPackages(Jest); built per-place places exclude DevPackages and**/*.spec.luau— so unit tests only exist when serve is connected or when using the default tree (details: roblox-validation-and-qa). - If your change is in a project-file mount (StreamingEnabled, environment StringValues, CharacterAutoLoads=false, any property set in the .project.json), it only takes effect on rebuild, never via serve.
Verify what each project file mounts by reading it — never guess: cat default.project.json <place>.project.json.
2. Studio: connecting and driving it
Manual connect: open the built .rbxl in Studio, then in Studio's Plugins tab click the Rojo plugin → Connect (defaults to localhost:34872, Rojo's default port). Install the plugin from the Rojo VS Code extension or rojo plugin install if missing. UNVERIFIED: plugin install state on any given machine — check before assuming.
Scripted Studio (MCP): an MCP server named Roblox-Studio is available via the house plugins (roblox-testing@dig1t-plugins, install-ui-labs@dig1t-plugins in .claude/settings.json). Its .mcp.json launches /Applications/RobloxStudio.app/Contents/MacOS/StudioMCP --stdio (path verified on macOS in the reference setup). It exposes tools like start_stop_play, execute_luau, console_output, screen_capture, keyboard_input, search_game_tree. Studio must already be open with a place loaded for the proxy to have something to drive.
The scripted way to run tests/playtests is /roblox-testing:test (Jest suite in play mode) and /roblox-testing:playtest (scenario files under tests/scenarios/). Mechanics, scenario format, and pass/fail rules are owned by roblox-validation-and-qa — do not improvise your own play-mode harness.
For runtime breakpoint debugging through the same MCP tools, use the skill roblox-debugging.
3. CI/CD reality (boilerplate .github/workflows/)
Every house project carries the same boilerplate workflows. Verified against the reference repo 2026-07-05 — re-read the current project's copies before relying on details:
| Workflow | Trigger | What actually happens |
|---|---|---|
deploy.yml |
Push to main or production; manual workflow_dispatch |
npm run build:deps, then rojo upload <place>.project.json for each place directly to a real Roblox universe. Auth: secrets.CLOUD_API_KEY. Target IDs come from GitHub Actions repo variables: DEV_* for main, PRODUCTION_* for production. |
format.yml |
PRs (non-draft) only in practice — in the reference copy the push paths filter is src (not src/**), which never matches files under src/, so it has never fired on a push |
StyLua action (pinned) formats src, auto-commits "Format with StyLua" and force-pushes with lease to the branch. |
linter.yml |
PRs (non-draft) only | wally install, download globalTypes, rojo sourcemap --include-non-scripts, wally-package-types, luau-lsp analyze ... src, selene src. Nothing runs on plain pushes. |
docs.yml |
Push to main touching src/**, docs/**, moonwave.toml, README.md |
npx moonwave build → publishes ./build to the studio's external docs repo (firebit-dev/docs, branch gh-pages, per-project subdirectory). |
release.yml |
Push to production touching version.txt |
release-drafter publishes a GitHub release named from version.txt. |
version-updater.yml |
Push to main touching version.txt |
Writes src/Shared/Config/Version.luau = return "<version>" and force-pushes an "Update version" commit back to main (pushed with GITHUB_TOKEN, so it does NOT re-trigger deploy or any other workflow — the deploy for that version came from the original human push of version.txt). |
This is why hard rule 1 exists (never push origin main/production; canonical statement in roblox-change-control): there is no staging gate — a push to main uploads every place to the universe in the DEV_* repo variables, and a push to production uploads to PRODUCTION_* (the live game). Treat both branches as deploy triggers. Commits stay local until the owner pushes.
To confirm the dev↔main / live↔production mapping in the current project: gh variable list -R <owner>/<repo> and compare against the in-game identity constants (section 6).
Note the deploy path never runs tests or the linter — linting is PR-only. A direct push to main deploys unlinted code.
4. rblxsync operating procedure
rblxsync (aftman-pinned; dig1t/rblxsync@0.2.2 in the reference repo — check the current project's aftman.toml) declaratively syncs monetization metadata (game passes, dev products, badges, universe settings) from rblxsync.yml to Roblox Open Cloud, then generates src/Shared/Data/Monetization/Generated.luau (typed ID tables, "do not edit") and writes real IDs to rblxsync-lock.yml (commit both).
Agent-safe sequence (never mints anything on Roblox); run from the repo root:
rblxsync validate # schema/config check
rblxsync run --dry-run # preview the exact create/update plan
The live sync — rblxsync run with no flag — is owner-gated (hard rule 2, canonical in roblox-change-control): it mints real products and badges (badges cost 100 Robux each) and matches resources by case-sensitive NAME; a name mismatch between yml and live creates a duplicate product on Roblox. Agents may edit rblxsync.yml freely and dry-run it.
Standing facts to verify per project:
- Auth:
ROBLOX_API_KEYin.env(gitignored). Confirm presence without printing:grep -c ROBLOX_API_KEY .env. - Universe
name/descriptioninrblxsync.ymlrequire cookie auth (ROBLOSECURITYin.env). If they are commented out, that usually means the cookie is stale — do not uncomment them without a fresh cookie, or every subsequent run fails. The lock file may carry stale name/description from a prior run. - Which universe does
rblxsync runtarget? Readuniverse.idinrblxsync.ymland compare against the dev/live identity table (section 6). In the reference project it targeted the live universe with no dev-universe config — assume the same until you check. - Trace where each generated ID actually flows at runtime. Case study (shipped firebit game, 2026): badge IDs did NOT flow from Generated.luau — they were hand-pasted from the lock into a table in the game-data module, so every new badge needed that paste step after the owner's live run. Grep for consumers of the lock/Generated IDs before assuming codegen covers everything.
- Live resources deliberately left undeclared in yml (legacy or previous-era products) may exist — look for an explanatory comment above
developer_products:inrblxsync.yml. Leaving them undeclared and untouched is intentional (hard rule 3 adjacent): do not "clean up" by declaring or deleting them. - CLI subcommand mismatch (rblxsync 0.2.2): a yml header may recommend
rblxsync export --output ...to reconcile names, butexportemits Luau of existing resources;rblxsync importis the subcommand that pulls live metadata intorblxsync.yml+ lock. Both are read-only against Roblox, butimportrewrites your local manifest — diff before committing.
Deeper CLI theory: global skill rblxsync. Which products exist and their prices: roblox-config-and-flags / the project's rblxsync.yml itself. Subscriptions cannot be minted by rblxsync at all — a manual Creator Hub act.
5. asphalt (asset upload + ID codegen)
asphalt (aftman-pinned; jacktabscode/asphalt@1.0.0-pre.14 in the reference repo) uploads local asset files to Roblox and generates Luau modules mapping filename → rbxassetid:// URL, so code never hardcodes asset IDs.
House-standard wiring (read asphalt.toml for the current project's exact inputs):
Input (from asphalt.toml) |
Output |
|---|---|
assets/images/**/*.png |
src/Shared/Data/Images.luau (generated header, flat keys, extensions stripped) |
assets/audio/**/*.{mp3,wav,ogg} |
src/Shared/Data/Sounds.luau |
| — | asphalt.lock.toml (upload state; committed) |
Invocation (verified from asphalt --help / asphalt sync --help at the pinned version — NOT wrapped in any npm script; run manually from the repo root):
asphalt sync --dry-run # preview what would upload (agent-safe)
asphalt sync # OWNER-EXECUTED: upload to Roblox cloud + regenerate Images/Sounds.luau
- Auth:
--api-keyflag orASPHALT_API_KEYenv var (note: a different variable than rblxsync'sROBLOX_API_KEY; whether.envcarries it is typically UNVERIFIED —.envis unread by design). - Uploads go to the creator group set in
asphalt.toml [creator]— read it, do not assume. --target studiosyncs to local Studio instead of cloud;--target debugfor inspection.- A cloud sync uploads real assets to the studio's group. Per the change taxonomy in roblox-change-control ("Asset (asphalt)" row), the live
asphalt syncis owner-executed: agents runasphalt sync --dry-runonly and hand off — the same pattern asrblxsync run.
6. Identity table (dev vs live) — how to build it for your project
Never quote universe/place IDs from memory. Each project keeps its identity constants in a shared game-data module (in the reference repo: a GameData-style module under src/Shared/Data/Core/). Build the table fresh:
- Grep for the constants:
grep -rn "universeId\|placeId\|PlaceId" src/Shared/Data/ - Cross-check the deploy targets:
gh variable list -R <owner>/<repo>—DEV_*variables should equal the dev IDs (deployed on push to main) andPRODUCTION_*the live IDs (push to production). - Cross-check
rblxsync.yml'suniverse.idagainst that table to know whether monetization syncs target dev or live.
Result: a table of dev vs live universe ID + one place ID per place. Record where each ID came from.
- A
game.PlaceId == 0check (often exposed as anisLocalFile-style flag in the game-data module) is true when running an unpublished built .rbxl in Studio — the normal state of the local dev loop.
7. What output lands where
| Artifact | Produced by | Committed? |
|---|---|---|
Per-place .rbxl files (repo root) |
npm run build:<place> |
No (*.rbxl gitignored; stray .rbxl files in root are usually old manual backups) |
sourcemap.json |
npm run build:sourcemap (also session-start hook — traps owned by roblox-build-and-env) |
No |
src/Shared/Data/Monetization/Generated.luau |
rblxsync run (owner) |
Yes |
rblxsync-lock.yml |
rblxsync run (owner) |
Yes |
src/Shared/Data/Images.luau, Sounds.luau |
asphalt sync (owner) |
Yes |
asphalt.lock.toml |
asphalt sync (owner) |
Yes |
src/Shared/Config/Version.luau |
version-updater.yml in CI — it is machine-written; if it disagrees with version.txt, that is drift to flag, not to hand-fix silently |
Yes |
| Moonwave docs site | docs.yml → studio docs repo (firebit-dev.github.io/docs/<project>) |
External repo |
| Live place publish | deploy.yml on push to main/production |
n/a |
Common mistakes
- Editing
assets/environment/*and expecting the connected Rojo session to show it — serve is code-only (default.project.json); rebuild the place. - Running
rblxsync run"to test" — it is a live mint against the universe inrblxsync.yml. Dry-run only; the live run is the owner's. - Running
asphalt sync(no flag) as an agent — a live asset upload to the studio's group is owner-executed (roblox-change-control taxonomy);--dry-runonly. - Pushing to origin main to "save work" — that is a deploy. Keep commits local (roblox-change-control).
- Assuming Generated.luau is hand-editable, or assuming all IDs flow from it at runtime — Generated is machine-written, and some ID classes may be hand-pasted elsewhere (see the badge case study in section 4). Trace consumers.
- Using
ROBLOX_API_KEYfor asphalt — asphalt wantsASPHALT_API_KEY(or--api-key). - Uncommenting the universe name/description in
rblxsync.ymlwithout a freshROBLOSECURITYcookie — every subsequent run fails. - Following a yml header's
rblxsync export --output ...literally and expecting YAML — at 0.2.2,exportemits Luau; userblxsync importfor yml reconciliation (diff before committing). - Expecting CI to lint or format your push — a direct branch push runs no checks at all (main additionally triggers deploy/docs); both format and lint are effectively PR-only in the boilerplate, because
format.yml's push paths filter (src, notsrc/**) never matches files undersrc/. Confirm in the current project's copy.
Provenance and maintenance
Derived 2026-07-05 from the firebit house-stack reference project. Project-specific state (IDs, product lists, flag values) was removed; only the shared conventions remain. Re-verify volatile facts against the current project repo (run from its root):
# npm scripts (dev loop, place names, .rbxl output names)
cat package.json
# CI truth
ls .github/workflows/ && cat .github/workflows/deploy.yml
# Deploy target repo variables (dev↔main, live↔production mapping)
gh variable list -R <owner>/<repo>
# Universe/place IDs (identity constants)
grep -rn "universeId\|placeId" src/Shared/Data/
# rblxsync target universe + commented-out metadata + header notes
sed -n '1,90p' rblxsync.yml
# rblxsync CLI subcommands/flags (version pinned in aftman.toml)
cat aftman.toml && rblxsync --help && rblxsync run --help
# asphalt inputs/outputs, creator group, and CLI
cat asphalt.toml && asphalt sync --help
# Studio MCP server definition + binary (macOS)
cat ~/.claude/plugins/roblox-testing/.mcp.json && ls /Applications/RobloxStudio.app/Contents/MacOS/StudioMCP
# .env has the rblxsync key (do not print the value)
grep -c ROBLOX_API_KEY .env
# Version drift between source of truth and CI-written module
cat version.txt src/Shared/Config/Version.luau