Repository navigation
Releases: zheref/nen
Release list
Nen v0.22.0
v0.22.0 — 2026-10-18
Release unit for v0.21.0..v0.22.0. It brings list slots and alternations in nen parse with a label and severity filter on the backlog reads, line-bound release units, hardened declared inputs, an agent-trailer requirement on wc squash, and the runner preflight refreshed from Hatsu:
- the deliveries: #411 (closes #341), #412 (closes #348), #413 (closes #410), #414 (closes #404), #422 (closes #405).
The compatibility floor moves to 0.22. No delivery adds or removes a verb; the documented count stays 134.
Added
- parse —
nen parsegains list slots (<slot>{+},<slot>{&}; every item of an enumerated list is checked) and alternations (<a:..>{+}|<b:..>[+]|<c>; the free alternative is taken only when no piece of the value is an enumerated value, so@g4+model/gptis refused rather than read as free text).--jsonaddsalternativeanditemsonly where they apply (#413, closes #410). - backlog, report —
nen backlog fetchandnen report datagain a repeatable--label(AND over an effort's issue and PR labels) and a--severity <name>[+]band, read from--severity-familyand--severity-order. An undeclared label, or a piece of a declared label containing&, is refused at exit 2 before any read; a filter that keeps nothing is an explicit zero at exit 0; an unfiltered document is unchanged. A filtered fetch keepsissueCount/prCountas fetched (before the filter) and adds afilterkey; a filteredreport datakeeps every object of a kept effort, appendsobjectsFilteras its last key (with or without--register), refuses a filter given no objects source, and refuses at exit 2 a--registerdesk naming an object the filter removed.backlog orderandreport renderrefuse the filter flags (#413). - release, pr —
release.unitPathsaccepts{ "path": "<exact text file>", "lines": ["<whole-line regex>"] }entries.release unit-checkandpr merge --release-unitcheck every removed and added line against complete merge-base/head blobs, failing closed on unreadable text or a diff beyond the stated work bounds. Existing JSON-key bounds keep their report shape;linedPathsandlineScopedViolationsare additive fields (#412, closes #348). - schema —
schema checkreportsnen/workflow.json#release, including exact, prefix/glob, key-bound and line-bound counts (#412).
Fixed
- release — an
=pathstring claims one exact path, so=README.mdcannot admitREADME.md/**. Nen's own release unit bounds changed lines insrc/version.ts,README.mdanddocs/USAGE.mdto version/floor and declared install/example forms, and names=CHANGELOG.mdexactly. Both removed and added lines are checked independently; allowed lines may be deleted, moved or replaced by another allowed form, including optional fixed command fragments. The bound does not preserve presence, position, pairing or consistent versions (#412). - release — line bounds use a release-only bounded state matcher, preserving legacy identity-regex consumers. Unsupported syntax fails schema loading by pointer. Matching never backtracks through PR text; source, nesting, state, class and pattern-count limits bound compilation, and changed-line length plus per-line/per-file operation budgets fail closed with a distinct exhaustion reason. The limits and supported expression subset are documented in USAGE (#412).
- schema, report — exact markers in review scopes and report tiers are validated at their read seams, so an empty
=or malformed repo-relative exact path cannot silently claim nothing. Bare prefixes and globs retain their existing meaning (#412). - schema — lane cwd paths share stdoutTo's repository-relative validation, including Windows absolute, drive-relative and traversal spellings on every host. Dependabot review exemptions name one anchored literal bot account rather than a catch-all or human-shaped regex (#414, closes #404).
- pr — threads settle scans every inline and file body before any GitHub call for GitHub/URL credentials, AWS access keys, Google API keys, Azure account keys, private-key headers and high-entropy tokens. Refusals name the shape and redact the value. A URL authority's numeric port followed by an @ in its path, query or fragment no longer reads as a password (#414).
- gates — NEXT_RELEASE is derived as VERSION's next minor; release proposals need no separate edit to gates.ts (#414).
- reports — Nen declares final/register sections for Hatsu's Rikugan template (#414).
- wc —
wc squash --message-filerefuses a message missing every agent trailer admitted bycommits.allowedAttributionTrailers, at exit 2 before reset or commit, naming the keys and policy source (#422, closes #405). - runner — the runner preflight fixture and Nen's Windows workflow are refreshed from Hatsu 0.111.2: branch-only pushes stop release tags waking the pool, and Windows tool resolution uses the runner service's recorded PATH. Each pool keeps its own label and SHA-pinned checkout (#411, closes #341).
Breaking / consumer notes
- Repin:
"0.21"→"0.22", andv0.21.0→v0.22.0. The floor moves because changes in #412, #413, #414 and #422 alter meaning on unchanged inputs (the notes below). A repository whosedependency.minimumis below0.22is refused at exit 5 by this build: raise it to"0.22"and movepinned_reftov0.22.0. zheref/hatsu is the consumer to repin; its declarations passschema checkunder this build unchanged (checked 2026-10-18), and zheref/hatsu#297 follows the repin. - parse — a
--grammartemplate that used|or{…}outside<…>as literal text changes meaning:|there is now an alternation (refused when it cannot resolve), and{…}declares a list joiner on the slot before it, so<a> {x} <b>leaves<b>without a separator and drops it silently (exit 0,ok: true) until #419 refuses that shape. No Hatsu template uses either form (#413). - release, review, report —
=is an explicit exact-path marker in the shared path grammar (release.unitPaths,review.scopesand report tiers). Bare strings retain their prefix/glob meaning; a literal path beginning=can be named with another leading=. Content-entry paths now also refuse globs, empty or dot segments, backslashes and control characters. A line-bound file must be readable as UTF-8 without NUL at the same path at both commits; additions, deletions and renames that cannot satisfy that check are refused (#412). - schema — a lane traversal is now rejected while loading the schema: a shu invocation exits 1 for the malformed declaration, where it formerly reached runtime containment at exit 2. No subprocess runs in either case; filesystem/symlink containment still runs at use (#414).
- schema — lane cwd declarations that are empty, absolute, drive-relative, contain a parent segment or a glob now fail at their own pointer. A
dependabot_carve_out.author_patternmust name one anchored literal[bot]login, one literal-botlogin, or the legacy dependabot/renovate account (those latter forms also admit an optional[bot]suffix). Regex alternation, wildcard bot sets and human-shaped names are refused. Consumers see these new validator refusals throughschema checkon repin (#414). - pr — the shared content scanner is heuristic: its entropy rule flags base64-like tokens of at least 32 characters, at least three character classes and Shannon entropy at least 4.5 bits per character. Hex hashes and low-entropy text do not meet that rule; a shape it does not know can pass. Only threads settle currently calls it (#414).
- wc squash — repositories with a nonempty
commits.allowedAttributionTrailersmust include one of those trailer keys with a value in the squash message's final paragraph. Previously an otherwise valid message without an agent trailer could be squashed. Empty or absent policy is unchanged; forbidden-trailer and post-commit injection checks remain separate (#422). - nen's own release unit — this repository's
nen/workflow.jsonnow usesrelease.unitPaths[].lines, which a0.21.xbinary refuses: run nen verbs in this checkout from a0.22build.
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](...
Nen v0.20.0
v0.20.0 — 2026-10-05
Release unit for v0.19.0..v0.20.0. It brings the classify and direct verb families, the round quorum that fulfils its members' rounds, declared check exclusions, the bounded delivery merge, compound PR wakes, and a committed-range triage:
- the deliveries: #352, #353, #354, #356, #359, #364, #365, #367, #368, #369, #370, #371, #375, #376, #377, #378, #379;
- #380, which carries #371's commits to
main: #371 was merged into its base branch after that branch had already landed through #370, somainnever received thedirectfamily until #380.
The compatibility floor moves to 0.20.
Added
nen classify— a new family over a consumer'scontracts/classify.taxonomy.json(hatsu.classify-taxonomy/v1: thelang/andjob/label axes zheref/hatsu#224 introduced) (#370):classify labelsrenders the taxonomy's label set;classify install --writedeclares it into the consumer'snen/labels.json, and--sync --targetrefuses until that declaration has landed;classify status [--with-body]reports which axes each open issue still lacks;classify apply --plan [--run] [--include-low]applies a confidence-graded plan, refusing a plan with any invalid row, applying high and medium rows only unless told otherwise, and refusing to edit an issue whose ledger cannot be appended to before the first edit — a directory named as the ledger is refused by its own stat on every platform (#380).nen direct— a new family over a consumer'scontracts/direct.registry.json(hatsu.direct-registry/v1, zheref/hatsu#225) (#371, #380):direct registryvalidates the registry and prints its snapshot, aliases, surfaces and live-lookup sources, refusing by pointer a cell whose surface differs from its alias's, a reviewer runner-up withoutalso, non-contiguous effort bands and a phase name the phases do not carry;direct resolvederives the domain from the taxonomy's structured rules, collects one routing pair per job and language (a non-code language always reads the shared cell), aggregates a winner and a runner-up, scores the effort and compares the session's surface, model alias and effort in dial space —unreadis never a mismatch — and--record <effort-id>files the verdict under.nen/direct/<percent-encoded id>.jsonwith symlink-real containment;direct answer --record <id> --answer continue|stopwrites the picker's decision into that same record, under the ledger lock and by rename, refusing a record that is not anen.direct.resolve/v0.1document for that effort.round_quorumfulfils its members' rounds (#359; the maintainer's ruling of 2026-10-04, #361): once at leastminimumofany_ofhave a round,pr readyrow 4 no longer fails on a member that is unavailable — no round-check run at head, a run that completed without a round, or a pending review request with no round check — and names each excused member in the row's note; a member whose round-check run is still in flight stays owed, a reviewer outsideany_ofis owed as before, andpr next-blockeragrees. A file with noround_quorumgets byte-identical output.- Declared check exclusions —
nen/gates.json→checks.excluded[](name,matchexact or glob,reason,ruled,untila date or{ "condition" }), validated by pointer at load (#359).pr ready,pr next-blockerandpr merge --release-unitread the list fromnen/gates.jsonat the PR's base, so a PR can never exempt its own failing check, and every honoured exclusion is printed once, in--explain, inmeta.warningsand in the merge transcript. The CLI's--exclude-checkstill applies on top. nen pr merge --delivery— the one merge a run performs into a non-main base (#377; contractnen.pr.merge-delivery/v0.1): the base must not be the default branch,branch.base, a protected or ruleset-bound branch, or the head of an auto-merging PR into one; thenpr ready, the head pin,pr body-check, and whose PR, every verdict quoted; without--runit prints the plan only.nen watch until --pr <ref> --until checks-settled|review-posted|ready|settled-and-reviewed— compound wake predicates over the same readpr readymakes (#376). Only--until readyis a merge signal; the others are wakes, and--jsoncarriesreadyAtWake. Polls are paced bymonitor.pollSeconds, bounded bymonitor.maxCycles, never under 30 s, and stop after two hours with no bound.nen stage triage --range <base>..<head>triages a committed range with the working-copy detectors, exit codes and--jsonshape (#379): names from every path any commit in the range touched, sizes from every blob it introduced,in-historyon a finding the net change no longer carries, and undecodable names flagged rather than guessed.nen stage listemits the exact add list a checkpoint should stage, through the same pass asstage triage(#364): newline,--nulor--json; exit 3 on an empty tree, exit 1 with the list withheld when anything needs a human.nen shu coverage --touched --from-capturemeasures touched-file coverage from an existing capture a producing run wrote, provenance-checked by a tree fingerprint and the reports' digests, and never re-runs the lane (#369).nen surface mirror check --surface claude-code --plugin <name> --installed <dir|auto>judges a Claude Code plugin install in its own layout (nen.surface.mirror.check-plugin/v0.1), the worst copy winning underauto, with the six verdicts Hatsu'splugin_cache_check.shgave (#378).nen report dataderives the effort's context —repo,worktree,effortStage,gate,stageClass— from facts, or null with its reason on stderr (#375); and--register <desk>emits the backlog register document, every fact nen's and the desk file carrying judgement only (#365): on the live path every verdict ispr ready's ownreadiness.reason, quoted; on the--objects-frompath verdicts come from the caller's file, and each such row and the footer say so (verdict read from , not from GitHub) — an offline register is file evidence, never a fresh readiness assessment.nen issue edit-body --expect-body-sha256 <hex>refuses at exit 3, writing nothing, when the live body no longer hashes as the caller read it;--current-body-outwrites the certifying read's exact bytes; every report saysbodyCheck.atomic: false(#356).nen issue file | comment | edit-bodyrefuse a private repository's name before any write to a public target, at exit 4, naming only the field and line, with a normalised second pass over markup and spelling tricks, and failing closed on an unreadable list or visibility (#367).nen shu toolsreports a host nen belowdependency.pinned_refasBEHIND, exit 7 when every row otherwise passes, with thenen bootstrap --refremedy;--jsonrows gainpinnedRefandbehindPinnedRef(#354). The scaffolded CI step treats 7 as a warning.nen wc catch-upclassifies conflicts as manifest, changelog, mirror or other against a newmechanicalblock innen/contract.json, and exits 3 with the resolution steps per class when every conflicted path is mechanical — it never resolves and never runs anything (#368).
Fixed
nen release preflightgathers every usage problem and refuses once, naming all of them, where it refused one per run;canon mirror generatenames a destination's unresolved merge conflict by line instead of calling it hand-written (#353).- nen's own
nen/workflow.jsonroutessrc/parse/**,src/watch/**and the files that decide the bounded merge to thesecurityreview scope, and declaresrelease.unitPaths, sorelease unit-checkandpr merge --release-unithave a unit to hold this proposal to (#352).
Breaking / consumer notes
- Repin:
"0.19"→"0.20", andv0.19.0→v0.20.0. The maintainer moved the floor for this release because changes in #354, #359, #364 and #368 alter meaning on unchanged inputs (the notes below). A repository whosedependency.minimumis below0.20is refused at exit 5 by this build: raise it to"0.20"and move `pinned_...
Nen v0.19.0
v0.19.0 — 2026-10-04
Release unit for v0.18.3..v0.19.0. It brings interactive desktop runners, the runner trust fixes, and stricter readiness:
- the deliveries: #334, #335, #336, #342, #343, #355 and #357;
- #358, the release proposal, and #362, which folds in #355 and #357. Those two merged while #358 was open, ahead of its merge.
The compatibility floor moves to 0.19.
Added
-
runner (#335, closes #333) — interactive Windows runners, which run inside a signed-in desktop session for jobs that drive a real UI. A service runs in session 0, where such a job times out.
- Declaration:
nen/workflow.json→runners.pools[].modeisserviceorinteractive.- Absent means
serviceon Windows and Linux, andinteractiveon macOS, whose runner is a LaunchAgent. macOS+serviceandLinux+interactiveare refused.- An interactive Windows pool's labels are exactly
["self-hosted", "Windows", <arch>, "desktop"].
- Absent means
runner plan: its contract is nownen.runner.plan/v0.2, which addsmodeanddailyAccount; a v0.1 plan still reads, as a service plan.- An interactive identity must be a local account; the built-in service identities are refused.
- Every rendering says the runner is online only while that account is signed in, with an active, unlocked session.
- It also says such a runner takes every job aimed at the plain
[self-hosted, Windows, <arch>]set, not onlydesktopjobs. - When the identity is the Windows account computing the plan, the plan warns and records
dailyAccount: true.
runner script: adailyAccountplan renders only with the new--accept-daily-account, and the verb repeats that check itself on Windows.- For an interactive pool the script installs no service and asks no password.
- It resets each
Runner<N>folder to the locked root's baseline, then grants the identity Modify there. - It registers a Scheduled Task at that account's logon that runs
run.cmdin its desktop session: interactive token, priority 4, no time limit, restarted on failure. - A folder still installed as a service is refused.
- The service-form script is unchanged.
runner workflow: it fills an eighth value,@@MODE@@. A template that predates it still renders. nen's fixture template gains a desktop-session probe, which fails in session 0, and mode-aware remediation.
- Declaration:
-
runner (#336, closes #330) — on the pool's own host,
runner planreads the existingactions.runner.*services read-only (Get-CimInstance Win32_Service/systemctl).- It warns on stderr, at exit 0, when the planned account already serves a repository of different or unknown visibility, and recommends a per-repository account (
runner-<repo>). - It also names the target's own runners left on such an account.
- Nothing is refused, and nen never creates an account.
- It warns on stderr, at exit 0, when the planned account already serves a repository of different or unknown visibility, and recommends a per-repository account (
-
pr (#355, closes #345) —
nen pr mark-ready --target <owner/name> --pr <n> [--require-head <sha>] [--dry-run]is the explicit draft-to-ready transition, now thatpr readyfails a draft at row 1. Its contract isnen.pr.mark-ready/v0.1.- It reads the PR first. Exit 2 means a target that does not resolve, exit 3 a closed or merged PR, and exit 8 a head-pin mismatch. A PR already out of draft is
already-readyat exit 0, with nothing sent. - It sends
markPullRequestReadyForReviewby node id, on gh's own credential. - It reports
marked-readyonly when a read-back showsisDraft:false. A head that moved during the call ismarked-ready-head-movedat exit 8. - Every other
prflag is refused at exit 2, with no gh call.
- It reads the PR first. Exit 2 means a target that does not resolve, exit 3 a closed or merged PR, and exit 8 a head-pin mismatch. A PR already out of draft is
-
commit (#357, closes #273) —
nen commit writeandnen wc squashread the commit they just wrote back through git's own trailer parser (git cat-file commitpiped throughgit interpret-trailers --parse --unfold).- A new
injectedmember lists every refused trailer the written commit carries: a key a hook added that the policy refuses or that ends in-by/-withwithout being admitted, or a refused key the message itself carried. - Either case is exit 3. The commit is left in place, never amended, and stderr names the undo.
- On a real write,
commit write --json'strailersis read back from the commit. - USAGE § Exit codes gains one table of every verb that returns a code above 2.
- A new
Fixed
- runner (#334, closes #319) —
runner enablecertifies only aworkflow_dispatchrun on the default branch that ran the default branch's own preflight blob.runner scripttreats a configuredRunner<N>as stale when its service is absent or GitHub no longer lists it. It re-registers that runner with--replaceinstead of counting it as skipped.- For an interactive runner, which has no service, only GitHub's listing makes it stale (#335).
- gates (#342, closes #317 and #331) —
pr readyno longer counts a head as green when all of its checks were skipped; it needs at least one success.- A draft PR is not-ready at row 1.
- A queued re-run is the latest run of its check, and holds row 2 until it concludes.
pr next-blocker --json'skindgainsdraft.
- surface (#343, closes #328) — a mirror's
summary:line now parses as YAML for everydescription, block scalars included.
Breaking / consumer notes
- Repin:
"0.18"→"0.19", andv0.18.3→v0.19.0. The maintainer moved the floor for this release because changes in #334, #335, #342 and #357 alter meaning on unchanged inputs. A repository whosedependency.minimumis below0.19is refused by this build at exit 5, and must raise its minimum and repin. zheref/hatsu declares0.18(bootstrap refv0.18.2); its repin rides with zheref/hatsu#206, which adoptsmodeand@@MODE@@. - gates —
pr readyanswers not-ready where it answered ready (#342): for a head whose checks are all skipped, for a draft, and for a check whose latest run is a queued re-run. A repository whose only checks are conditional must make one job run and succeed on every head. - runner —
runner enableexits 1 for a run it accepted before (#334): apushrun, or a run on a branch other than the default. Dispatch withnen runner preflighton the default branch and pass that run id. - runner —
runner plan --json/--outnames the contractnen.runner.plan/v0.2, where it namedv0.1, and carriesmodeanddailyAccount(#335). A reader that matches the contract string must acceptv0.2.runner scriptstill reads a v0.1 plan. - commit —
commit writeandwc squashexit3when the written commit carries a refused trailer (#357). Exit 3 means the commit exists, so a caller that reads any non-zero exit as "nothing was committed" must handle it. - wc —
wc squashloadsnen/workflow.jsonon every run, before anything moves (#357). A malformed file is exit 1 even with--baseand a message with no trailers, and a broken policy with a bad message is exit 1, not 2. - surface — on repin, Hatsu's
surfaces/codex/ten/SKILL.mdreads stale until onenen surface mirror generatefor codex: itssummary:gains double quotes (#343).
Nen v0.18.2
v0.18.2 — 2026-09-30
Release unit for v0.18.1..v0.18.2, two fixes from hatsu:futon nen@bug and the Windows runner proof:
- #315, which closes #304: a pending check's empty conclusion.
- #316, which closes #307: a finished rebase read as in progress.
- #318: the
windows-x64runner preflight workflow. - #322, the release proposal.
The compatibility floor stays 0.18.
Fixed
- pr (#304, #315).
gh pr view --json statusCheckRollupwritesconclusion: ""for a CheckRun that is still running, and the rollup parser refused it, sonen pr fetchandnen pr next-blockerexited 1 on every pull request with a pending check. A""conclusion on a run whose status is present and is notCOMPLETEDnow reads as unset (null), and CON-32(a) still reads it as not green. A completed run with"", a run with no status, and any unknown non-empty conclusion are still refused at their path.nen pr readyreads GraphQL, where the value is alreadynull, and was never affected.nen report data's register counts such a check pending with no degradation note; its lenient fallback's note now says an unrecognised conclusion counts red, as it always did. - shu, wc (#307, #316).
nen shu warmupread a rebase as in progress wheneverREBASE_HEADresolved, and git leaves that ref behind after a rebase completes, so warmup refused a clean checkout afternen wc catch-upfinished a conflicted rebase. Warmup and catch-up now share one reading ofgit rebase --show-current-patch: exit 0 is a rebase stopped on a patch, 1 one paused at abreakor a failedexecline, 128 no rebase oncegit rev-parse --git-dirshows git can answer in the repository, and anything else (a signal included) unanswered.MERGE_HEADandCHERRY_PICK_HEADkeep their ref checks.
Added
- ci (#318).
.github/workflows/runner-preflight-windows-x64.yml, rendered bynen runner workflow --pool windows-x64: the proofnen runner enableneeds before thecheck-windowsleg runs on the self-hosted Windows pool. No verb or flag changes.
Breaking / consumer notes
This release declares no breaking consumer note, which is what leaves COMPATIBLE_MINOR_FLOOR at 0.18. The section still states the outcome, as the release-time floor step requires.
- No repin: the compatibility floor stays
0.18. A repository pinnedminimum: "0.18"needs no change. Three refusals are new, each where the old reading was wrong rather than a changed contract:nen shu warmuprefuses a rebase paused at abreakor a failedexec(exit 2), which it used to warm through;nen wc catch-uprefuses that paused rebase instead of merging inside it, with--abortstill backing it out (exit 2); and both refuse a rebase probe git did not answer (exit 2).pinned_refSHOULD move tov0.18.2on a repository whose skills drivepr fetch,pr next-blockeror a warm-up after a catch-up.
Nen v0.18.1
v0.18.1 — 2026-09-30
Release unit for v0.18.0..v0.18.1, the hardening of the host script nen runner script renders:
- the delivery: #313, which closes #312 — Feitan's three security findings from Hatsu's review of
hatsu:jusshin. - #314, the release proposal.
The compatibility floor stays 0.18.
Fixed
- runner — Security (#312, #313). The rendered Windows script passed the service-account password and the registration token on
config.cmd's argv, a batch file cmd.exe re-parses:& | < > ^ %split the argument, expanded%PATHEXT%and ran|whoami, andHide-Secretmasked only the whole secret, so fragments were replayed into theregister-*.logthe caller reads; both secrets also sat on the process command line. Now both reach the runner through its own environment inputs,ACTIONS_RUNNER_INPUT_TOKENandACTIONS_RUNNER_INPUT_WINDOWSLOGONPASSWORD(actions/runnerCommandSettings.cs,ACTIONS_RUNNER_INPUT_<ARG>, secrets masked and the variables removed from the listener's environment), set just before the call and cleared in itsfinally;config.cmd's output is discarded and never replayed; a password that is empty, has leading or trailing whitespace, or carries& | < > ^ % " 'or a line break is refused before first use, at exit 1, naming the rule. On Linux and macOS the token takes the same environment path (a pipe read by the service user's shell on Linux, wheresudowould strip an inherited variable). - runner — Security (#312, #313). The runner root an unelevated session created inherited
Authenticated Users: ModifyfromC:\, so a pool's own service account could swap the.ps1before UAC, the zip after its one hash check, orconfig.cmdbefore it ran. The elevated script now locks the root down first, before the transcript,ghand any download — owner Administrators, inheritance removed,SYSTEMandAdministratorsfull control, the package directory admin-only, the elevated user read on_jusshin— re-hashes the archive immediately before everyExpand-Archive(and everytaron Linux and macOS), and empties a pendingRunner<N>folder before extraction. The service account keeps(RX)on the root and project folder only. - runner —
nen runner scriptrefuses an--outat or under the plan's runner root at exit 2, naming%LOCALAPPDATA% en\jusshin\(Windows) or~/.local/state/nen/jusshin/(Linux, macOS) instead; every host script writes the one summary line, and only that line, to_jusshin/register-<ts>.summary;--jsongains the additivescriptSha256andsummarykeys. Flags and exit codes are otherwise unchanged.
Breaking / consumer notes
This release declares no breaking consumer note, and under the rule #200 landed that is a decision rather than an absence: it is what leaves COMPATIBLE_MINOR_FLOOR at 0.18, and therefore what keeps every "0.18" pin already written valid against a v0.18.1 binary. The section is still here, and still carries exactly one bullet, because the release-time floor step requires the cut to STATE the outcome either way.
- No repin: the compatibility floor stays
0.18. A repository pinnedminimum: "0.18"needs no change to read this build.pinned_refSHOULD move tov0.18.1on a repository whose skills render the Windows host script: that is how a host picks up the secret-path and root-lockdown fixes above, and the--outrefusal is the one new exit-2 a caller can meet.
Nen v0.18.0
v0.18.0 — 2026-09-30
Release unit for v0.17.0..v0.18.0, the nen runner family that hatsu:jusshin executes:
The compatibility floor moves to 0.18.
Added
-
runner (#305) —
nen runner, seven verbs that raise a repository's self-hosted runner pools. Every step that can be decided is a verb; every human step is asked. Every GitHub call goes through theghseam.inventorylists every runner (every page) and the runner downloads, parses each name or marks it runner 0, and groups runners by pool on a label superset.planpicks the lowest free slots per machine and consumer, the root in the host's separators, the identity (askwhen none is named) and the package with its SHA-256. Its output is the contractnen.runner.plan/v0.1.scriptrenders the host script — PowerShell 5.1, Linux bash (sudo) or macOS bash (run as yourself) — and prints one launch line. It never runs the script. The Windows script asserts elevation andgh auth status, grantsicacls … (RX)on the root and project folder only, asks for the password once as a secure string, mints each registration token itself, keeps both secrets out of the transcript, verifies the SHA-256, and requires every service to beRunning.verifynever passes on a partial result.workflowrenders a caller's@@NAME@@template. A leftover placeholder or invalid YAML is exit 1.preflightdispatches the pool's preflight workflow, finds its run by id difference rather than by clock, waits, and names a queued job for what it is.enablere-reads the run, requires a green run of this pool's preflight beforegh variable set, then reads the value back.- Exit codes:
0,1(the family's own failure, a GitHub refusal included),2(usage, or a missing or malformedrunnersblock) and5(ghcould not be started). Under--dry-run,scriptandworkflowwrite nothing, andpreflightandenableread GitHub but dispatch or set nothing.preflightandenablerequire--target; the other four fall back to--repo'sorigin. - Izanami's table classifies the family:
inventoryandverifyread-only,plangated on--out, the other fourdry-run-gated. - Not in this release: runner removal,
--ephemeral, a shared actions cache and runner groups. The Windows ARM64 and Linux ARM64 scripts are rendered but have never been executed.
-
workflow (#305) — an optional
runnersblock innen/workflow.json, the policynen runneracts on. It has no default: absent, everyrunnerverb that needs it refuses at exit 2 naming the key.namingis exactly{machine}-{consumer}R{slot}; any other template is refused, naming this one.- Each pool states
id,os(Linux/macOS/Windows) andarch(X64/ARM64) in GitHub's case;windows,x64,darwinandamd64are refused, naming the canonical spelling.labelsmust be exactly["self-hosted", os, arch].enableVariableis optional,toolsmust be non-empty and shell-safe,preflightWorkflowis a.ymlbasename, androotholds quote-free per-OS defaults. - Two pools may not share a label set, and each pool names its own preflight file, because the file is rendered for one pool's
runs-onandenablemust not accept one pool's run as proof of another. nen schema checkgains anen/workflow.json#runnersrow.- This repository declares two pools:
windows-x64(thecheckleg's Windows runners, gated onNEN_WINDOWS_RUNNER, preflightrunner-preflight-windows-x64.yml) andmacos-arm64(thecheckandcompilejobs, not variable-gated, preflightrunner-preflight-macos-arm64.yml).
-
commit (#302) — a
nen/workflow.jsonkey,commits.bodyMaxLineLength: a whole number ≥ 1 that declares the body width for a repository whose commitlint config nen cannot read (code, unreadable or absent). A malformed value is exit 1 by pointer. Beside a readable data config it is only noted, never applied.nen schema checkreports it.
Fixed
- commit (#290) —
nen commit formatemitted--bodyexactly as typed, so commitlint'sbody-max-line-lengthrefused the commit at the hook afterwc squashhad already reset.src/commit/linelength.tsports commitlint's line-length rules (@commitlint/ensure21.2.3, the conventional-commits-parser 7.1.2 body/footer split) with no new dependency.commit formatwraps a--bodyline over the width at spaces and tabs only. It never splits a word or a URL, never moves a footer, comment or gpg line, and is idempotent. Each rewrapped line gets anen: note:on stderr. A message within its limits is unchanged byte for byte, and a rule set to[0]is not wrapped.- The width comes from a readable data config first, then
commits.bodyMaxLineLength, then 100 for a config nen cannot read. Where nen's own width binds, it holds every line before the trailer block. - A level-2 violation the wrap cannot fix, or any in a
commit write --message-file, is exit 2, naming the line, its length, the rule and the fix. A level-1 violation is a warning.commit writevalidates and never rewrites the file. - Known gap: under a code or unreadable config, a trailer line over 100 gets only a warning at exit 0, and commitlint's
footer-max-line-lengthstill refuses it at the hook.
- test (#280) — main is green under both
bunx vitest run(the declared runner, and CI's) and a fullbun test, with the same count per file (#306). The templates sweep is linear, about 50ms instead of 2.5s, where it could pass bun's 5000ms limit under load. Thedetectfixtures read their own edits back and fail naming the fixture. The client suite runs one fake clock under both runners. A new guard fails any test file that imports another. No product source changed. - watch (#288) —
nen watch untilrefused the ordinary jq idiom--jq "[.reviews[]|select(...)|.author.login]"at exit 2, because it scanned the joined line for metacharacters that never reach a shell. It also classified one argument list and spawned another, so a single-quoted--jq '.number'reached jq with its quotes on and failed on every poll.- The
--commandline is now tokenised once with POSIX shell quoting (src/parse/command-line.ts), and the verdict and the spawn read that one argument list. - On macOS and Linux, a metacharacter inside one quoted or escaped argument is accepted. A metacharacter a shell would act on, and any metacharacter on Windows, still refuse with the same message;
nen parse izanamikeeps its whole-line check. - A write hidden behind quoting (
git branch '-D' x,gh api … -X 'DELETE') is now named mutating instead of unknown. - New exit-2 refusals: an unclosed quote, a trailing
\, whitespace other than space and tab, and a NUL byte. v0.17.0 split such a line on whitespace, quotes kept, and spawned it.
- The
- watch / parse (#303) — Security. Two
gh apispellings were certified read-only while gh sends a write.- A bare
=on a value shorthand (-q=,-H=,-p=,-t=), e.g.gh api repos/o/r/issues/1 -q= -XDELETE: gh gives-qthe value=and sends DELETE. This affectednen parse izanamiandnen watch untilin every release from v0.2.0 through v0.17.0, v0.15.1 included; the watch spawned the line itself on every interval. - An empty attached value
-q'', e.g.gh api repos/o/r/issues/1 -q'' -H -XDELETE: a shell hands gh a bare-q, which takes-Has its value and leaves-XDELETEas the method. This affectednen parse izanamiin v0.7.0 through v0.17.0 wherever a skill-side shell ran the certified line. - The
gh apirow now walks the spawned argument list the way gh's flag parser does. Over a 2,223-line adversarial corpus checked against a pflag oracle validated on real gh 2.100.0, v0.17.0 made 27 read-only verdicts on requests gh sends as writes; v0.18.0 makes 0. - Until a host runs v0.18.0, do not pass either verb a
gh apiline whose value flag carries an empty or bare-=value.
- A bare
- shu (#296) —
nen shu coverage --touchedcredited every touched Cobertura file with its package's figure, matched by path against package names, so a per-file floor could not be read (four changed C# files collapsed into oneKroCore 61.18%row). A name that resolved nowhere, or to more than one file, was silently credited to something.- Rows,
targets,metandbandare now per source file, under its on-disk spelling, resolved against every stated<source>root, the artifact, the lane cwd and the repository root. - A file named by several
<class>entries is the union of their lines; each line keeps one condition figure, never a sum. - Every name that cannot be placed is listed in
touched.artifacts[].unresolvedas{name, reason}(ambiguous, unverifiable, outside the repository only, a URL, another machine's path, not found) and printed under thefrom:line. A per-file Cobertura artifact also carriesroot,basis(which may readsource) andonDisk. LCOV and JaCoCo entries keep their seven keys. - A Cobertura report none of whose names joins a touched file now exits 6, nothing measured, where it passed at exit 0 on package rows. It stays exit 0 wheneve...
- Rows,
Nen v0.17.0
v0.17.0 — 2026-09-29
Release unit for v0.16.0..v0.17.0, the hatsu:futon nen@bug run:
- the deliveries: #279, #282, #283, #284, #285 and #289, which close every open
bugissue; - #287, the maintainer's review-gate ruling;
- #295, the release proposal.
The compatibility floor moves to 0.17.
Added
- gates —
nen/gates.jsontakes an optionalround_quorum: { any_of, minimum }. CON-32(b)'s rounds-owed row fails when fewer thanminimumof those reviewers have a round. It only adds that failure; it never excuses a round that is otherwise owed.--jsoncarries an additiveconjuncts[].roundQuorumon that row.nen pr next-blockerchecks the quorum too, on the head only, and says so.- This repository declares Copilot
bounded_policy_exempt, Cursor Bugbot asbugbot, and a one-of-two quorum. That follows the maintainer's ruling of 2026-09-29: "at least one round of reviews from both Copilot OR Cursor (or both) as applicable".
- commit —
nen commit formatandnen commit writeapply the target repository's commitlintsubject-caserule and give commitlint's own verdict.- Level 2 is refused at exit 2; level 1 prints a warning. The implementation ports
@commitlint/ensure21.2.3 and adds no dependency. - The config is read from a data config in commitlint's own search order. For a config that is code, unreadable or absent, the rule comes from the new
nen/workflow.jsonkeycommits.subjectCase, which is"config-conventional"or a rule tuple. - nen never executes a JavaScript config.
- Level 2 is refused at exit 2; level 1 prints a warning. The implementation ports
- changelog — the CON-33(c) release-PR allowance: the range's terminal PR merge is excused from citing itself when its own merge introduced the dated section being cut. At most one PR can be excused.
changelog completenessandrelease preflightnow share one reconciliation (src/changelog/reconcile.ts).--jsongainsreleasePrAllowance.
- pr —
pr request-reviews --jsoncarriesunrecordedBots. - pr —
--exclude-checkonpr readymay repeat. It splits only on commas outside brackets, so a matrix check such ascheck (Windows, [...])can be excluded. - pr / release —
pr mergeandrelease unit-check --praccept<CODE>#<n>, resolved throughpr ready's own registry lookup. - repo —
nen/repos.jsonmaintained_tools[]andpending_onboarding[]rows may carryscenario, read byrepo scenarioandcanon resolve. - surface —
surface mirror generatere-aims every relative link in a mirrored body for the depth its copy lands at.--jsongainslinkRoot,linksRewrittenandlinksVerbatim, and--repobounds the tree a link may reach.
Fixed
- repo (#219) — a repository recorded under
maintained_tools[]could never carry a scenario, so neither tool repository could resolve a pinned handbook. The refusal's remedy also told the caller to misfile it as a consumer; it now names the section the target is really in. - changelog (#229) — every release cost a follow-up "reconcile" PR, because the release PR had to cite a number that did not exist yet.
- A heading marked
— unreleasedno longer counts as dated. - A release section carried in by any merge, local merges included, is detected.
- An option-shaped
--rangeis refused before any tool runs.
- A heading marked
- pr (#277) —
pr request-reviewsreportedokfor a bot request GitHub accepted but never recorded. It now exits 9 and names the bot. - pr (#243) —
pr ready --exclude-checkcould not name a check whose name contains a comma. - pr (#269) —
pr mergeaccepted<CODE>#<N>in its grammar and then refused it as anowner/name. - commit (#263) —
nen commit formatdid not enforce the target repository's commitlintsubject-caserule. - surface (#270) — relative links in generated mirrors dangled at every depth: 408 of 2701 in zheref/hatsu's mirrors, 0 after regeneration.
- wc (#271) —
wc publishpushed onto a differently named upstream branch. A stacked effort's commit landed on its base branch's PR.
Breaking / consumer notes
- Repin:
"0.7"→"0.17", andv0.16.0→v0.17.0. The maintainer moved the floor for this release. A repository whosedependency.minimumis below0.17is refused by this build at exit 5 and must raise its minimum and repin. zheref/hatsu declares0.15, pinnedv0.15.1. - wc — a bare
nen wc publishon a branch whose upstream names another branch, the trunk included, now exits 2 instead of pushing.- Pass
--set-upstream. It publishes to<remote>/<own name>and retracks, taking the remote from--remote, elseorigin, else the upstream's own. nen shu warmupcuts branches with--no-track, so a new branch tracks nothing until its first publish.
- Pass
- gates — a round check (
round_check_pattern) now counts only on a SUCCESS conclusion. A cancelled or neutral run is no longer a round. Underbounded, an earlier-head run counts only if GitHub lists it against this pull request. - commit —
nen/workflow.jsonis read on everycommit format/writerun, not only with--trailer.- A malformed file, or a malformed
commits.subjectCase, is now exit 1. - A nonexistent
--repois now exit 2. nen: warning:andnen: note:lines can appear on stderr at exit 0.
- A malformed file, or a malformed
- pr —
pr request-reviews: a new exit 9 for an unrecorded bot, and a response with noreviewRequestslist is exit 1, where it was 0. - pr —
pr ready --exclude-check: a value with an unclosed opener and a later comma is exit 2. - pr — a product code that matches registry keys differing only by case is refused at exit 2 by
pr ready,pr mergeandunit-check. - repo — a non-string
scenarioon amaintained_tools[]orpending_onboarding[]row now stops the registry loading. It used to be ignored. - surface —
surface mirror checkreports a mirror generated by an older build asstaleuntil it is regenerated. - changelog —
changelog completenessandrelease preflightexit 0, where they exited 1, on a range whose only uncited PR is the qualifying release PR.
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.
Nen v0.15.1
v0.15.1 — 2026-09-28
Release unit for v0.15.0..v0.15.1: #267 (the delivery) and #268 (the release proposal).
Fixed
- parse — the futon advance-go gate fails closed. When the target's
nen/workflow.jsonhas nofuton.advanceGo, or declares it empty, a built-in default policy applies:mugetsu→process/library;kagutsuchi,getsuga→ any kind. Eachgategainssource: "declared" | "default", rendered as[declared]/[default]. A declared non-empty map replaces the default wholesale. Before, a repository without the key got no gate at all. - parse —
nen parse futonchecks the selector before anythen-clause refusal, and never fills one in. An empty selector is refused withcorrectedLine: null, so no corrected line can carry a publishing chain on the maintainer's behalf. - parse — a chain naming the same skill with the same target twice is refused. Targets are compared case-insensitively, and the corrected line drops every later repeat. Different targets stay allowed, and there is no step-order rule.
- release —
release.unitPathsentries may be{path, keys}: that exact JSON file may change only at those keys, written as dotted paths or RFC 6901 pointers. The file is read at the merge base and the head, with the path percent-encoded;#,?and%are refused in a keyed path. Both copies are parsed strictly (control characters, leading-zero numbers and invalid base64 read as unreadable) into a number-preserving tree, then diffed by segment path with container types and key sets. That catches added or removed keys, type changes (including at an ancestor of an allowed key) and precision loss. Unreadable content, or a keyed file with no content context, fails closed. Used byrelease unit-checkandpr merge --release-unit.release unit-check --jsonkeepsunitPathsas strings and addskeyedPathsandkeyScopedViolations.
Breaking / consumer notes
- No repin: the compatibility floor stays
0.7.gate.source,keyedPathsandkeyScopedViolationsare additive, and no contract version is bumped. Threenen parse futonbehaviours change in place, each toward refusal:- a repository with no
futon.advanceGonow gets gated steps under the default policy; - a duplicated chain step exits
2where it exited0; - an empty selector's
correctedLineisnullwhere it was a string namingcritical.
- a repository with no