Check
One command that runs every CI gate in order, from formatting and the directive linter to Rojo builds, strict type analysis and the Luau regression runners.
python tools/check.py is the command agents run after every change and the command CI runs on every push. It runs formatting, lint, the directive linter, catalog freshness, two Rojo builds, strict luau-lsp type analysis and every regression runner, and ends with CHECK PASSED or CHECK FAILED. The decision that defines it: CI calls this script and nothing else, so a local pass on the pinned toolchain is a CI pass. AGENTS.md makes a passing check the definition of done, and forbids weakening a check, deleting a test or adding a suppression to get there. The workflow around it is on Agent workflow.
Running it
Section titled “Running it”python tools/check.py # every steppython tools/check.py --quick # inner loop: tools, format, lint, directives, catalog, types, corepython tools/check.py --fix # format src and templates with StyLua first, then checkpython tools/check.py --only types,lintpython tools/check.py --list # print the steps and exitEach step prints PASS name (1.2s) or FAIL name (1.2s). A failing step also prints the last 40 lines of its output, indented. Steps keep running after a failure, so one run shows every broken gate. The last line is CHECK PASSED (n steps) or CHECK FAILED: a, b, and the exit status is 1 when anything failed. --only with an unknown step name exits before running anything and points at --list.
Tools come from <repo>/.tools/bin (installed by tools/setup.py) or from PATH, and LUAU_BIN selects the Luau runtime. A step whose tool is missing fails at once with missing <tool>; run python tools/setup.py. Build outputs go to a temporary directory that is removed afterwards, never into the repository.
Steps in order
Section titled “Steps in order”| Step | Quick | What it runs | Fails when |
|---|---|---|---|
tools |
yes | Looks up rojo, stylua, selene, luau-lsp and luau. |
Any is missing. The output names the fix. |
format |
yes | stylua --check src templates. With --fix, stylua src templates first. |
A file is not StyLua-formatted (tabs, double quotes, 130 columns, from stylua.toml). |
lint |
yes | selene src templates with the roblox+buffer_bitops standard library from selene.toml. |
Selene exits with a failure. |
directives |
yes | python tools/directives.py. |
Any CK rule reports a finding. See Directives. |
catalog |
yes | python tools/catalog.py --check. |
docs/agents/Catalog.md differs from what the module headers generate. Rerun python tools/catalog.py. |
build |
no | rojo build ChloeKernel.project.json to a temporary .rbxl. |
The project does not build. |
runtime |
no | rojo build runtime.project.json to a temporary .rbxmx, then tools/check_runtime.py on it. |
The build fails, or the model holds development code (see Releases and vendoring). |
types |
yes | rojo sourcemap, then luau-lsp analyze over src with the Roblox definitions. |
Any type or syntax error, or a warning missing from tools/types-baseline.txt. |
core |
yes | tools/regressions/core.py |
A core regression fails. |
network |
no | tools/regressions/network.py |
A networking regression fails. |
gameplay |
no | tools/regressions/gameplay.py |
A gameplay regression fails. |
persistence |
no | tools/regressions/persistence.py |
A persistence regression fails. |
datasafety |
no | tools/regressions/datasafety.py |
A data-loss regression fails. |
presentation |
no | tools/regressions/presentation.py |
A client presentation regression fails. The step prints SKIP if the file does not exist. |
vendor |
no | python tools/test_vendor.py |
The vendor sync tool’s own tests fail. |
tooling |
no | tools/test_directives.py, tools/test_scaffold.py, tools/test_engine.py |
The linter, scaffolder or engine lookup tests fail. |
--quick runs the seven steps marked quick. It skips both builds, five of the six runners and the tool tests. It is the inner loop while you iterate.
Strict types and the baseline
Section titled “Strict types and the baseline”The types step builds a sourcemap with Rojo, then runs:
luau-lsp analyze --definitions=<globalTypes.d.luau> --base-luaurc=.luaurc --sourcemap=<sourcemap.json> --no-strict-dm-types src.luaurc sets strict mode and turns on every lint except LocalShadow. The definitions file comes from the repository root or .tools/globalTypes.d.luau. When neither exists, the step downloads it from the luau-lsp repository and saves it under .tools, so the first run needs network access.
Every diagnostic is parsed into path: Code: message with the line and column stripped. TypeError and SyntaxError always fail. Any other diagnostic fails unless that normalized form appears in tools/types-baseline.txt, so edits elsewhere in a file do not churn the baseline. A baseline entry that is no longer reported prints a note asking for its removal but does not fail. An analyzer crash with no parsable output fails. At 0.9.1 the baseline holds one entry, a TableOperations warning in Serde.luau about # on a table with string keys.
.github/workflows/ci.yml runs on pushes to main, on pull requests, and as a reusable workflow. Its one job:
- checks out the repository,
- installs the
aftman.tomlpins withok-nick/[email protected], - sets up Python 3.12,
- downloads
luau-ubuntu.zipfrom the Luau 0.737 release and verifies its SHA-256 (c3f000d0bb190301b9548f50f82ae9e217a8b5ba7b3551b038dbf28cb2e5ed32) before putting it onPATH, - runs
python tools/check.py.
The release workflow calls this workflow first and builds nothing unless it passes. See Releases and vendoring.
The pinned toolchain
Section titled “The pinned toolchain”| Tool | Pin | Pinned in |
|---|---|---|
| Rojo | 7.4.4 | aftman.toml |
| StyLua | 2.0.2 | aftman.toml |
| Selene | 0.28.0 | aftman.toml |
| luau-lsp | 1.68.0 | aftman.toml |
| Luau runtime | 0.737 | .github/workflows/ci.yml (URL and SHA-256) |
python tools/setup.py installs them into <repo>/.tools on Linux x86_64, macOS x86_64 or aarch64, and Windows x86_64:
- With
aftmanonPATH, it runsaftman install. Otherwise it downloads each pinned GitHub release zip into.tools/bin. - It installs the Luau runtime pinned in
ci.yml, verifying the SHA-256 on Linux as CI does. - It downloads
.tools/globalTypes.d.luaufor luau-lsp. - It runs Selene once on a probe file so Selene fetches its Roblox standard library now and the check stays offline later.
- It adds
/.tools/to.gitignore. - It records installed versions in
.tools/versions.jsonand skips pins that are already installed, without network access.--forcereinstalls everything.
check.py puts .tools/bin on PATH for its own run. Add it to your shell’s PATH to run the tools directly.
Claude Code hooks
Section titled “Claude Code hooks”The repository’s .claude/settings.json wires two hooks for Claude Code:
- After every
Edit,WriteorMultiEditof a.luaufile inside the project,.claude/hooks/after-edit.pyformats the file with StyLua and runs the directive linter on it. Findings exit with status 2, which hands them back to the agent to fix in the same turn. - At session start in Claude Code on the web (
CLAUDE_CODE_REMOTE=true),.claude/hooks/session-start.shrunstools/setup.py, puts.tools/binon the sessionPATHand warms Selene’s standard library cache, building a native-certificate Selene withcargowhen the session proxy blocks the release binary’s download.