brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
Convert a Nevermore Luau file from --!nonstrict (or untyped/--!nocheck) to --!strict, adding the project's explicit type annotations and fixing every type error the checker reports. Use this whenever the user asks to "strictly type", "add types to", "make strict",
Claude Code
$ npx -y skills add Quenty/NevermoreEngine --skill strict-typing-luau --agent claude-code
Codex
$ npx skills add Quenty/NevermoreEngine --skill strict-typing-luau -a codex -y
Cursor
$ npx skills add Quenty/NevermoreEngine --skill strict-typing-luau -a cursor -y
Gemini CLI
$ npx skills add Quenty/NevermoreEngine --skill strict-typing-luau -a gemini-cli -y
GitHub Copilot
$ npx skills add Quenty/NevermoreEngine --skill strict-typing-luau -a github-copilot -y
OpenCode
$ npx skills add Quenty/NevermoreEngine --skill strict-typing-luau -a opencode -y
Windsurf
$ npx skills add Quenty/NevermoreEngine --skill strict-typing-luau -a windsurf -y
name: strict-typing-luau description: Convert a Nevermore Luau file from --!nonstrict (or untyped/--!nocheck) to --!strict, adding the project's explicit type annotations and fixing every type error the checker reports. Use this whenever the user asks to "strictly type", "add types to", "make strict", "type-annotate", "convert to --!strict", or clean up the typing on a .lua/.luau file in this repo — including when they just select a file with a `--!nonstrict` header and say "type this" or point you at a legacy module. Also use it when a strict file is throwing luau-lsp type errors and the user wants them resolved following Nevermore conventions.
Convert a file to `--!strict` and make it pass the type checker cleanly. This is mostly **mechanical pattern application** — move fast and correctly. The checker is your fast feedback loop; lean on it instead of guessing.
`setmetatable`): flip the header, give every function typed params/returns, run single-file analyze. Usually no `export type` block needed.
`export type` block. Follow the patterns below and `references/conventions.md`.
Luau can't infer fields through `setmetatable`, so strict mode turns that blind spot into errors. The fix is an explicit `export type` block enumerating every `self` field, plus dot-syntax methods that name `self`. Flipping the header without these just produces a wall of errors — supplying the types the checker can't infer *is* the job.
`luau-lsp analyze` is the same engine as `lint:luau`, pointed at one file (~2.5s). One-time setup if `sourcemap.json` / `globalTypes.d.lua` are missing at repo root: `npm run prelint:luau`.
luau-lsp analyze --sourcemap=sourcemap.json --base-luaurc=.luaurc \ --defs=globalTypes.d.lua --flag:LuauSolverV2=false --ignore='**/node_modules/**' \ src/<package>/src/<Realm>/<File>.lua
Clean = only the `[INFO] Loading...` line. `LuauSolverV2=false` is required (repo pins the old solver). **Never drop `--defs=globalTypes.d.lua` or `--base-luaurc=.luaurc`** — without `--defs` the analyzer loses the Roblox global declarations and reports *false* `Unknown global 'tick'/'time'` (and similar) errors while still resolving `game`/`Enum`, which looks like a real conversion bug but isn't. If you see unknown-global errors only on deprecated globals, you forgot `--defs`; confirm with `npm run lint:luau` (which always passes the flag) before "fixing" anything. Iterate until clean, then run `npm run lint:luau` **once** as the final gate — single-file analyze can't see files that depend on *yours*, and tightening a type ripples to subclasses and callers. Triage new downstream errors: pre-existing → leave & flag; your type is genuinely too tight for the real contract → loosen *your* type (often `T?` not `T`); a small obvious follow-on → fix it.
The gate is **both** `lint:luau` (types) **and** `lint:selene` (lints), which exits non-zero. Dot-syntax conversion routinely trips selene even when analyze is clean: an `unused_variable: self` (method body ignores the now-explicit param → rename it **`_self`**) or a `shadowing` from an Rx escape `local X = X :: any` inside a function (→ cast at the source, `local X: any = require("X")`). See the "selene" section in `references/conventions.md`.
**Class with a parent (the common case):**
--!strict
local require = require(script.Parent.loader).load(script)
local BaseObject = require("BaseObject")
local MyClass = setmetatable({}, BaseObject)
MyClass.ClassName = "MyClass"
MyClass.__index = MyClass
export type MyClass = typeof(setmetatable(
{} :: {
_serviceBag: ServiceBag.ServiceBag, -- EVERY self field, with its type
_enabled: ValueObject.ValueObject<boolean>,
},
{} :: typeof({ __index = MyClass })
)) & BaseObject.BaseObject -- intersection pulls in inherited _maid, _obj, etc.
function MyClass.new(serviceBag: ServiceBag.ServiceBag): MyClass
local self: MyClass = setmetatable(BaseObject.new() :: any, MyClass)
self._serviceBag = assert(serviceBag, "No serviceBag")
return self
end**Methods use dot syntax with explicit `self`** (colon syntax loses the `self` type in strict mode). Callers still write `obj:Method()`; only the definition changes:
function MyClass.GetEnabled(self: MyClass): boolean return self._enabled.Value end
The patterns above assume the ordinary metatable (`__index = MyClass`, default `__newindex`). Some classes define a **custom `__index` and/or `__newindex` function** that intercepts field access — often to expose computed keys (`.Value`, `.Changed`) and to **`error()` on any unknown key**. Grep for `(MyClass :: any).__index = function` / `.__newindex = function` before touching the constructor or any `self._field` access. For these classes, two edits that look like harmless cleanups are **runtime-breaking changes** — do not make them:
1. **Do not replace `rawget(self, "_x")` with `self._x`, or `rawset(self, "_x", v)` with `self._x = v`.** The raw calls are load-bearing: they deliberately bypass the custom metamethod. Routing the access through `self._x` fires the metamethod, which may compute a different value or `error("Bad index")` — silently for present fields, fatally for absent ones (e.g. lazy caches that start unset). Keep every `rawget`/`rawset` exactly as-is; the only allowed change is adding the receiver cast: `rawget(self, "_x")` → `rawget(self :: any, "_x")` (and `:: any` on the result if the checker complains about `any?`).
ModuleScript loader with reusable and easy unified server-client modules for faster game development on Roblox
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or…
Ask which skill or flow fits your situation. A router over the skills in this repo.