Using Cinder with AI coding agents
Cinder's documentation is built to be read by agents as well as people. Everything on this site is
available as plain Markdown or JSON, there is a ready-made context file for coding agents, and a
read-only MCP server answers documentation queries. Everything here is generated from the engine
repository at commit 47c9352 (Cinder 0.1.0).
1. Drop the agent brief into your repository #
The brief covers what Cinder is and its honest status, the container build workflow and every
pixi task you need, the layering rules, the zero-allocation rule and the Mojo 1.2.0 pitfalls that
the engine's own notes record. Claude Code reads CLAUDE.md; Codex, Cursor, Aider and most other
agents read AGENTS.md. The content is identical.
curl -fsSL https://cinder.ash10.dev/agents/AGENTS.md -o AGENTS.md
curl -fsSL https://cinder.ash10.dev/agents/CLAUDE.md -o CLAUDE.mdDownload: AGENTS.md · CLAUDE.md. The full text is at the end of this page.
2. Point the agent at the docs #
| What | URL | Use it for |
|---|---|---|
| llms.txt index | https://cinder.ash10.dev/llms.txt | A map of every page with one-line summaries, per the llms.txt convention. |
| Full text | https://cinder.ash10.dev/llms-full.txt | Every page plus the brief in one file, for long-context models. |
| Any page as Markdown | page URL without the trailing slash plus .md, e.g. /docs/guides/ecs.md |
Fetching exactly the guide or API page the task needs. |
| JSON index | https://cinder.ash10.dev/docs/index.json | Pages, headings and 1306 API symbols with signatures, summaries and anchors. |
| Search index | https://cinder.ash10.dev/search-index.json | Section-level plain text, for local retrieval. |
| MCP | https://cinder.ash10.dev/mcp |
Search and page tools over Streamable HTTP (below). |
Every HTML page also advertises its Markdown twin with <link rel="alternate" type="text/markdown">,
and the server returns the Markdown when a request sends Accept: text/markdown. Each docs page has
View as Markdown, Copy page as Markdown and Copy agent prompt buttons at its foot.
3. Connect the MCP server #
The site runs a small, stateless, read-only MCP server next to the static pages. It needs no key and
stores nothing. Tools: search_docs (ranked hits across pages, headings and API symbols),
get_page (a page or one section as Markdown), get_symbol (signature and summary of an API
symbol, e.g. CommandBuffer or cinder.physics.character.CharacterController) and list_pages.
Every page is also exposed as an MCP resource.
Claude Code:
claude mcp add --transport http cinder-docs https://cinder.ash10.dev/mcpClients configured with JSON (Cursor, Claude Desktop through a remote connector, and others):
{
"mcpServers": {
"cinder-docs": { "type": "http", "url": "https://cinder.ash10.dev/mcp" }
}
}Why an MCP endpoint and not only static files: the static files already cover agents that can fetch
URLs, but an MCP server lets an agent search the API by symbol name without loading the whole corpus.
It is served by the same Node process as the pages (the ash10 panel runs Node), it is read-only and
stateless, and it can be switched off with MCP_ENABLED=0 without affecting anything else.
4. Prompts that work #
Each prompt assumes the brief is in the repository root. Copy, then fill in the brackets.
Start a new game on Cinder:
Read AGENTS.md, then https://cinder.ash10.dev/docs/tutorial.md and https://cinder.ash10.dev/docs/examples/first-game.md.
Create a new game in games/[name]/ modelled on examples/first_game: a struct implementing the Game trait
(start, fixed_update, update, draw_ui) run by cinder.app.App. The game is: [describe it].
Use only public engine APIs, keep physics in fixed_update, keep every per-frame path allocation-free,
add a headless test like tests/examples/test_first_game.mojo, and run it with
tools/in_container.sh. Report exactly which commands you ran and their results.Add a feature to an engine subsystem:
Read AGENTS.md. I want to change cinder/[module]: [the change].
Before editing, read https://cinder.ash10.dev/docs/guides/[module].md, https://cinder.ash10.dev/docs/api/[module].md and the ADRs it links.
Respect the layering rules (no imports from higher layers, never hand-edit cinder/ffi), add docstrings to
any new public symbol, add or extend a test under tests/[module]/, then run pixi run lint, format-check,
api-docs-check and the module's test task in the container. Tell me if the change needs an ADR.Review a change for the zero-allocation rule:
Read AGENTS.md, section "The zero-allocation rule", and https://cinder.ash10.dev/docs/mojo-notes.md, section
"Allocation facts measured in Phase 8". Review the diff below for anything reachable from App.run_frame
that allocates per frame (List growth, list literals, long String building, new Lists in loops).
For each finding give the file, line, why it allocates and an allocation-free rewrite. Then tell me to run
tools/in_container.sh pixi run test-zero-alloc to confirm.
[paste the diff]Fix a Mojo compile error:
Read AGENTS.md, section "Mojo 1.2.0: what bites", and https://cinder.ash10.dev/docs/mojo-notes.md. This code fails to compile
under Mojo 1.2.0 (pinned; do not suggest nightly features or pre-1.0 syntax such as fn, let, alias, inout or
UnsafePointer). Explain the error and give a fix that also passes tools/lint_legacy_mojo.sh.
[paste the code and the compiler output]Answer a question from the docs only:
Use the cinder-docs MCP server (or https://cinder.ash10.dev/llms.txt if it is not connected). Answer from the documentation
only and cite the page URL and section anchor for every claim. If the docs do not say, say so.
Question: [question]5. Ground rules to give any agent #
- Cinder is pinned to Mojo 1.2.0. Much of what models remember about Mojo (
fn,let,alias,inout,UnsafePointer,@parameter if) is removed or banned here. - Linux x86-64 is the only verified platform. Do not let an agent claim macOS support or performance budgets: the status report is the source of truth.
- Mojo has no Windows build; on Windows everything runs through
tools/in_container.sh. - The generated bindings in
cinder/ffiand the generated API reference indocs/apiare never edited by hand.
The brief #
This is the exact content of /agents/AGENTS.md.
# AGENTS.md: working on or with the Cinder engine
Context file for AI coding agents (Claude Code reads it as `CLAUDE.md`; Codex, Cursor and others read
`AGENTS.md`). Drop it at the root of a Cinder checkout or of a game built on Cinder. Generated by
https://cinder.ash10.dev from the engine documentation at commit 47c9352 (Cinder 0.1.0, imported 2026-11-09).
When this file and the engine repository disagree, the repository wins; re-download this file.
## What Cinder is
A 3D game engine written in **Mojo 1.2.0** (pinned; nightlies are never used) over one C11 shim,
`libcinder_native`, wrapping wgpu-native (WGSL), SDL3, Jolt (through joltc), cgltf, KTX-Software,
meshoptimizer, miniaudio, Dear ImGui and Tracy.
Honest status: Linux x86-64 is built and tested. macOS arm64 is a declared target but has **not been
built or run**. Benchmarks ran only on Mesa lavapipe (a CPU rasteriser), so the performance budgets are
recorded there, not met on reference hardware. Never claim macOS support or performance numbers that
`REPORT.md` does not state.
## Documentation for agents
- Index of every page: https://cinder.ash10.dev/llms.txt; everything in one file: https://cinder.ash10.dev/llms-full.txt
- Any docs page as Markdown: its URL without the trailing slash plus `.md`, e.g. https://cinder.ash10.dev/docs/guides/ecs.md
- Pages and every API symbol (name, signature, summary, URL) as JSON: https://cinder.ash10.dev/docs/index.json
- MCP server (read-only search and page tools): `https://cinder.ash10.dev/mcp` (Streamable HTTP)
- Read first: the guide for the module you touch (https://cinder.ash10.dev/docs/guides/), then its API page
(https://cinder.ash10.dev/docs/api/), then `docs/MOJO_NOTES.md` before writing any Mojo.
## Build, run and test
Mojo has no Windows build. On Windows (and anywhere without a native toolchain) every command runs in
the Linux toolchain container through `tools/in_container.sh`; on a native Linux host drop the prefix.
If the repository is reached through a substituted drive on Windows, set `CINDER_HOST_ROOT` to its real
path.
```bash
docker build -t cinder-toolchain:1 -f docker/Dockerfile.toolchain docker # once
tools/in_container.sh pixi install # once
tools/in_container.sh pixi run native # build libcinder_native (Release) + CTest
tools/in_container.sh pixi run test # lint, API doc check, every Mojo test, X11 + Wayland spikes
tools/in_container.sh pixi run format-check # mojo format, then git diff --exit-code
tools/in_container.sh pixi run lint # banned legacy Mojo forms
tools/in_container.sh pixi run api-docs-check # every public symbol has a docstring
tools/in_container.sh pixi run api-docs # regenerate docs/api/ from docstrings
tools/in_container.sh pixi run native-sanitize # Debug shim with ASan + UBSan
tools/in_container.sh pixi run test-asan # tests against the sanitized shim
tools/in_container.sh pixi run test-zero-alloc # zero Mojo allocations per steady-state frame
tools/in_container.sh pixi run test-golden # 16 golden images (SSIM + pixel thresholds)
tools/in_container.sh pixi run example # the tutorial game (docs/TUTORIAL.md)
tools/in_container.sh pixi run test-example # the tutorial game, compiled and run headless
tools/in_container.sh pixi run sample # the sample game, windowed
tools/in_container.sh pixi run sample-headless # the sample game, offscreen autopilot
tools/in_container.sh pixi run bench-all # stress + sample benchmarks, 10% regression gate
tools/in_container.sh pixi run -e bindgen bindgen # regenerate cinder/ffi from the C headers
```
A single test program runs as `mojo run -I . tests/<area>/test_<name>.mojo` with
`CINDER_NATIVE_LIB=native/build/release/lib/libcinder_native.so` set (see the matching `pixi.toml` task).
There is no `mojo test` subcommand in 1.2.0. Every Mojo build uses `--Werror`. Headless runs
(`--headless`) need no display and are what tests, CI and benchmarks use.
## Layering rules (never break these)
Dependencies point downward only: a module imports from layers below it, never from above.
```
sample/ (and your game) -> cinder/app -> cinder/tools
cinder/{render,physics,anim,audio,input,assets,scene}
-> cinder/ecs, cinder/gfx, cinder/platform
-> cinder/core -> cinder/ffi -> native/ (libcinder_native)
```
- `cinder/ffi` is generated by `tools/bindgen` and **never edited by hand**; a layout test checks every
struct offset and enum constant against the C compiler. Change the C header, then regenerate.
- One shim, one shared library, flat C11 ABI. Every C callback lives in C and writes fixed-size events
into lock-free rings; Mojo drains them once per frame on the main thread (the pull model, ADR-0003).
Mojo code never runs on a C thread.
- `cinder/platform` is the only module with per-OS branches. `cinder/core` does no GPU or window I/O.
- Every `unsafe_` call carries a `# SAFETY:` comment. Each shared library is loaded exactly once.
- Games use public engine APIs only (the sample game is the reference: `sample/`).
## The zero-allocation rule
A steady-state frame performs **zero heap allocations from Mojo code**; `tests/perf/test_zero_alloc.mojo`
asserts it through an LD_PRELOADed counter (`pixi run test-zero-alloc`). In anything reachable from
`App.run_frame` (systems, render, animation, physics sync, your `fixed_update`/`update`/`draw_ui`):
- Keep per-frame data in persistent scratch fields that are cleared, never freed. `List.clear()` keeps
capacity; `resize(n, fill)` allocates only when growing.
- No `List[...] = [x]` literals, no growing lists, no new `String` building of long text per frame.
- Use `Array[T, N]` locals (stack storage) for small fixed-size per-call data.
- Moving a `List` out of a struct and back does not allocate (how the frame graph recycles records).
- Third-party libraries may still allocate; the rule covers Mojo code.
## Mojo 1.2.0: what bites
The compiler and the current Mojo docs win over memory: Mojo changed a lot in 1.0 to 1.2. Before
writing Mojo, read https://cinder.ash10.dev/docs/mojo-notes.md. Key forms:
- `def` is the only function keyword (`fn` removed in 1.1.0). Functions do not raise unless marked
`raises`. `var` declares every variable; there is no `let`; `alias` is gone, use `comptime`.
- Conventions: default (immutable, `imm` when explicit), `mut`, `var` (owned, caller passes `x^`), `ref`,
`out`, `deinit` (`def __deinit__(deinit self)`). `read`, `inout`, `borrowed`, `owned` are errors or banned.
- `comptime if` / `comptime for` replace `@parameter if/for`. Constraints: `where <cond> else "msg"`.
- Structs are `Movable`, not copyable, by default; declare `Copyable` and prefer `x.copy()`.
- `Pointer[T, origin]` replaces `UnsafePointer`; unsafe operations are prefixed (`unsafe_load`,
`unsafe_store`, `ptr[unsafe_offset=i]`, `unsafe_free`). `memcpy`/`memset` are gone.
- `Array[T, N]` replaces `InlineArray`; a list literal defaults to `Array`, so annotate `List` explicitly.
- Format with t-strings: `t"value={x}"`.
- **ASAP destruction.** Values die after their last *syntactic* use. A stack value handed to C by
pointer must be followed by `std.benchmark.keep(value)` after the last C call that reads it; an
`OwnedDLHandle` closes at its last use (function pointers live only in the struct owning the handle);
early returns skip a trailing `keep`, so structure such code as if/else with one `keep` at the end.
- `Optional[def(...) thin abi("C")]` is 16 bytes: nullable C function pointers are bound as opaque pointers.
- A `mut self` method cannot also take a field of `self` (mutably or not) as an argument: copy to a local
or let the method touch `self.field` directly.
- Inside a parametric struct write `Self.T` in field and method signatures.
- Struct fields are destroyed in declaration order: a struct holding a native library and objects whose
destructors call into it declares the library **last**.
- A struct storing pointers to its own fields breaks when moved: box it (`cinder/core/boxed.mojo`).
- wgpu-native aborts the process on a validation error inside `wgpuQueueSubmit`; keep copy strides
256-aligned and pass them explicitly.
- Tests: module-level `def test_*() raises:` functions, and the file ends with
`def main() raises: TestSuite.discover_tests[__functions_in_module()]().run()`.
Banned in `*.mojo` (CI lint `tools/lint_legacy_mojo.sh` fails on any of them):
`tools/lint_legacy_mojo.sh`, `fn `, `alias `, `inout`, `borrowed`, `owned `, `let `, `@parameter`, `__del__`, `InlineArray`, `StringSlice`, `UnsafePointer`, `MutUnsafePointer`, `ImmUnsafePointer`, `std.gpu`, `.load(`, `.store(`, `.free(`, `memcpy(`, `memset(`, `read `, `@parameter if`, `@parameter for`, `@register_passable`, `__comptime_assert`, `steal_data`, `.size`.
## Conventions
Right-handed, Y up, -Z forward, metres, column-major matrices, reverse-Z with an infinite far plane,
linear lighting, sRGB render targets, RGBA16F intermediates. Recoverable failures raise typed errors;
broken invariants abort with a message. The simulation is a fixed 60 Hz step (`Game.fixed_update`,
where physics happens); rendering runs once per frame (`Game.update`, `Game.draw_ui`).
A program is an `App` plus your `Game` struct (`start`, `fixed_update`, `update`, `draw_ui`). End the
game before the app, explicitly, because the game's bodies live in the app's physics world:
```mojo
game.shutdown(app)
_ = game^
_ = app^
```
## Threading
The main thread owns SDL, the window, presentation and ImGui. Everything else runs on the work-stealing
job system (one worker per physical core minus one). Shared state crosses threads only through jobs,
atomics or the locks named in `docs/THREADING.md`. Jolt steps on its own two-thread pool (ADR-0012).
miniaudio's device thread runs C only.
## How to work in this repository
- Read the relevant guide and ADR before changing a subsystem; a change to a pinned version or a
layering rule needs a new ADR.
- Add or update tests with every change; never skip a test to make a run pass.
- Public symbols need docstrings (`pixi run api-docs-check`); regenerate `docs/api/` after API changes.
- Run `pixi run format-check`, `pixi run lint` and `pixi run test` before declaring work done, and
report results faithfully, including anything not run (for example macOS).