Skip to content

Releases: zheref/nen

Nen v0.22.0

Choose a tag to compare

@zheref zheref released this 18 Oct 17:14
46d333b

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 compatibility floor moves to 0.22. No delivery adds or removes a verb; the documented count stays 134.

Added

  • parse — nen parse gains 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/gpt is refused rather than read as free text). --json adds alternative and items only where they apply (#413, closes #410).
  • backlog, report — nen backlog fetch and nen report data gain a repeatable --label (AND over an effort's issue and PR labels) and a --severity <name>[+] band, read from --severity-family and --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 keeps issueCount/prCount as fetched (before the filter) and adds a filter key; a filtered report data keeps every object of a kept effort, appends objectsFilter as its last key (with or without --register), refuses a filter given no objects source, and refuses at exit 2 a --register desk naming an object the filter removed. backlog order and report render refuse the filter flags (#413).
  • release, pr — release.unitPaths accepts { "path": "<exact text file>", "lines": ["<whole-line regex>"] } entries. release unit-check and pr merge --release-unit check 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; linedPaths and lineScopedViolations are additive fields (#412, closes #348).
  • schema — schema check reports nen/workflow.json#release, including exact, prefix/glob, key-bound and line-bound counts (#412).

Fixed

  • release — an =path string claims one exact path, so =README.md cannot admit README.md/**. Nen's own release unit bounds changed lines in src/version.ts, README.md and docs/USAGE.md to version/floor and declared install/example forms, and names =CHANGELOG.md exactly. 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-file refuses a message missing every agent trailer admitted by commits.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", and v0.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 whose dependency.minimum is below 0.22 is refused at exit 5 by this build: raise it to "0.22" and move pinned_ref to v0.22.0. zheref/hatsu is the consumer to repin; its declarations pass schema check under this build unchanged (checked 2026-10-18), and zheref/hatsu#297 follows the repin.
  • parse — a --grammar template 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.scopes and 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_pattern must name one anchored literal [bot] login, one literal -bot login, 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 through schema check on 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.allowedAttributionTrailers must 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.json now uses release.unitPaths[].lines, which a 0.21.x binary refuses: run nen verbs in this checkout from a 0.22 build.

Nen v0.21.0

Choose a tag to compare

@zheref zheref released this 11 Oct 22:31
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](...
Read more

Nen v0.20.0

Choose a tag to compare

@zheref zheref released this 05 Oct 02:00
d37c652

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 compatibility floor moves to 0.20.

Added

  • nen classify — a new family over a consumer's contracts/classify.taxonomy.json (hatsu.classify-taxonomy/v1: the lang/ and job/ label axes zheref/hatsu#224 introduced) (#370): classify labels renders the taxonomy's label set; classify install --write declares it into the consumer's nen/labels.json, and --sync --target refuses 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's contracts/direct.registry.json (hatsu.direct-registry/v1, zheref/hatsu#225) (#371, #380): direct registry validates 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 without also, non-contiguous effort bands and a phase name the phases do not carry; direct resolve derives 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 — unread is never a mismatch — and --record <effort-id> files the verdict under .nen/direct/<percent-encoded id>.json with symlink-real containment; direct answer --record <id> --answer continue|stop writes the picker's decision into that same record, under the ledger lock and by rename, refusing a record that is not a nen.direct.resolve/v0.1 document for that effort.
  • round_quorum fulfils its members' rounds (#359; the maintainer's ruling of 2026-10-04, #361): once at least minimum of any_of have a round, pr ready row 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 outside any_of is owed as before, and pr next-blocker agrees. A file with no round_quorum gets byte-identical output.
  • Declared check exclusions — nen/gates.json → checks.excluded[] (name, match exact or glob, reason, ruled, until a date or { "condition" }), validated by pointer at load (#359). pr ready, pr next-blocker and pr merge --release-unit read the list from nen/gates.json at the PR's base, so a PR can never exempt its own failing check, and every honoured exclusion is printed once, in --explain, in meta.warnings and in the merge transcript. The CLI's --exclude-check still applies on top.
  • nen pr merge --delivery — the one merge a run performs into a non-main base (#377; contract nen.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; then pr ready, the head pin, pr body-check, and whose PR, every verdict quoted; without --run it prints the plan only.
  • nen watch until --pr <ref> --until checks-settled|review-posted|ready|settled-and-reviewed — compound wake predicates over the same read pr ready makes (#376). Only --until ready is a merge signal; the others are wakes, and --json carries readyAtWake. Polls are paced by monitor.pollSeconds, bounded by monitor.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 --json shape (#379): names from every path any commit in the range touched, sizes from every blob it introduced, in-history on a finding the net change no longer carries, and undecodable names flagged rather than guessed.
  • nen stage list emits the exact add list a checkpoint should stage, through the same pass as stage triage (#364): newline, --nul or --json; exit 3 on an empty tree, exit 1 with the list withheld when anything needs a human.
  • nen shu coverage --touched --from-capture measures 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 under auto, with the six verdicts Hatsu's plugin_cache_check.sh gave (#378).
  • nen report data derives 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 is pr ready's own readiness.reason, quoted; on the --objects-from path 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-out writes the certifying read's exact bytes; every report says bodyCheck.atomic: false (#356).
  • nen issue file | comment | edit-body refuse 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 tools reports a host nen below dependency.pinned_ref as BEHIND, exit 7 when every row otherwise passes, with the nen bootstrap --ref remedy; --json rows gain pinnedRef and behindPinnedRef (#354). The scaffolded CI step treats 7 as a warning.
  • nen wc catch-up classifies conflicts as manifest, changelog, mirror or other against a new mechanical block in nen/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 preflight gathers every usage problem and refuses once, naming all of them, where it refused one per run; canon mirror generate names a destination's unresolved merge conflict by line instead of calling it hand-written (#353).
  • nen's own nen/workflow.json routes src/parse/**, src/watch/** and the files that decide the bounded merge to the security review scope, and declares release.unitPaths, so release unit-check and pr merge --release-unit have a unit to hold this proposal to (#352).

Breaking / consumer notes

  • Repin: "0.19" → "0.20", and v0.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 whose dependency.minimum is below 0.20 is refused at exit 5 by this build: raise it to "0.20" and move `pinned_...
Read more

Nen v0.19.0

Choose a tag to compare

@zheref zheref released this 04 Oct 18:21
384711e

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 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[].mode is service or interactive.
      • Absent means service on Windows and Linux, and interactive on macOS, whose runner is a LaunchAgent.
      • macOS + service and Linux + interactive are refused.
      • An interactive Windows pool's labels are exactly ["self-hosted", "Windows", <arch>, "desktop"].
    • runner plan: its contract is now nen.runner.plan/v0.2, which adds mode and dailyAccount; 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 only desktop jobs.
      • When the identity is the Windows account computing the plan, the plan warns and records dailyAccount: true.
    • runner script: a dailyAccount plan 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.cmd in 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.
  • runner (#336, closes #330) — on the pool's own host, runner plan reads the existing actions.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.
  • 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 that pr ready fails a draft at row 1. Its contract is nen.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-ready at exit 0, with nothing sent.
    • It sends markPullRequestReadyForReview by node id, on gh's own credential.
    • It reports marked-ready only when a read-back shows isDraft:false. A head that moved during the call is marked-ready-head-moved at exit 8.
    • Every other pr flag is refused at exit 2, with no gh call.
  • commit (#357, closes #273) — nen commit write and nen wc squash read the commit they just wrote back through git's own trailer parser (git cat-file commit piped through git interpret-trailers --parse --unfold).

    • A new injected member lists every refused trailer the written commit carries: a key a hook added that the policy refuses or that ends in -by/-with without 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's trailers is read back from the commit.
    • USAGE § Exit codes gains one table of every verb that returns a code above 2.

Fixed

  • runner (#334, closes #319) — runner enable certifies only a workflow_dispatch run on the default branch that ran the default branch's own preflight blob.
    • runner script treats a configured Runner<N> as stale when its service is absent or GitHub no longer lists it. It re-registers that runner with --replace instead 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 ready no 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's kind gains draft.
  • surface (#343, closes #328) — a mirror's summary: line now parses as YAML for every description, block scalars included.

Breaking / consumer notes

  • Repin: "0.18" → "0.19", and v0.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 whose dependency.minimum is below 0.19 is refused by this build at exit 5, and must raise its minimum and repin. zheref/hatsu declares 0.18 (bootstrap ref v0.18.2); its repin rides with zheref/hatsu#206, which adopts mode and @@MODE@@.
  • gates — pr ready answers 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 enable exits 1 for a run it accepted before (#334): a push run, or a run on a branch other than the default. Dispatch with nen runner preflight on the default branch and pass that run id.
  • runner — runner plan --json / --out names the contract nen.runner.plan/v0.2, where it named v0.1, and carries mode and dailyAccount (#335). A reader that matches the contract string must accept v0.2. runner script still reads a v0.1 plan.
  • commit — commit write and wc squash exit 3 when 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 squash loads nen/workflow.json on every run, before anything moves (#357). A malformed file is exit 1 even with --base and 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.md reads stale until one nen surface mirror generate for codex: its summary: gains double quotes (#343).

Nen v0.18.2

Choose a tag to compare

@zheref zheref released this 01 Oct 01:40
264d27c

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-x64 runner preflight workflow.
  • #322, the release proposal.

The compatibility floor stays 0.18.

Fixed

  • pr (#304, #315). gh pr view --json statusCheckRollup writes conclusion: "" for a CheckRun that is still running, and the rollup parser refused it, so nen pr fetch and nen pr next-blocker exited 1 on every pull request with a pending check. A "" conclusion on a run whose status is present and is not COMPLETED now 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 ready reads GraphQL, where the value is already null, 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 warmup read a rebase as in progress whenever REBASE_HEAD resolved, and git leaves that ref behind after a rebase completes, so warmup refused a clean checkout after nen wc catch-up finished a conflicted rebase. Warmup and catch-up now share one reading of git rebase --show-current-patch: exit 0 is a rebase stopped on a patch, 1 one paused at a break or a failed exec line, 128 no rebase once git rev-parse --git-dir shows git can answer in the repository, and anything else (a signal included) unanswered. MERGE_HEAD and CHERRY_PICK_HEAD keep their ref checks.

Added

  • ci (#318). .github/workflows/runner-preflight-windows-x64.yml, rendered by nen runner workflow --pool windows-x64: the proof nen runner enable needs before the check-windows leg 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 pinned minimum: "0.18" needs no change. Three refusals are new, each where the old reading was wrong rather than a changed contract: nen shu warmup refuses a rebase paused at a break or a failed exec (exit 2), which it used to warm through; nen wc catch-up refuses that paused rebase instead of merging inside it, with --abort still backing it out (exit 2); and both refuse a rebase probe git did not answer (exit 2). pinned_ref SHOULD move to v0.18.2 on a repository whose skills drive pr fetch, pr next-blocker or a warm-up after a catch-up.

Nen v0.18.1

Choose a tag to compare

@zheref zheref released this 30 Sep 22:33
86d545a

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, and Hide-Secret masked only the whole secret, so fragments were replayed into the register-*.log the caller reads; both secrets also sat on the process command line. Now both reach the runner through its own environment inputs, ACTIONS_RUNNER_INPUT_TOKEN and ACTIONS_RUNNER_INPUT_WINDOWSLOGONPASSWORD (actions/runner CommandSettings.cs, ACTIONS_RUNNER_INPUT_<ARG>, secrets masked and the variables removed from the listener's environment), set just before the call and cleared in its finally; 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, where sudo would strip an inherited variable).
  • runner — Security (#312, #313). The runner root an unelevated session created inherited Authenticated Users: Modify from C:\, so a pool's own service account could swap the .ps1 before UAC, the zip after its one hash check, or config.cmd before it ran. The elevated script now locks the root down first, before the transcript, gh and any download — owner Administrators, inheritance removed, SYSTEM and Administrators full control, the package directory admin-only, the elevated user read on _jusshin — re-hashes the archive immediately before every Expand-Archive (and every tar on Linux and macOS), and empties a pending Runner<N> folder before extraction. The service account keeps (RX) on the root and project folder only.
  • runner — nen runner script refuses an --out at 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; --json gains the additive scriptSha256 and summary keys. 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 pinned minimum: "0.18" needs no change to read this build. pinned_ref SHOULD move to v0.18.1 on 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 --out refusal is the one new exit-2 a caller can meet.

Nen v0.18.0

Choose a tag to compare

@zheref zheref released this 30 Sep 21:10
53d8028

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 the gh seam.

    • inventory lists 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.
    • plan picks the lowest free slots per machine and consumer, the root in the host's separators, the identity (ask when none is named) and the package with its SHA-256. Its output is the contract nen.runner.plan/v0.1.
    • script renders 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 and gh auth status, grants icacls … (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 be Running.
    • verify never passes on a partial result.
    • workflow renders a caller's @@NAME@@ template. A leftover placeholder or invalid YAML is exit 1.
    • preflight dispatches 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.
    • enable re-reads the run, requires a green run of this pool's preflight before gh 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 malformed runners block) and 5 (gh could not be started). Under --dry-run, script and workflow write nothing, and preflight and enable read GitHub but dispatch or set nothing. preflight and enable require --target; the other four fall back to --repo's origin.
    • Izanami's table classifies the family: inventory and verify read-only, plan gated on --out, the other four dry-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 runners block in nen/workflow.json, the policy nen runner acts on. It has no default: absent, every runner verb that needs it refuses at exit 2 naming the key.

    • naming is exactly {machine}-{consumer}R{slot}; any other template is refused, naming this one.
    • Each pool states id, os (Linux/macOS/Windows) and arch (X64/ARM64) in GitHub's case; windows, x64, darwin and amd64 are refused, naming the canonical spelling. labels must be exactly ["self-hosted", os, arch]. enableVariable is optional, tools must be non-empty and shell-safe, preflightWorkflow is a .yml basename, and root holds 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-on and enable must not accept one pool's run as proof of another.
    • nen schema check gains a nen/workflow.json#runners row.
    • This repository declares two pools: windows-x64 (the check leg's Windows runners, gated on NEN_WINDOWS_RUNNER, preflight runner-preflight-windows-x64.yml) and macos-arm64 (the check and compile jobs, not variable-gated, preflight runner-preflight-macos-arm64.yml).
  • commit (#302) — a nen/workflow.json key, 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 check reports it.

Fixed

  • commit (#290) — nen commit format emitted --body exactly as typed, so commitlint's body-max-line-length refused the commit at the hook after wc squash had already reset. src/commit/linelength.ts ports commitlint's line-length rules (@commitlint/ensure 21.2.3, the conventional-commits-parser 7.1.2 body/footer split) with no new dependency.
    • commit format wraps a --body line 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 a nen: 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 write validates 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-length still refuses it at the hook.
  • test (#280) — main is green under both bunx vitest run (the declared runner, and CI's) and a full bun 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. The detect fixtures 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 until refused 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 --command line 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 izanami keeps 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.
  • watch / parse (#303) — Security. Two gh api spellings 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 -q the value = and sends DELETE. This affected nen parse izanami and nen watch until in 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 -H as its value and leaves -XDELETE as the method. This affected nen parse izanami in v0.7.0 through v0.17.0 wherever a skill-side shell ran the certified line.
    • The gh api row 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 api line whose value flag carries an empty or bare-= value.
  • shu (#296) — nen shu coverage --touched credited 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 one KroCore 61.18% row). A name that resolved nowhere, or to more than one file, was silently credited to something.
    • Rows, targets, met and band are 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[].unresolved as {name, reason} (ambiguous, unverifiable, outside the repository only, a URL, another machine's path, not found) and printed under the from: line. A per-file Cobertura artifact also carries root, basis (which may read source) and onDisk. 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...
Read more

Nen v0.17.0

Choose a tag to compare

@zheref zheref released this 30 Sep 00:14
14da302

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 bug issue;
  • #287, the maintainer's review-gate ruling;
  • #295, the release proposal.

The compatibility floor moves to 0.17.

Added

  • gates — nen/gates.json takes an optional round_quorum: { any_of, minimum }. CON-32(b)'s rounds-owed row fails when fewer than minimum of those reviewers have a round. It only adds that failure; it never excuses a round that is otherwise owed.
    • --json carries an additive conjuncts[].roundQuorum on that row.
    • nen pr next-blocker checks the quorum too, on the head only, and says so.
    • This repository declares Copilot bounded_policy_exempt, Cursor Bugbot as bugbot, 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 format and nen commit write apply the target repository's commitlint subject-case rule and give commitlint's own verdict.
    • Level 2 is refused at exit 2; level 1 prints a warning. The implementation ports @commitlint/ensure 21.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.json key commits.subjectCase, which is "config-conventional" or a rule tuple.
    • nen never executes a JavaScript config.
  • 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 completeness and release preflight now share one reconciliation (src/changelog/reconcile.ts).
    • --json gains releasePrAllowance.
  • pr — pr request-reviews --json carries unrecordedBots.
  • pr — --exclude-check on pr ready may repeat. It splits only on commas outside brackets, so a matrix check such as check (Windows, [...]) can be excluded.
  • pr / release — pr merge and release unit-check --pr accept <CODE>#<n>, resolved through pr ready's own registry lookup.
  • repo — nen/repos.json maintained_tools[] and pending_onboarding[] rows may carry scenario, read by repo scenario and canon resolve.
  • surface — surface mirror generate re-aims every relative link in a mirrored body for the depth its copy lands at. --json gains linkRoot, linksRewritten and linksVerbatim, and --repo bounds 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 — unreleased no longer counts as dated.
    • A release section carried in by any merge, local merges included, is detected.
    • An option-shaped --range is refused before any tool runs.
  • pr (#277) — pr request-reviews reported ok for a bot request GitHub accepted but never recorded. It now exits 9 and names the bot.
  • pr (#243) — pr ready --exclude-check could not name a check whose name contains a comma.
  • pr (#269) — pr merge accepted <CODE>#<N> in its grammar and then refused it as an owner/name.
  • commit (#263) — nen commit format did not enforce the target repository's commitlint subject-case rule.
  • 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 publish pushed 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", and v0.16.0 → v0.17.0. The maintainer moved the floor for this release. A repository whose dependency.minimum is below 0.17 is refused by this build at exit 5 and must raise its minimum and repin. zheref/hatsu declares 0.15, pinned v0.15.1.
  • wc — a bare nen wc publish on 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, else origin, else the upstream's own.
    • nen shu warmup cuts branches with --no-track, so a new branch tracks nothing until its first publish.
  • gates — a round check (round_check_pattern) now counts only on a SUCCESS conclusion. A cancelled or neutral run is no longer a round. Under bounded, an earlier-head run counts only if GitHub lists it against this pull request.
  • commit — nen/workflow.json is read on every commit format/write run, not only with --trailer.
    • A malformed file, or a malformed commits.subjectCase, is now exit 1.
    • A nonexistent --repo is now exit 2.
    • nen: warning: and nen: note: lines can appear on stderr at exit 0.
  • pr — pr request-reviews: a new exit 9 for an unrecorded bot, and a response with no reviewRequests list 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 merge and unit-check.
  • repo — a non-string scenario on a maintained_tools[] or pending_onboarding[] row now stops the registry loading. It used to be ignored.
  • surface — surface mirror check reports a mirror generated by an older build as stale until it is regenerated.
  • changelog — changelog completeness and release preflight exit 0, where they exited 1, on a range whose only uncited PR is the qualifying release PR.

Nen v0.16.0

Choose a tag to compare

@zheref zheref released this 29 Sep 20:21
594cfe7

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|check render a canon set into every surface the consumer declares, not one hard-coded rules directory. --surfaces (or the canon-values file's own surfaces: key) names them, and each surface's location is its own documented one: codex → AGENTS.md as 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 check is the CI half: it renders in memory and diffs each surface's committed copy, classifying every file ok, MISSING, EXTRA, STALE (the marker names another source, ref or scenario — generated once, never regenerated after the pin moved), HAND-EDITED, or FOREIGN. Exit 1 iff any surface has drift; --markdown-out also writes it as a Surface | File | Issue table.
  • canon — nen canon pin --repo <consumer> reads the canonical handbooks repository and the tag it is pinned to out of the consumer's own nen/repos.json — the pinned field on its maintained_tools entry. It exists so a sync can check the canon out at that tag before rendering, and so mirror check has 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 --source does not disambiguate, or when the recorded pin is not tag-shaped.
  • schema — nen/repos.json maintained_tools[] entries may carry pinned, surfaced as the registry's toolPins. 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 on maintained_tools because that is the list whose entries classify as canon, and it reuses the spelling consumers[] 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.md carries 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 no CLAUDE.md, .claude/CLAUDE.md or CLAUDE.local.md at or above the root — so a consumer rendering another surface beside Codex loads the canon twice unless it keeps a CLAUDE.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.md and the Gradle/Android profile now name the canonical handbooks repository rather than bankai-core, which is no longer where a consumer's handbooks come from.

Fixed

  • parse — nen canon pin was in no NEN_VERB_TABLE row, 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. Classified RO: it reads the consumer's registry and writes nothing. In the same row canon mirror generate was MUT and its text named --out-dir, a flag the verb no longer takes — it is DRY, because --dry-run reports 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 generate and read back as split by the very next check, 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.0 deliberately: canon pin and canon mirror generate|check --surfaces exist 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:limbo calls exactly those verbs and cannot run until a release carries them.

Nen v0.15.1

Choose a tag to compare

@zheref zheref released this 28 Sep 19:18
f063b99

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.json has no futon.advanceGo, or declares it empty, a built-in default policy applies: mugetsu → process/library; kagutsuchi, getsuga → any kind. Each gate gains source: "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 futon checks the selector before any then-clause refusal, and never fills one in. An empty selector is refused with correctedLine: 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.unitPaths entries 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 by release unit-check and pr merge --release-unit. release unit-check --json keeps unitPaths as strings and adds keyedPaths and keyScopedViolations.

Breaking / consumer notes

  • No repin: the compatibility floor stays 0.7. gate.source, keyedPaths and keyScopedViolations are additive, and no contract version is bumped. Three nen parse futon behaviours change in place, each toward refusal:
    • a repository with no futon.advanceGo now gets gated steps under the default policy;
    • a duplicated chain step exits 2 where it exited 0;
    • an empty selector's correctedLine is null where it was a string naming critical.