Repository navigation
Nen v0.16.0
v0.16.0 — 2026-09-29
Release unit for v0.15.1..v0.16.0: #272 and #274 (the delivery), #278 (the landing — #274 was stacked and its merge never reached main), and #281 (the release proposal).
Added
- canon —
nen canon mirror generate|checkrender a canon set into every surface the consumer declares, not one hard-coded rules directory.--surfaces(or the canon-values file's ownsurfaces:key) names them, and each surface's location is its own documented one:codex→AGENTS.mdas one managed block,cursor→.cursor/rules/*.mdc,antigravity→.agents/rules/*.md,claude-code→.claude/rules/*.md. A document surface keeps the consumer's own prose outside the block byte for byte; a per-file surface gets one file per canon file. Only changed bytes are written, and a marked file whose canon source is gone is deleted as an orphan. - canon — the marker is the ownership claim. A destination that exists and carries no generated-from marker was written by hand, so the whole run is refused (exit 2) before the first byte is written on any surface — a half-mirrored consumer is worse than an unmirrored one. An unmarked file with no canon source is the consumer's own: never deleted, reported
foreign, and never drift. - canon —
mirror checkis the CI half: it renders in memory and diffs each surface's committed copy, classifying every fileok,MISSING,EXTRA,STALE(the marker names another source, ref or scenario — generated once, never regenerated after the pin moved),HAND-EDITED, orFOREIGN. Exit 1 iff any surface has drift;--markdown-outalso writes it as aSurface | File | Issuetable. - canon —
nen canon pin --repo <consumer>reads the canonical handbooks repository and the tag it is pinned to out of the consumer's ownnen/repos.json— thepinnedfield on itsmaintained_toolsentry. It exists so a sync can check the canon out at that tag before rendering, and somirror checkhas a pin to hold the mirror to. Exit 1 when no pin is recorded (naming the field to record it in), when several tools are pinned and--sourcedoes not disambiguate, or when the recorded pin is not tag-shaped. - schema —
nen/repos.jsonmaintained_tools[]entries may carrypinned, surfaced as the registry'stoolPins. This is the one exception to "only the slug is modelled": CON-13's canon pin has to be data a sync and a drift check can read, not prose in a$comment. It lives onmaintained_toolsbecause that is the list whose entries classify as canon, and it reuses the spellingconsumers[]already uses, so one word means one idea. - surface — the rules rendering refuses a set that will not fit where it is going, at generate time rather than at read time: Antigravity's 24,000-byte rules limit, and Codex's
project_doc_max_bytes(32 KiB by default, set in.codex/config.toml). A limit discovered by a surface silently truncating is a limit discovered too late. - surface —
AGENTS.mdcarries a documented double-load caveat: Cursor and Antigravity read it at any level, and Claude Code (v2.1.277+) reads it when the consumer has noCLAUDE.md,.claude/CLAUDE.mdorCLAUDE.local.mdat or above the root — so a consumer rendering another surface beside Codex loads the canon twice unless it keeps aCLAUDE.md. The Claude Code setting that reads both is honoured in user and managed scope only and cannot ship in a repository.
Changed
- docs — the canon family is redirected off the frozen reference implementation.
README.md,docs/USAGE.md,docs/STACK-MATRIX.mdand the Gradle/Android profile now name the canonical handbooks repository rather thanbankai-core, which is no longer where a consumer's handbooks come from.
Fixed
- parse —
nen canon pinwas in noNEN_VERB_TABLErow, so izanami fail-closed it as unclassified. A caller that classifies before it runs would refuse the one verb whose whole job is to read a pin. ClassifiedRO: it reads the consumer's registry and writes nothing. In the same rowcanon mirror generatewasMUTand its text named--out-dir, a flag the verb no longer takes — it isDRY, because--dry-runreports every write and performs none, so the dry form is a read and belongs inside a watch. - canon — a canon body containing a bare section-marker line is refused when the sources are read. The document surface delimits its sections with that exact shape and cannot match it first-line-only the way the filename guard does, because one block carries many bodies in sequence — so such a body would be written by
generateand read back as split by the very nextcheck, the block failing against itself. The refusal names file and line and gives the three ways out (indent it, fence it, or list the file under--not-mirrored). An indented or fenced occurrence is untouched.
Breaking / consumer notes
-
No repin: the compatibility floor stays
0.7. Everything here is additive — new verbs, a new optional registry field, and new refusals on inputs that previously produced a broken mirror. No existing pin is refused by this release, and nothing that worked stops working.A consumer that wants the new verbs must still pin to
v0.16.0deliberately:canon pinandcanon mirror generate|check --surfacesexist in no earlier tag, so a caller pinned below this one refuses them as unknown rather than failing oddly. That is why this tag is cut now rather than at the next convenient moment —hatsu:limbocalls exactly those verbs and cannot run until a release carries them.