Repository navigation
Nen v0.21.0
v0.21.0 — 2026-10-11
Release unit for v0.20.0..v0.21.0. It brings declared focused tests, live shu output, the refusal of unread nen/gates.json keys with nen's own round caps, per-pool runner labels, and six additions that retire Hatsu shell (canon checkout, canon pin --write, surface install/status, wc worktree, schema set, pr threads settle):
- the deliveries: #381, #385, #386, #387, #388, #392, #393, #394, #395, #396, #397, #398, #399, #400, #401.
The compatibility floor moves to 0.21.
The verb count at each merge was 128 (#386), 129 (#387), 131 (#392), 132 (#395), 133 (#397, #398) and 134 (#400) — #387 and #395 stated theirs against an older main; the net effect is 127 → 134 verbs: canon checkout, issue reconcile, surface install, surface status, runner relabel, wc worktree and schema set. pr threads settle is a subcommand and canon pin --write a flag, so neither moves the count.
Added
- shu —
--streamrelays each step's stdout and stderr live, one whole line at a time, onbuild,test,ui-test,lint,archive,release,deploy,coverageandtest-report(under--jsonthe child's stdout goes to stderr); a step still running after 30 s prints a heartbeat on stderr every 30 s,--heartbeat <s>sets the interval and--heartbeat 0without--streamrestores the old captured path (#381, closes #244). On the apple lane a growing log now proves nen is alive, not the child: use--stream, or declare astallguard. - shu —
nen shu test --select <id>[,<id>…]runs the lane's one declared focused form,project.verbs.<lane>.test.selectinnen/contract.json(exe,argv,eachwith{id},pattern, a requiredresults: { path, format }under.nen/focused/) (#393, closes #207). A bad selection, or a lane with noselect, is refused at exit 2 before anything spawns, never a fallback to full regression; a selection that executed zero tests is exit 6 withselection.executed: 0andzeroMatch: true. Old results are moved aside into.nen/focused/.previous/<run-id>/under a lock, never deleted in place, and the last 2 runs are kept. - pr —
pr body-checkwith no--requirements-fromderives the requirement set from the repository's pull-request template, naming the source on the verdict line and in--jsonsource, or refuses at exit 2 with a bootstrap pointer;--base <ref>reads the template at a commit, so a PR cannot loosen its own requirements (#385, closes #239). A derived section with no real content isEMPTY, distinct fromMISSING. - pr —
nen pr threads settle --target <owner/name> --pr <n> --from <file.json>(nen.pr.threads-settle/v0.1) settles a reviewer round in one walk plus one reply and one resolve per entry (2N + 1calls, where a loop cost4N); the whole file is checked before any write, analready-resolvedthread exits 3, a refused mutation stops the batch, and abodyFileis confined to the batch file's directory, refused on dot-segments, hard links and credential shapes (#398, closes #230). - pr —
nen pr open --assignee <login>[,…]:@meresolves throughGET /userand is printed, every login is checked assignable before anything opens, and the assignees are read back;nen.pr.open/v0.1appendsassignees,meandassigned(#398, closes #293). - pr — under
--explainand--json,pr readydiagnoses each queued required check against the runners that could take it:meta.runnerAvailability[]with a determination (online,offline-only,no-runner,github-hosted,started,environment-wait,undetermined),infrastructureStallafter 10 minutes queued, and the missing grant named when the token cannot read runners (#394, closes #224). The verdict, rows, exit codes and default output are unchanged, and the default path makes no extra read. - gates —
round_policy.minRoundsandround_policy.maxRoundsbecome nen keys, adopted under the consumer's names by the maintainer's ruling of 2026-10-04 (#388, closes #240):pr request-reviewsrefuses a bot's request pastmaxRoundsat exit 2, reading it at the PR's base and at the default branch's tip and applying the lower;pr ready --explainprints per-reviewer requested/posted counts (meta.roundCounts), never a conjunct; and the CON-32(b) rounds-owed row tells "no round at any head yet" from "pending request only" (plus "own-request"), with the verdict unchanged. - canon —
nen canon checkout --repo <consumer>(nen.canon.checkout/v0.1) resolves the canon checkout from--canon-checkout, then the row'scheckout_envvariable, then itscheckouttemplate (two new optionalnen/repos.jsonmaintained_toolsfields), and verifies it read-only: top of its work tree, origin on the consumer's host, HEAD at the pinned tag, a clean tree whatever the checkout's own configuration hides (#386, closes #294).canon mirror generate|checkaccept--stack-dir <dir> --leaf <name>in place of--rules-dir, so a mirror check is portable literal argv. - canon —
nen canon pin --repo <consumer> --write <tag>(nen.canon.pin.write/v0.1) moves exactly one value, thepinnedon the row the read path answers from, byte-preserving and atomically, after the tag resolves through the canon checkout; it reads the pin back and says the mirror must be regenerated in the same commit (#396, closes #291). It moves an existing pin only. - schema —
nen schema set --repo <path> --file <nen/…json> --pointer <ptr> --value <json>(nen.schema.set/v0.1) writes one declaration value in place, every other byte kept,$notes included, and validates the result with the file's own loader before writing (#400, closes #338). Gate changes fail closed: every pointer ingates.json/decisions.jsonand the gate scopes ofcontract.jsonandworkflow.jsonneed--gate-change, the flag on a non-gate pointer is exit 2, and a canonpinnedis refused in favour ofcanon pin --write. The$-metadata rule is now published inschema --helpand USAGE, and a fixture over all seven declaration files guards it. - schema —
nen/workflow.json→deploy.defaultTarget(nullor a target name) andproject.targets.<name>.nonProductioninnen/contract.json;schema checkgains anen/workflow.json#deploy.defaultTargetrow, andshu detectproposestestflightforxcode-ioswhen the repository declares it markednonProductionand runnable (#399, closes #300).shu deploystill requires--target. - runner —
nen runner relabel --target <o/n> --pool <id> [--runner <names>] [--run]adds a pool's ownpool-<id>label to runners named one by one, a dry run by default, refusing a misfit, a desktop runner for a plain pool and a twin-pool runner, and reading back every write;verify --poolandinventory'spools[].unlabelledreport what is still unlabelled (#395, closes #340). - surface —
nen surface installandnen surface status(nen.surface.install/v0.1,nen.surface.status/v0.1) install and report a first-party plugin on Claude Code, Codex and Antigravity, plugin-agnostic (--plugin <name> --root <checkout>), replacing only a link it owns, never writing settings (owed actions exit 4 with the command that clears each), and reading every surface back (#392, closes #298).status --treesjudges only the copy Claude Code serves.claude,codexandagyare optional dependencies of these two verbs only. - wc —
nen wc worktree --repo <checkout> --name <n> --base <branch> [--copy-from-core <path>…](nen.wc.worktree/v0.1) keeps one detached worktree at<core>/.nen/worktrees/<name>on the fetched tip, idempotently (created,reused,moved,recreated), never resetting a dirty one and never pruning another registration; copies of gitignored, untracked files from core are proved against the tip's tree and recorded under an HMAC in the worktree's git dir (#397, closes #299). - issue —
nen issue reconcile --target <owner/name>(nen.issue.reconcile/v0.1,proposesOnly: true) proposesclose,hold,review,waitorverifyfor open issues a landed PR already closes, citing the PR, merge commit and closing reference; it never writes, refuses--dry-run, and reports an unreadable or truncated source as a finding at exit 1, never as clean (#387, closes #332).
Fixed
- shu — both pipes of a watched step are decoded as UTF-8 streams, so a character split across chunks is never U+FFFD, in a
stdoutTofile too; a synchronous spawn throw resolves as exit 5 with its--jsondocument (#381). - schema — the
nen/colors.ymlloader skips$-prefixed keys, which it read as categories, values and extra fields (#400). - cli —
plainLinestrips Unicode bidi and zero-width characters as well as control characters (#395), and the top-level catch runs it over every line of a thrown message, for every verb (#398). - ci — nen's own
ci.yml,release-assets.ymlandrunner-preflight-windows-x64.ymltarget the full pool label sets, and the runner-policy guard reads the allowed sets fromnen/workflow.json(#395). - config — nen's own
nen/labels.jsondeclares hatsu:direct's fivemodel/labels, and itsnen/workflow.jsonsetsdirect.publishtolabel, so a direct verdict on a zheref/nen issue lands as the primary pick'smodel/<line>label (#401).
Breaking / consumer notes
- Repin:
"0.20"→"0.21", andv0.20.0→v0.21.0. The floor moves because changes in #381, #385, #386, #388, #393, #395, #398, #399 and #400 alter meaning on unchanged inputs (the notes below). A repository whosedependency.minimumis below0.21is refused at exit 5 by this build: raise it to"0.21"and movepinned_reftov0.21.0. zheref/hatsu is the consumer to repin, and zheref/hatsu#227 must land before, or with, that repin (the gates note below); Hatsu'sround_policy.maxRounds: 3then becomes enforced bypr request-reviews, and its$comment("Hatsu's own keys") needs restating at the repin. - gates —
nen/gates.jsonmay carry only keys this build reads (#388, closes #310).pr ready,pr next-blockerandschema checkexit 2, where they ignored the key, naming every unread key by pointer with a nearest-key hint; a repeated key is refused by pointer wherever the file is read; only$-prefixed keys are exempt, and the consumer-owned-key carve-out is gone. Hatsu'smainstill carriescheck_exclusionsandreviewer_fallback, so until zheref/hatsu#227 lands everypr ready,schema checkandnext-blockerthere exits 2 and every botrequest-reviewsexits 1.--jsongainsunknownKeys(andschema checkduplicateKeys), additive. - gates — on a file that states
round_policy.maxRounds,pr request-reviewsrefuses a bot's request past it at exit 2, where it sent it, and makes more GitHub reads (commit and file reads at the base and the default branch, the timeline,nodes(ids:));pr readymakes oneGET /userwhile a request is pending; a legacy--add-botsid is refused naming the canonical one. The pull-request base-exclusions reader honours no exclusion, and warns, when the base file repeats a key (#388). - runner — every pool's label set gains its own
pool-<id>, last (#395).runner planandrunner scriptregister with it,runner workflowrenders it intoruns-on, andrunner enablerequires it; a plan written before this change is refused. A runner without the label stops receiving the pool's jobs: before your workflows use the full sets, name each runner and runnen runner relabel --target <o/n> --pool <id> --runner <names> --run. A matrix job carries the labels in its check name, so re-key any name-matched--exclude-check,checks.excludedentry or required check. Adesktoprunner is refused into a plain pool, a runner with a free-form extra label reads unpooled, and the schema still refuses any other extra label or the wrong order, now accepts the pool's ownpool-<id>written last, and no longer refuses two pools with the same declared set (each now has its own label). - pr —
pr openreports a pull requestghcreated as opened, at exit 1, whenghexits non-zero after printing its URL, where the run read as a plain failure (#398): a caller must read the report before retrying, or it opens a duplicate. - cli — every verb's thrown message is printed with control characters stripped, line by line (#398), and
plainLinealso strips bidi and zero-width characters (#395). A caller that matched raw bytes in an error must match the plain text. - pr —
pr body-checkwithout--requirements-fromderives the requirements from the pull-request template, or refuses with a bootstrap pointer, where it refused for the missing flag (#385);--jsongainssource, additive.pr merge --delivery/--release-unitstill require the flag. - schema — a key repeated inside
nen/contract.json→project.targetsornen/workflow.json→deployis refused by pointer (#399), as is a non-booleannonProduction; andschema checkFAILs adeploy.defaultTargetthat names an undeclared target or one not markednonProduction: true— mark the target before naming it. - schema —
nen/colors.ymlno longer reads a$key as a category or value (#400): a file that relied on one reads differently.canon pin --write, new in this release, reports a read-back failure after the rename asread-back-mismatchwithwritten: true(net of #396 and #400); no earlier release shipped another form. - canon — when
nen/repos.jsoncarries duplicatemaintained_toolsrows for one tool, the last row answers for both the pin and the checkout, so a later unpinned row erases an earlier pin (#386). Neither zheref/nen's nor zheref/hatsu's registry has duplicate rows. - shu — a step running past 30 s prints heartbeat lines on stderr by default, where it was silent; stdout and the
--jsondocument are unchanged (#381). By default a step printing more than 64 MB now reports the tool's own exit code, not exit 5, and past 64 MB relays the rest live with a notice on stderr;--heartbeat 0without--streamkeeps the old exit 5. A log-growth watchdog can no longer detect a hung child from nen's output. - shu — every
nen.shu.<verb>/v0.1report gains a trailingselectionkey,nullunless--selectwas used (#393); a reader pinning an exact key list sees one more key. Aselectblock on any verb buttest, which an older nen preserved unread, now fails the contract at load (exit 1), andshu testgains exit 6 for a zero-match selection.