AGENTS.md · git:20261030.08f6a14 · 2026-10-30 · sha256 20be8c45510b255e
AGENTS.md git:20261030.08f6a14A
Immutable. This exact content never changes and is served at /api/v1/blob/20be8c45510b255e.
# Nino
Source-generated binary serializer for C#: .NET (netstandard2.0 to net10.0), Unity (IL2CPP, Burst) and HybridCLR,
with runtimes that read and write the same bytes in Go, Rust and TypeScript (generated by nino-gen). Published to
NuGet (`Nino`, `Nino.AspNetCore`, `Nino.Codegen.Tool`, ...), OpenUPM, crates.io, npm and as a Go module. Public API,
code comments, commits and docs are in English; the docs site is English and Chinese.
## Commands
```bash
export PATH=$HOME/.dotnet:$HOME/go/bin:$PATH DOTNET_ROOT=$HOME/.dotnet # cloud sessions
dotnet build # the solution (Nino.slnx), 0 warnings
dotnet test tests/Nino.UnitTests # the unit tests (CONTRIBUTING.md has the matrix)
QUICK=1 FILTER='*SimpleTest*' benchmarks/run.sh # benchmarks while developing (`benchmark` skill)
python3 benchmarks/asm.py compare origin/main # what a change did to the machine code
tools/agents.sh # refresh the indexes below (--check in CI)
```
The Go, Rust and TypeScript runtimes have their own commands (`runtimes/*/AGENTS.md`). The website is the separate
repository JasonXuDeveloper/Nino.Docs (https://nino.xgamedev.net; its own AGENTS.md).
**Instruction files.** `AGENTS.md` holds the instructions (the open standard Codex, Cursor, Copilot and other agents
read); each `CLAUDE.md` next to one is the single line `@AGENTS.md`, which makes Claude Code import it. Edit only
`AGENTS.md`. Before you read or change files in a directory listed below, read its `AGENTS.md` in full (file map and
local rules): Claude Code loads it when it reads a file there, other agents may not. The two tables are generated by
`tools/agents.sh` from each file's `> Read when:` line and each skill's description; run it after adding or changing
one (CI runs `tools/agents.sh --check`, which also checks that every file is in its directory's map).
<!-- agents-index:start -->
| Directory | Read its AGENTS.md when |
|---|---|
| [`benchmarks/`](benchmarks/AGENTS.md) | adding, changing or running a .NET benchmark, or a script that turns benchmark results into the README, docs or a release body |
| [`conformance/`](conformance/AGENTS.md) | changing the wire format, a layout, a malicious-input check or a sample type, or making a runtime read the shared conformance corpus |
| [`docs/`](docs/AGENTS.md) | changing the Markdown documentation in docs/ (the wire format spec, the design, codegen, diagnostics, pitfalls, benchmarks), or deciding where a piece of documentation belongs |
| [`runtimes/go/`](runtimes/go/AGENTS.md) | reading or changing the Go runtime (runtimes/go, package nino), its string kernels, struct tags, sample codecs or Go benchmark |
| [`runtimes/rust/`](runtimes/rust/AGENTS.md) | reading or changing the Rust runtime (runtimes/rust: crates nino-serialization and nino-serialization-derive), its SIMD kernels, tests or benchmarks |
| [`runtimes/typescript/`](runtimes/typescript/AGENTS.md) | reading or changing the TypeScript runtime (runtimes/typescript: src/nino.ts, its WebAssembly kernels, tests or benchmark) |
| [`src/Nino.AspNetCore/`](src/Nino.AspNetCore/AGENTS.md) | reading or changing the ASP.NET Core integration (package Nino.AspNetCore) |
| [`src/Nino.Codegen/`](src/Nino.Codegen/AGENTS.md) | reading or changing nino-gen (the TypeScript, Go and Rust code generator), its templates, its options or the Nino.Codegen build package |
| [`src/Nino.Core/`](src/Nino.Core/AGENTS.md) | reading or changing anything in `src/Nino.Core` (the runtime: the public API, formatters, Reader/Writer, settings, polymorphism, pooling, the internal fast paths) |
| [`src/Nino.Generator/`](src/Nino.Generator/AGENTS.md) | reading or changing the source generator, the emitted formatter code, the analyzer, the code fixes or the Roslyn 4.8 / 4.12 builds |
| [`src/Nino.Immutable/`](src/Nino.Immutable/AGENTS.md) | reading or changing the System.Collections.Immutable formatters (package Nino.Immutable) |
| [`src/Nino.Unity/`](src/Nino.Unity/AGENTS.md) | changing the Unity package (com.jasonxudeveloper.nino), its Burst kernels, the Unity tests or src/Nino.UnityTypes |
| [`tests/`](tests/AGENTS.md) | adding or changing a test, or finding where a feature, a platform or hostile input is tested |
<!-- agents-index:end -->
## Layout
```
src/ the packages: Nino.Core (runtime), Nino.Generator (+ CodeFixes, Roslyn4.8/4.12 builds), Nino.Immutable,
Nino.AspNetCore, Nino.Codegen (+ .Tool: nino-gen), Nino.Unity (the Unity project and package),
Nino.UnityTypes; src/Nino is the meta package
tests/ unit, runtime-internals, AOT, fuzz, ASP.NET Core and codegen tests (tests/AGENTS.md)
benchmarks/ BenchmarkDotNet projects and the scripts that run, compare and report them
runtimes/ go, rust, typescript: the wire format outside .NET
conformance/ the corpus every runtime reads and writes (valid and malicious cases)
docs/ design, wire-format spec, codegen, diagnostics, pitfalls, benchmark notes (Markdown in the repository)
tools/ CodeGen (generated formatter families), agents.sh
build/ the props users import to turn the generator on
.github/ workflows, the setup-dotnet action, release and test scripts
skills/ the public skill nino-serialization: how AI agents use Nino (`npx skills add JasonXuDeveloper/Nino`)
.claude/skills/ the development skills (internal); .agents/skills links here
```
## Workflows
Repeatable procedures live in `.claude/skills/<name>/SKILL.md`: Claude Code loads them as skills, `.agents/skills`
links to the same files for other agents, and anyone can follow them as checklists. A change is done when the
`feature` skill's checklist is; it lands through `verify`; a review of the codebase is an `audit` round.
<!-- skills-index:start -->
| Skill | What it does | Use it |
|---|---|---|
| [`audit-analyzer`](.claude/skills/audit-analyzer/SKILL.md) | Audit Nino's diagnostics (NINO rules in src/Nino.Generator/Analysis), the suppressor and the code fixes | when adding or changing a rule or code fix, when users hit a runtime error a rule could have caught, or when a rule fires wrongly |
| [`audit-ci`](.claude/skills/audit-ci/SKILL.md) | Audit and change Nino's GitHub Actions (ci.yml, runtimes.yml, unity.yml, fuzz.yml, codeql.yml, release.yml, report.yml, ab-bench.yml and the test branch's A/B workflows) | when changing a workflow, when CI is slow or red, or before a release |
| [`audit-code`](.claude/skills/audit-code/SKILL.md) | Audit Nino's code for cleanliness and maintainability and fix it | for a cleanup round, after large merges or a burst of optimization work, or when code was written in a hurry |
| [`audit-core`](.claude/skills/audit-core/SKILL.md) | Audit Nino's runtime libraries (src/Nino.Core, Nino.Immutable, Nino.AspNetCore, Nino.UnityTypes) for correctness and design | for a core review round, before a release, or when a bug shows up in serialization, deserialization into existing instances, pooling or registration |
| [`audit-docs`](.claude/skills/audit-docs/SKILL.md) | Audit and write all of Nino's docs | once the code is final (the docs pass), after a user-facing change lands, and before a release |
| [`audit-generated-code`](.claude/skills/audit-generated-code/SKILL.md) | Audit what the generators write | when changing an emitter or a template, when generated code is slow, large or hard to read, or when a type's serializer is slower than a hand-written one |
| [`audit-generator`](.claude/skills/audit-generator/SKILL.md) | Audit the Nino source generator itself (src/Nino.Generator and its Roslyn 4.8/4.12 builds) | when changing the generator, adding a way to declare a type, when a type shape does not compile, or when builds or the IDE are slow |
| [`audit-layout`](.claude/skills/audit-layout/SKILL.md) | Audit and fix Nino's directory structure | when adding a project, folder or script, when the tree feels messy, after a large merge, or when tools/agents.sh --check fails |
| [`audit-naming`](.claude/skills/audit-naming/SKILL.md) | Audit every name in Nino | in every audit round and on every new public name before it lands. Report first; public renames are the owner's call |
| [`audit-perf`](.claude/skills/audit-perf/SKILL.md) | Find the next performance win in Nino (.NET, Unity/Burst, Go, Rust, TypeScript) | for a performance round, when a case is slower than a competitor or than the previous main, or when asked to optimize further |
| [`audit-redundancy`](.claude/skills/audit-redundancy/SKILL.md) | Find and remove redundancy everywhere in Nino | in every audit round, after large merges or a burst of feature work, and whenever something looks familiar ("didn't we already have this?") |
| [`audit-runtimes`](.claude/skills/audit-runtimes/SKILL.md) | Audit Nino's runtimes outside .NET (runtimes/go, runtimes/rust, runtimes/typescript) and nino-gen (src/Nino.Codegen), which writes code for them | when changing a runtime or a template, when a runtime is slower than a competitor or than the previous main, or for a runtimes review |
| [`audit-security`](.claude/skills/audit-security/SKILL.md) | Audit and extend Nino's defenses against hostile input in every runtime (.NET, Go, Rust, TypeScript and the code nino-gen writes) | for any change to a read path, a new type or collection, a new setting, or a security review |
| [`audit`](.claude/skills/audit/SKILL.md) | Run an audit round of Nino with parallel agents in worktrees | when asked to review the whole codebase, to clean up, to find what is left, or to "do another round" |
| [`benchmark`](.claude/skills/benchmark/SKILL.md) | How to measure Nino and what may be published | when running, adding or reading a benchmark, deciding whether a change is faster, or touching a number in README, docs or a release |
| [`feature`](.claude/skills/feature/SKILL.md) | The definition of done for a Nino change | when building or reviewing any change before it lands; a fresh session working on a new request follows it |
| [`optimize`](.claude/skills/optimize/SKILL.md) | Nino's optimization techniques, measured and kept | when writing or reviewing a hot path in any runtime, before committing a change to a read or write path, when a case got slower, or when a new trick is found (record it here) |
| [`platforms`](.claude/skills/platforms/SKILL.md) | Every platform Nino runs on and how to test each locally | when a change adds a platform-specific path, when something fails on one platform only, or before landing a change to a read or write path |
| [`runtime-internals`](.claude/skills/runtime-internals/SKILL.md) | How Nino reads and builds a runtime's own objects through their fields instead of the public API | when adding or changing such a path, when a collection reads or writes wrong on Mono, IL2CPP, .NET Framework or 32-bit, or when a startup check fails |
| [`verify`](.claude/skills/verify/SKILL.md) | The full Nino gate before anything reaches main or the test branch | before any push to main or the test branch, after landing other branches, and before a release |
| [`wire-format`](.claude/skills/wire-format/SKILL.md) | Everything a change to Nino's bytes must update | when a change alters what Nino writes or accepts for any type, or when a golden-bytes or conformance test fails |
<!-- skills-index:end -->
### Keep what you learn
Skills are the project's memory: a fresh session knows only what AGENTS.md and the skills say, so anything worth
reusing is written down before the task ends, in the same series of commits as the work (a `docs(skills)` commit).
- **What counts**: a rule or convention the owner states or corrects ("never write slower", "numbers only from
runners"); an optimization technique that measured faster, and one that measured slower (so nobody retries it
blind); a trap that cost time (a qemu flag, a JIT behavior, a CI failure that slipped through); a check or command
that proves something; a new place a change must reach (a test, a doc, a runtime).
- **Where it goes**: into the skill that owns that kind of work (an optimization technique, faster or slower, to
`optimize`, how to measure to `benchmark`, a new place for wire changes to `wire-format`, a docs rule to `audit-docs`, a local fact
about some files to that directory's `AGENTS.md`, a rule for every task here). Nothing fits: a new skill, with its
row in the `audit` scopes.
- **How it is written**: one bullet, the rule first in the imperative, then why, then the evidence (the case, the
numbers, the commit): "Mark every small helper a read path calls `AggressiveInlining` ... `NinoLanes.Paired` stayed a
call per load: AVX2 writes took twice as long". Extend the bullet that already covers it rather than adding a second;
a rule that turned out wrong is corrected or removed, not contradicted further down.
- **Tell the owner** in the report which skill gained what.
### Keep the skills consistent
- **A rule is stated in full once**, in the skill that owns it; other skills it affects carry a check phrased for
their job and point to the owner. After changing a skill, read the skills it names and the ones that own
neighboring files (the `audit` scopes table) and add the pointer where it is missing.
- **Put a rule where the work that needs it loads it.** A rule for every task goes here; a rule for one kind of work
goes in its skill; a directory's `AGENTS.md` holds only its file map and rules local to its files; the public skill
holds what users' agents need. Search before adding a rule: extend the existing one.
- **Descriptions decide when a skill runs**: what it does, `Use when ...`, and `Triggers:` with the paths it owns,
the symptoms, and the owner's words in English and Chinese. A skill that did not run when it should have gets the
missing trigger.
- **Split, never shrink.** A skill that has grown past one concern is split by concern (every line moves, the old
skill keeps a pointer, `audit` gains the scope, `tools/agents.sh` the index).
- **Decide, then say.** Do what clearly fits the project's rules without asking; ask only what is the owner's to
decide (a public API removal, a wire-format change nobody asked for, spending CI or license time).
## Principles (every change)
1. **Fast on every platform.** x64 (AVX-512, AVX2, SSSE3), Arm64, Arm32 and other 32-bit targets, WebAssembly and no
SIMD, Mono and Unity (IL2CPP), .NET Framework. A fast path is behind `#if` or a startup check with the shared path as
fallback, and both write the same bytes (`optimize`, `platforms`).
2. **Safe on hostile input.** Nino reads untrusted bytes: nothing may crash, read outside the input, allocate or take
time out of proportion to it, or recurse without a bound (`audit-security`), in every runtime.
3. **The bytes are the contract.** .NET, Go, Rust, TypeScript and the generated code write identical bytes; a change
to them is a wire-format change with everything `wire-format` lists.
4. **DRY and decoupled.** One implementation, one way per job, one source of truth per fact; dependencies point one
way through public seams; standard practice over a new mechanism. The standard is stated in `audit-code`.
5. **Proven.** Tests for behavior, the machine code for hot paths, runner numbers for speed (`benchmark`).
## Rules
- **Numbers**: performance claims come only from GitHub runners (`report.yml` for releases, the A/B workflows while
developing). Never publish quick, local or editor numbers. Within 5 % is on par; the docs never call Nino
"slower". v3 means 3.3.13.
- **CI stays green** on `main` and on the test branch: run the checks for what you touch before every push, never push
an experiment that fails on purpose, batch test-branch pushes (`verify`, `audit-ci`).
- **CI is a budget**: the account runs a limited number of jobs at once. One A/B run in flight across all workers,
queued lines batched into the next push, fewest lines that decide the question, and one push to `main` per series
(docs and skills commits ride along with code) (`benchmark`, "Spend CI as a budget").
- **Commits**: Conventional Commits, `type(scope): summary` and a body that says why (types `feat`, `fix`, `perf`,
`refactor`, `docs`, `test`, `ci`, `build`, `chore`; scope = area). Author:
`git -c "user.name=JasonXuDeveloper - 傑" -c user.email=jason@xgamedev.net -c commit.gpgsign=false commit`. The
message ends with `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>` only: no session link, no model id.
- **Release notes are trailers.** `.github/scripts/release_notes.py` groups commits by type and takes highlights
from trailers in the last paragraph, next to `Co-Authored-By`: `Wire-Format:` (bytes that change, for which
types), `Added:` (API, attribute, behavior), `Performance:` (what, which types, how much, runner numbers),
`Fixed:` (a user-visible bug). One or two sentences of Markdown for users; `Closes #N` in the body above them. Tests,
CI, docs and refactors get none. The docs site's changelog is written from the same trailers (`audit-docs`).
- **Landing**: `main` moves only by fast-forward
(`git fetch -q origin main && git merge-base --is-ancestor origin/main HEAD && git push -q origin HEAD:refs/heads/main`);
the test branch only by merge. No force push, no pull request unless the owner asks, never trigger a release.
- **Shell**: never bare `git stash`, `pkill -f` or `pgrep -f`; heavy commands (builds, tests, benchmarks, cargo) run
under `flock -o /tmp/nino-heavy.lock` when agents share the machine; scratch files stay out of the repository. In
cloud sessions `env` on the PATH is uv's `~/.local/bin/env` script, which runs nothing and prints nothing: write
`VAR=0 cmd` or `/usr/bin/env VAR=0 cmd`.
- **Secrets** go only into GitHub secrets, never into chat, files or logs.
- **The owner talks in Chinese**: answer in Chinese; everything committed stays English (docs: English and Chinese).