Repository navigation
Scaffold Roblox game project with local-first quality gates - #1
Merged
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)roblox.yml(hermetic lint runs)Quality gates —
lune run check(same gates in CI)stylua --check src tests luneselene src tests lunelune run analyze— strict luau-lsp analysis: Roblox pass (downloadedglobalTypes.d.luau+ Rojo sourcemap) and a Lune pass for tests/scriptslune run test— 21 unit tests under Lune with vendored tiniest (BSD-3)rojo buildsmoke testExample slice (the architecture to copy)
src/shared/Currency/Wallet.luau— pure logic, no Roblox APIssrc/server/Services/CurrencyService.luau— dependency-injected orchestration, tested againsttests/helpers/FakeDataStore.luausrc/server/Adapters/DataStoreAdapter.luau+ composition root — the only Roblox-touching files, kept logic-freedefault.project.jsonmountssrc/sharedinto both ReplicatedStorage and ServerScriptService so relative string requires resolve identically in the engine and LuneAgent 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.mdimports it via@AGENTS.mddocs/cloud-integration-tests.mdstubs the future Open Cloud Luau Execution harnessTest plan
🤖 Generated with Claude Code
https://claude.ai/code/session_01HfzdcKErige9QKVtsMtQoY
Generated by Claude Code