Skip to content

Nen v0.21.0

Choose a tag to compare

@zheref zheref released this 11 Oct 22:31
· 137 commits to main since this release
365ff37

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 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 — --stream relays each step's stdout and stderr live, one whole line at a time, on build, test, ui-test, lint, archive, release, deploy, coverage and test-report (under --json the 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 0 without --stream restores 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 a stall guard.
  • shu — nen shu test --select <id>[,<id>…] runs the lane's one declared focused form, project.verbs.<lane>.test.select in nen/contract.json (exe, argv, each with {id}, pattern, a required results: { path, format } under .nen/focused/) (#393, closes #207). A bad selection, or a lane with no select, is refused at exit 2 before anything spawns, never a fallback to full regression; a selection that executed zero tests is exit 6 with selection.executed: 0 and zeroMatch: 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-check with no --requirements-from derives the requirement set from the repository's pull-request template, naming the source on the verdict line and in --json source, 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 is EMPTY, distinct from MISSING.
  • 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 + 1 calls, where a loop cost 4N); the whole file is checked before any write, an already-resolved thread exits 3, a refused mutation stops the batch, and a bodyFile is 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>[,…]: @me resolves through GET /user and is printed, every login is checked assignable before anything opens, and the assignees are read back; nen.pr.open/v0.1 appends assignees, me and assigned (#398, closes #293).
  • pr — under --explain and --json, pr ready diagnoses 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), infrastructureStall after 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.minRounds and round_policy.maxRounds become nen keys, adopted under the consumer's names by the maintainer's ruling of 2026-10-04 (#388, closes #240): pr request-reviews refuses a bot's request past maxRounds at exit 2, reading it at the PR's base and at the default branch's tip and applying the lower; pr ready --explain prints 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's checkout_env variable, then its checkout template (two new optional nen/repos.json maintained_tools fields), 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|check accept --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, the pinned on 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 in gates.json/decisions.json and the gate scopes of contract.json and workflow.json need --gate-change, the flag on a non-gate pointer is exit 2, and a canon pinned is refused in favour of canon pin --write. The $-metadata rule is now published in schema --help and USAGE, and a fixture over all seven declaration files guards it.
  • schema — nen/workflow.json → deploy.defaultTarget (null or a target name) and project.targets.<name>.nonProduction in nen/contract.json; schema check gains a nen/workflow.json#deploy.defaultTarget row, and shu detect proposes testflight for xcode-ios when the repository declares it marked nonProduction and runnable (#399, closes #300). shu deploy still requires --target.
  • runner — nen runner relabel --target <o/n> --pool <id> [--runner <names>] [--run] adds a pool's own pool-<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 --pool and inventory's pools[].unlabelled report what is still unlabelled (#395, closes #340).
  • surface — nen surface install and nen 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 --trees judges only the copy Claude Code serves. claude, codex and agy are 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) proposes close, hold, review, wait or verify for 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 stdoutTo file too; a synchronous spawn throw resolves as exit 5 with its --json document (#381).
  • schema — the nen/colors.yml loader skips $-prefixed keys, which it read as categories, values and extra fields (#400).
  • cli — plainLine strips 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.yml and runner-preflight-windows-x64.yml target the full pool label sets, and the runner-policy guard reads the allowed sets from nen/workflow.json (#395).
  • config — nen's own nen/labels.json declares hatsu:direct's five model/ labels, and its nen/workflow.json sets direct.publish to label, so a direct verdict on a zheref/nen issue lands as the primary pick's model/<line> label (#401).

Breaking / consumer notes

  • Repin: "0.20" → "0.21", and v0.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 whose dependency.minimum is below 0.21 is refused at exit 5 by this build: raise it to "0.21" and move pinned_ref to v0.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's round_policy.maxRounds: 3 then becomes enforced by pr request-reviews, and its $comment ("Hatsu's own keys") needs restating at the repin.
  • gates — nen/gates.json may carry only keys this build reads (#388, closes #310). pr ready, pr next-blocker and schema check exit 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's main still carries check_exclusions and reviewer_fallback, so until zheref/hatsu#227 lands every pr ready, schema check and next-blocker there exits 2 and every bot request-reviews exits 1. --json gains unknownKeys (and schema check duplicateKeys), additive.
  • gates — on a file that states round_policy.maxRounds, pr request-reviews refuses 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 ready makes one GET /user while a request is pending; a legacy --add-bots id 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 plan and runner script register with it, runner workflow renders it into runs-on, and runner enable requires 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 run nen 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.excluded entry or required check. A desktop runner 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 own pool-<id> written last, and no longer refuses two pools with the same declared set (each now has its own label).
  • pr — pr open reports a pull request gh created as opened, at exit 1, when gh exits 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 plainLine also strips bidi and zero-width characters (#395). A caller that matched raw bytes in an error must match the plain text.
  • pr — pr body-check without --requirements-from derives the requirements from the pull-request template, or refuses with a bootstrap pointer, where it refused for the missing flag (#385); --json gains source, additive. pr merge --delivery/--release-unit still require the flag.
  • schema — a key repeated inside nen/contract.json → project.targets or nen/workflow.json → deploy is refused by pointer (#399), as is a non-boolean nonProduction; and schema check FAILs a deploy.defaultTarget that names an undeclared target or one not marked nonProduction: true — mark the target before naming it.
  • schema — nen/colors.yml no 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 as read-back-mismatch with written: true (net of #396 and #400); no earlier release shipped another form.
  • canon — when nen/repos.json carries duplicate maintained_tools rows 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 --json document 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 0 without --stream keeps 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.1 report gains a trailing selection key, null unless --select was used (#393); a reader pinning an exact key list sees one more key. A select block on any verb but test, which an older nen preserved unread, now fails the contract at load (exit 1), and shu test gains exit 6 for a zero-match selection.