Skip to content

Getting started

Install the pinned toolchain with tools/setup.py, sync the project into Studio with Rojo, and press Play to run the spec gate and boot both kernels.

The repository runs as cloned. The boot scripts are wired, the Game layer is in place with its default services, and pressing Play in Studio runs the spec gate and boots a kernel on the server and on each client. Getting there takes three commands. After that, game work happens in src/*/Game and ends with python tools/check.py passing. The Workspace overview covers that layer in depth.

  • Python 3.10 or newer. tools/setup.py, tools/check.py and tools/scaffold.py are Python scripts.
  • The Rojo plugin in Roblox Studio, to sync the filesystem project into a place.
  • Access to the repository. It is private and granted individually (see the license note on the Introduction).
  • aftman is optional. tools/setup.py uses it when it is on PATH.
Terminal window
python tools/setup.py # once per machine: pinned Rojo, StyLua, Selene, luau-lsp, Luau runtime, type definitions
rojo serve ChloeKernel.project.json # then connect the Rojo plugin in Studio
python tools/check.py # after every change; must end with CHECK PASSED

tools/setup.py reads the tool pins from aftman.toml (Rojo 7.4.4, StyLua 2.0.2, Selene 0.28.0, luau-lsp 1.68.0). With aftman on PATH it runs aftman install. Without it, it downloads the matching release zips into .tools/bin. It also installs the standalone Luau runtime pinned in CI (0.737), downloads .tools/globalTypes.d.luau for luau-lsp, and adds /.tools/ to .gitignore. Pins that are already installed are skipped without network access; --force reinstalls everything. tools/check.py puts .tools/bin on its own PATH, so add that folder to your shell PATH only if you want to run rojo directly as in the second command.

ChloeKernel.project.json maps src/Shared to ReplicatedStorage, src/Server to ServerScriptService and src/Client to StarterPlayer.StarterPlayerScripts. Workspace, Lighting, terrain, models and UI are authored in Studio and live in the place file, not on disk.

python tools/check.py runs exactly what CI runs, 16 steps: StyLua, Selene, the directive linter, catalog freshness, both Rojo builds, strict luau-lsp type analysis, every Luau regression runner, the vendor tool test and the tooling tests. --quick runs the inner-loop subset and --fix formats with StyLua first. See check.py for each step and how to read a failure.

The server script src/Server/Main.server.luau runs these steps in order:

  1. In Studio, the spec gate runs the shared suite (ReplicatedStorage.ChloeKernel.Tests) and the server suite (ChloeKernelServer.Tests). A failure stops the script before the kernel boots. The play session keeps running so the Output window stays readable. If src/Server/Game/Tests exists, the game suite runs next and gates the boot the same way.
  2. ServerKernel.boot() creates the kernel, starts a session for every player who joins, and binds one BindToClose for the close phase.
  3. src/Server/Bootstrap.luau calls require(script.Parent.Game)(kernel). Game/init.luau builds the channel handles from Game.Channels with Registry.server, sets them as Net on every service listed in Game/Registry.luau, and registers each service.
  4. Kernel:start() runs every service init, then every start, in dependency order, and prints the boot line.
  5. Diagnostics for the F8 debug panel start in Studio, or in a live place whose ReplicatedStorage has the ChloeKernelDebug attribute set to true.
  6. In Studio, the gate runs kernel:securityAudit() and fails on any High finding. After the first player joins (not in Run mode), it drives hostile payloads through every channel’s validators and fails if a validator throws.

The client script src/Client/Main.client.luau is shorter. In Studio it runs the client game specs in src/Client/Game/Tests when that folder exists and stops before boot if any fail. Then it calls Kernel.boot(), runs src/Client/Bootstrap.luau (which loads Game with Registry.client handles and registers every controller), calls start(), and attaches the F8 debug panel, which stays off outside Studio unless ChloeKernelDebug is set.

A clean checkout of v0.9.1 with the Unreleased fixes prints one [TestKit] PASS line per suite with its test count, spec module count and time (435 tests in the shared suite and 809 in the server suite, 1,244 in total), then the boot lines and the fuzz summary:

[TestKit] PASS: 435/435 tests across 51 specs in <ms>ms
[TestKit] PASS: 809/809 tests across 56 specs in <ms>ms
[ChloeKernel 0.9.1] Server booted 1 services in <ms>ms
[ChloeKernel 0.9.1] Client booted 2 services in <ms>ms
[Fuzz] <channels> channels, <cases> cases, validators only, 0 validator errors

The server boots one service by default (ComponentService) and each client boots two (ComponentController and UIController). The version in the boot line is Kernel.Version, and start() also writes it to the ChloeKernelServer or ChloeKernelClient attribute on ReplicatedStorage for external tooling.

Game code lives in three Game folders. The Bootstraps load them between kernel boot and start, and nothing else in src/ needs to change for a game.

Folder Studio destination Holds
src/Shared/Game ReplicatedStorage.Game Channels.luau (every network channel) and Config/ (client-visible tunables). Exploiters can read all of it.
src/Server/Game ServerScriptService.Game init.luau, Registry.luau, Services/, and Components/, Config/, Data/, Tests/ as you add them.
src/Client/Game StarterPlayerScripts.Game init.luau, Registry.luau, Controllers/, and Screens/, Components/, Tests/ as you add them.

Main.server.luau, Main.client.luau and the two Bootstrap.luau files own boot ordering. Leave them alone unless a framework driver must attach before services. The client Bootstrap also attaches the framework’s simulation control window (SimPanel); its comment says to delete those two lines for a real game. The full layout, including tools/, docs/agents and templates/, is in Project layout.

Generate files instead of writing boilerplate. scaffold.py adds the module, registers it in Registry.luau after its dependencies, and creates a spec.

Terminal window
python tools/scaffold.py service Wave # src/Server/Game/Services/WaveService.luau + Registry + Tests/WaveService.spec.luau
python tools/scaffold.py controller Wave # src/Client/Game/Controllers/WaveController.luau + Registry + Tests spec

Declare the channels once. Server and client both require this module, so the schemas cannot drift. A client-to-server channel needs a RateLimit and either Validate rules or Open = true; the directive linter (CK012) fails the check otherwise.

src/Shared/Game/Channels.luau
--!strict
-- Every network channel the game uses.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Registry = require(ReplicatedStorage.ChloeKernel.Net.Registry)
return Registry.define({
Wave = { Kind = "Intent", Schema = { "NumberU8" }, RateLimit = 1, Validate = { [1] = { Range = { 1, 3 } } } },
Waved = { Kind = "State", Schema = { "String", "NumberU8" } },
})

The service reads its channels through self.Net, which Game/init.luau injected before registration. By the time the handler runs, the session exists, the rate limit has passed and the Range rule has run.

src/Server/Game/Services/WaveService.luau
--!strict
-- WaveService: relays emote waves to every player.
local WaveService = {
Name = "WaveService",
Dependencies = {},
}
function WaveService.init(self: any, kernel: any)
self.Kernel = kernel
end
function WaveService.start(self: any)
self.Net.Wave.on(function(session: any, style: number)
self.Net.Waved.broadcast(session.Player.DisplayName, style)
end)
end
function WaveService.stop(_self: any) end
return WaveService

The controller binds the action on keyboard, gamepad and touch, and owns its connections through a scope that stop() closes.

src/Client/Game/Controllers/WaveController.luau
--!strict
-- WaveController: sends waves and shows the ones other players send.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local InputDriver = require(ReplicatedStorage.ChloeKernel.InputDriver)
local WaveController = {
Name = "WaveController",
Dependencies = {},
}
function WaveController.init(self: any, kernel: any)
self.Kernel = kernel
self.Scope = kernel:createScope()
end
function WaveController.start(self: any)
local Input = self.Scope:add(InputDriver.attach(self.Kernel))
Input:bindAction("Wave", {
Keyboard = Enum.KeyCode.G,
Gamepad = Enum.KeyCode.DPadUp,
TouchButton = true,
Handler = function(state: Enum.UserInputState)
if state == Enum.UserInputState.Begin then
self.Net.Wave.fire(1)
end
end,
})
self.Scope:add(self.Net.Waved.on(function(name: string, style: number)
print(`{name} waved (style {style})`)
end))
end
function WaveController.stop(self: any)
self.Scope:destroy()
end
return WaveController

Run python tools/check.py, then press Play. The game suite now prints its own [TestKit] PASS line, and the boot lines count two services on the server and three on each client. Services and controllers and Channels go further. Claude Code users can run the /new-game skill, which walks the same design, plan, scaffold and check pipeline from a game idea (Agent workflow).

templates/ holds three small complete games: a team gun arena, a wizard duel and a checkpoint obby. Each is one server module and one client module built on framework APIs only. They show kits configured and wired together with real gameplay attached.

Every tagged release ships two models: ChloeKernel-<tag>.rbxm, the full project from build.project.json (framework, specs, TestKit, Simulation, boot scripts and the Game skeleton), and ChloeKernelRuntime-<tag>.rbxm, the framework only, built from runtime.project.json without specs, benches, TestKit or Simulation. Insert one into a place to pin an exact version instead of syncing main, or build them locally:

Terminal window
rojo build build.project.json -o ChloeKernel.rbxm
rojo build runtime.project.json -o ChloeKernelRuntime.rbxm

Pushing a v* tag runs the CI gates, builds both models and cuts the release from the matching CHANGELOG.md section. Releases and vendoring covers the runtime model and tools/vendor.py for keeping a vendored copy in sync without overwriting local edits.

  1. Read Core concepts: the kernel, services, sessions, hooks, the Bus, scopes and the close phase.
  2. Read AGENTS.md in the repository and the Workspace overview. Both apply to people as much as to agents.
  3. Skim the Cookbook and docs/agents/Catalog.md so you know what already exists before writing it.
  4. Before shipping, read Fuzz and security audit and run kernel:securityAudit() on the finished channel set.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.