Skip to content

Scaffold Roblox game project with local-first quality gates - #1

Merged
dinoderek merged 5 commits into
mainfrom
claude/roblox-game-scaffold-x6bfd9
Aug 9, 2026
Merged

dinoderek merged 5 commits into
mainfrom
claude/roblox-game-scaffold-x6bfd9

Conversation

@dinoderek

Copy link
Copy Markdown
Owner

Summary

Scaffolds the repository for Roblox game development in native Luau, optimized for AI-agent workflows (Claude Code + Codex) and for verifying as much as possible before anything touches a Roblox server.

Toolchain (pinned via Rokit in rokit.toml)

  • Rojo 7.7.0, StyLua 2.5.2, selene 0.31.0, luau-lsp 1.68.1, Lune 0.10.5
  • selene's Roblox std committed as roblox.yml (hermetic lint runs)
  • No package manager yet by design — Wally is stale; policy adopts pesde when the first runtime dependency lands

Quality gates — lune run check (same gates in CI)

  1. stylua --check src tests lune
  2. selene src tests lune
  3. lune run analyze — strict luau-lsp analysis: Roblox pass (downloaded globalTypes.d.luau + Rojo sourcemap) and a Lune pass for tests/scripts
  4. lune run test — 21 unit tests under Lune with vendored tiniest (BSD-3)
  5. rojo build smoke test

Example slice (the architecture to copy)

  • src/shared/Currency/Wallet.luau — pure logic, no Roblox APIs
  • src/server/Services/CurrencyService.luau — dependency-injected orchestration, tested against tests/helpers/FakeDataStore.luau
  • src/server/Adapters/DataStoreAdapter.luau + composition root — the only Roblox-touching files, kept logic-free
  • default.project.json mounts src/shared into both ReplicatedStorage and ServerScriptService so relative string requires resolve identically in the engine and Lune

Agent docs

  • AGENTS.md (canonical): project map, layer rules, quality-gate policy, require rules, testing policy, and guardrails including "never trust memorized library versions/APIs — verify against live sources"
  • CLAUDE.md imports it via @AGENTS.md
  • docs/cloud-integration-tests.md stubs the future Open Cloud Luau Execution harness

Test plan

  • All gates green locally (format, lint, tests, build)
  • CI green including the luau-lsp type gate (run #2) — run Scaffold Roblox game project with local-first quality gates #1 caught a real strict-mode nil-comparison error, fixed in the second commit
  • Verified the gates actually gate: a deliberately broken test and the type error both fail the pipeline

🤖 Generated with Claude Code

https://claude.ai/code/session_01HfzdcKErige9QKVtsMtQoY


Generated by Claude Code

claude and others added 5 commits July 12, 2026 20:50
Native Luau + Rojo project set up for AI-agent development (AGENTS.md
canonical, CLAUDE.md imports it) with every gate runnable headlessly:

- rokit.toml pins Rojo 7.7.0, StyLua 2.5.2, selene 0.31.0 (pinned
  roblox.yml std), luau-lsp 1.68.1, Lune 0.10.5
- lune run check drives format, lint, luau-lsp type analysis, unit
  tests, and a rojo build smoke test; same gates run in GitHub Actions
- Unit tests run under Lune with vendored dphfox/tiniest (BSD-3);
  runner discovers tests/**/*.spec.luau
- Example slice demonstrates the architecture: pure Wallet logic in
  src/shared, DI-based CurrencyService in src/server/Services (tested
  against an in-memory fake store), thin DataStoreAdapter + composition
  root as the only Roblox-touching files
- default.project.json mounts src/shared into both ReplicatedStorage
  and ServerScriptService so relative string requires resolve
  identically in the engine and Lune
- docs/cloud-integration-tests.md stubs the future Open Cloud Luau
  Execution harness

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HfzdcKErige9QKVtsMtQoY
CI's luau-lsp gate caught what the local gates couldn't (no luau-lsp
binary in the dev container): strict Luau rejects ==/~= nil comparisons
on metatable-based types like Wallet. Switch CurrencyService to
truthiness checks and document the gotcha in AGENTS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HfzdcKErige9QKVtsMtQoY
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HfzdcKErige9QKVtsMtQoY
Three ways the currency slice could lose or corrupt a player's balance,
plus the contract that stops them recurring.

Correctness:

- Add game:BindToClose -> CurrencyService:unloadAll(). Balances only
  persisted on unloadPlayer, and PlayerRemoving is not guaranteed to fire
  on shutdown, so every online player lost the session's changes on every
  deploy. unloadAll lives in the service (pure, unit-tested) rather than
  in the root, which stays glue.
- Backfill Players:GetPlayers() after connecting PlayerAdded. A player who
  joined before the script initialised never got a wallet, and failed
  silently: getBalance nil, commands rejected, unload a no-op.
- A failed store read no longer leaves a silent walletless player. Reads
  are retried and, if they keep failing, loadPlayer returns (false, err)
  and creates NO wallet -- an invented wallet would be written over the
  player's real record on unload.

Data integrity:

- Unreadable stored data is quarantined at player/{id}/corrupt before the
  player is handed a replacement wallet. If quarantining fails the session
  is marked non-persistable and the original is never overwritten.
  Previously the corrupt record was silently replaced by a starting
  balance and then saved over, destroying it permanently. The deserialize
  reason was discarded; it is now reported through an injected log.

Error contract (STYLE.md):

- Expected failures return (ok, err); programmer errors assert, and assert
  at construction so a misconfigured service crashes at startup instead of
  failing one player at a time -- startingBalance is now validated in
  CurrencyService.new.
- No Roblox error crosses a layer boundary: DataStoreAdapter pcalls and
  converts to a tagged result, so services never pcall. StoreLike now
  carries `unknown` rather than pretending persisted data is typed.
- Recovered-from problems go to an injected log, never print/warn, so they
  can be asserted on.

Retry delays use an injected wait, so retry behaviour is tested with no
real time passing. Tests 21 -> 32.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI no longer restates the gate list, the type gate no longer downloads its
own inputs, and duplicated config facts now fail the build when they drift.

Single source of gates:

- The gate list moves to lune/util/gates.luau. lune/check.luau runs it and
  lune/parity.luau reads it. CI does setup then `lune run check` and
  nothing else, so adding a gate cannot silently skip CI. Previously
  ci.yml listed each gate by hand and AGENTS.md's claim that CI ran "the
  same gates" held only by manual synchronisation.

Hermetic type gate:

- globalTypes.d.luau is committed and pinned to the luau-lsp version in
  rokit.toml, the same way roblox.yml pins the selene std. analyze.luau
  previously fetched it from luau-lsp's main branch on every run with no
  cache in CI, so an upstream commit could turn CI red with no repo
  change. The pinned 1.68.1 definitions already differed from main.
- `lune run update-types` is the only script that touches the network and
  is deliberately not a gate.

Config parity gate (IF-CHANGE-THEN-CHANGE):

Some facts unavoidably live in two files. Comments asking people to keep
them in sync do not work, so this fails the build instead:

- .luaurc's Lune typedefs alias must match the rokit.toml lune pin. It
  could not be derived (.luaurc cannot interpolate), and analyze.luau only
  checked the parent directory, so a Lune bump produced an unresolved
  alias error rather than a clear one. It now checks the exact version.
- globalTypes.version must match the rokit.toml luau-lsp pin.
- ci.yml must defer to `lune run check` and not re-run any gate.
- default.project.json must keep mounting src/shared where requires expect
  it. ServerScriptService.shared is load-bearing for every
  require("../../shared/...") and renaming it breaks them at runtime, which
  no other gate catches. Both mounts are now named `shared` consistently.

ROB_ALLOW_CONFIG_DRIFT=1 downgrades parity failures to warnings for an
intentional, temporary divergence.

Each parity check and the override were verified by mutating the config and
confirming the gate goes red.

Docs: AGENTS.md gains the parity table, the load-bearing mount rules, the
separate-require-caches caveat, and a known-gaps list (no session locking,
no balance ceiling). It also drops an inaccurate claim that check may skip
the types gate -- a missing tool fails a gate, it never passes quietly. New
testing rule: a test you have not seen fail proves nothing, added after a
spec here passed with the behaviour it tested removed.

Also adds permissions: contents: read to the workflow.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@dinoderek
dinoderek merged commit 9bdb526 into main Aug 9, 2026
2 checks passed
@dinoderek
dinoderek deleted the claude/roblox-game-scaffold-x6bfd9 branch August 9, 2026 17:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants