Cut a release tag locally, end to end — preconditions, one folded release-proposal PR merged inside § 3a's bounds (or by the maintainer where they do not hold), the post-merge tag, then the CON-22 fan-out. Use when the maintainer invokes hatsu:getsuga [<hash | branch-name | main | last-commit | checkout>], or asks to cut a tag, cut a release, or ship a version. The cut point is never required: with no token it is origin/<branch.base> from nen/workflow.json (plain main when none is declared), stated with its source in the first line; a typed token always overrides, and a version-shaped token is read as the tag name. An off-main target is driven to main first, and susanoo builds the release unit. Merges exactly one PR itself — its own release-proposal PR, when its diff is the release unit only and CON-32 reads Ready, through the repository's own protection (the maintainer's ruling of 2026-09-26) — and never a delivery PR; never publishes a release — publication is mugetsu's, per target, at G3 — and never tags a commit unreachable from the configured trunk, origin/<branch.base>.
Install
npx skillscat add zheref/hatsu/getsuga Install via the SkillsCat registry.
Shared policy location: docs/DISCOVERY.md, docs/WORKFLOW.md,docs/LAUNCH-MIGRATION.md, docs/STANDALONE-ENTRY.md, docs/PROCESS.md and docs/SURFACES.md belong to the resolved Hatsu plugin
root, not the consuming repository. On an installed surface, use the absolute root printed byten to read those files (re-resolve through that skill if unavailable). Relative links
below identify source locations; a missing consumer docs/ copy is not a missing policy and must not
trigger a duplicate filing. Never copy or invent a second policy in the target repository.
Getsuga — one command from "cut it" to a tag on main
Nature: Emitter. The release-tag cut is Kurapika's own duty (CON-33(b)/CON-41); the merges
and the publish are not.
Say this on invocation, before anything else: what the resolved target is, and — when the
target is not already onmain— that this run will build and ship it, not just tag it. The
verb reads narrower than it acts, and a maintainer who typed "cut a tag" should not discover
forty minutes later that they started a delivery.
Composition — the phases this skill already is
Read from the phase lattice rather than from this file's own numbering, getsuga is the release half
of the line: everything that happens once the delivery work is already on main, ending at the
post-merge tag and the fan-out. It is not invoked after a release proposal has been merged — it
is what opens that proposal and then merges it itself through § 3a's bounded verb, standing at
the maintainer's declaration gate only where one of that verb's gates fails:
`susanoo` (builds the release unit) → the release-proposal PR (§ 3) →
the merge of that PR — § 3a'snen pr merge --release-unit, or the maintainer's where it fell
back → the post-merge tag (§ 4) → theCON-22fan-out (§ 7)
"Post-merge" qualifies the tag, not the phase. § 4 cuts at a commit reachable from origin/<branch.base>,
which is exactly why the merge stands in the middle of that chain rather than before all of it.
This is a restatement, and it changes no mechanics. Every section below stands exactly as it is
written: § 2's whole preflight table, § 3's one folded PR, § 4's pinned --at cut, § 5's G5 ask,
§ 6's off-main drive, § 7's fan-out and § 7a's printed deploy plan. What the line adds is where each
of them lives once the lattice is the map.
Three consequences follow, and none of them is new:
- Getsuga runs after a merge; it does not replace one. What § 3 opens is a proposal; it is
merged bynen pr merge --release-unitinside § 3a — its three gates green, this run's own PR —
or else stops at the declaration gate (G4 in a canon
repository, G2 in a consumer one) for the maintainer, and only then does § 4 tag that merge SHA. A tag cut
without a merged release PR would be a tag on a commit nobody approved, which is the whole reasonCON-33(b)asks for the PR first. - The release unit is `susanoo`'s, not this skill's. Where the repository
declares anarchive, producing the distributable is that phase — the declared verb, run locally,
uploading nothing — and getsuga consumes the unit rather than building it. A lane whosearchive
is a seat answers exit4with the declaration's own reason and that is the whole report for
such a repository; verified live at0.3.0against this plugin's ownpluginlane
(docs/ab/getsuga.md§ 2.7). - The release itself is never getsuga's. Publication is `mugetsu`'s phase,
per target, at G3, on the maintainer's recorded per-target go; a non-production upload is
`kagutsuchi`'s, per target, equally the maintainer's own call. Neither
ever runs from inside this skill, from any path — § 7a prints a plan and stops for exactly that
reason, and § 8 already names it as a Never. A futon chain that lists one after getsuga runs
it as its own later step on the advance go (mugetsu § 3), once this skill has cut the tag.
1. Invocation
P1 — this run opens with hatsu:ten getsuga (`ten` § 6 catches up this phase's
missing prerequisites under its own name; the phases this run calls skip theirs).
hatsu:getsuga [<hash | branch-name | main | last-commit | checkout>]| Token | Resolves to |
|---|---|
| (none) | the configuration default: origin/<branch.base>'s tip, re-fetched, branch.base read from nen/workflow.json — main here; plain main when the key or the file is absent |
main |
origin/<branch.base>'s tip, re-fetched — the trunk the configuration names, whatever it is called (main itself here) |
last-commit |
the tip of origin/<branch.base> — the same commit, named the way people say it |
<hash> |
that commit |
<branch-name> |
that branch's tip |
checkout |
the current working copy's HEAD |
The cut point is never required (maintainer's ruling of 2026-09-28, `ROSTER.md`
§ Rulings of 2026-09-26/27/28, rulings 7 and 9). getsuga always accepts an override and never
asks for one: with no token the run's first line after the mode header states the cut point and
its source — cut point: origin/develop (configuration default: nen/workflow.json branch.base = develop) or cut point: origin/main (no branch.base declared: main) — and proceeds. branch.base is
the same key breath fast-forwards and ao pulls from, so the default is the trunk the whole workflow
already agrees on, and the ancestry check below uses that same trunk (--trunk <branch.base>). A
typed token always overrides it.
A version-shaped token (v?\d+\.\d+\.\d+; a missing v is normalised, 0.47.0 → v0.47.0)
is checked against the tags first: git rev-parse --verify -q refs/tags/<vX.Y.Z> succeeding means
the tag exists and the run is refused — a tag is never re-cut. Absent as a tag and resolving to no
commit or branch, it is read as the tag name to cut, with the cut point defaulted as above, and the
first line after the mode header says so: tag: v0.47.0 (from the token) · cut point: origin/main (configuration default …). A tag name that disagrees with the version the manifest and changelog
carry (.claude-plugin/plugin.json version, the top CHANGELOG.md section) stops, naming both
— the maintainer reconciles, never the skill. Any other token that resolves to nothing is refused
naming the five valid forms.
Reached as a `futon` then step on an advance go — the definition is
`mugetsu` § 3's and is not restated here: the step's @<target> is the token
above, the configuration default with none. Everything here is unchanged by it — the same preconditions, the
release-proposal PR merged by § 3a or else standing at its declaration gate, the same fan-out, no
publication of its own; a later step of the same chain is that skill's own run, never this one's.
A missing argument or configuration item other than the cut point is asked for and set up inline
(missing-argument, missing-configuration; `WORKFLOW.md` § 4 Ask,
set up, continue) — the maintainer's word, never derived (row missing-maintainer-choice)
still holds for everything else here: a version the changelog and manifest do not settle, a go, a
destination. The cut point alone is the configuration's, by ruling. A missing registry or
declaration item is set up through its owner (nen scaffold init, or nen/repos.json's own key).
Resolve, then test reachability — this is the load-bearing check, and it is one verb now, not
two hand-run git commands:
git rev-parse --verify -q '<token>^{commit}' >/dev/null || <refuse: unresolvable, the five forms named> # a hash or branch token only
nen release resolve-target --repo <path to the checkout> --token <token> --trunk <branch.base> # no token typed: --token mainThe rev-parse runs first because resolve-target's exit 1 conflates an unresolvable token with
a resolved commit that is not an ancestor — the two need different answers (a refusal naming the
forms, versus § 6); a distinct exit code is a nen residue, filed as an issue. --trunk <branch.base>
is required, not decoration: nen's --trunk defaults to main, so in a repository whosebranch.base is develop the documented default was refused as NOT an ancestor of the trunk
(scripts/getsuga_default_cut_point_red.sh, green since ruling 9). With no token typed the token
passed is the LITERAL main: nen maps it to the re-fetched origin/<trunk> tip, whatever the
trunk is called, whereas --token <branch.base> would resolve a possibly stale LOCAL ref — a typed
token is passed through only as the explicit override it is. The verb re-fetches origin/<trunk>
itself, runs git merge-base --is-ancestor <resolved> origin/<trunk>, and refuses a
dirty checkout outright (uncommitted work is not in any commit, so there is nothing to tag). Verified live against the real <reference-repo> (docs/ab/getsuga.md
§ 2.1): --token main and --token last-commit both resolve to the freshly re-fetchedorigin/main tip and report an ancestor of the trunk -- safe to cut; a branch token pointing at a
live integration/* branch reports NOT an ancestor of the trunk -- it has to reach the trunk first before it can be tagged — exit 1, never a refusal to run.
- Exit
0→ an ancestor. Proceed to § 2. - Exit
1→ not an ancestor (therev-parseabove already excluded unresolvable). It has to
reach<branch.base>first (§ 6). Do not refuse, and do not tag it where it stands.
A dirty checkout is never the cut point. Hand it to
`hatsu:tensho`, which is the verb for that, and resume once its PR lands.
The phases this composite calls SKIP their own ## 0. Standalone entry sections. `docs/STANDALONE-ENTRY.md` is the
contract for a phase typed by hand into an arbitrary checkout; a phase reached from here inherits P1–P4
from this run — the warm-up, the orientation, the change set and every derived argument — and
re-deriving them would produce a second answer to a question this composite already settled. Each phase
says which composite is holding it instead.
2. Preconditions — all of them, before a single write
nen release preflight folds the whole table into one call, and reports it whole, never the
first failure:
export GH_TOKEN=$(gh auth token)
nen --repo <path to the checkout> release preflight \
--repo-slug <owner/name> --tag <vX.Y.Z> --range <vPrev>..<cut-point> \
--changelog <path to CHANGELOG.md at the cut point> --owner-repo <owner/name> \
--critical-issues <n,n or ''> --live-chores-from <path>--critical-issues and --live-chores-from are gathered by the caller, never invented by the
verb — nen owns the AND-logic and the "not supplied — not checked" honesty; it does not own the
GitHub reads that produce the raw facts:
- Critical issues:
gh issue list --repo <owner/name> --label "bankai:severity/critical" --state open --json number. The label is the fullbankai:severity/critical, never the barecritical— a barecriticallabel does not exist innen/labels.json,
and the query silently returns zero matches rather than erroring, which would defeat this precondition without
ever surfacing a mistake (regression caught in review; re-verified live with--state allthat
the corrected query returns the real historical criticals —docs/ab/getsuga.md§ 2.2). Pass the
numbers, or--critical-issues ''to assert there are none — never omit the flag, which reports
the row as not checked and fails the table. - Live chores (
CON-36's three-part test): per candidateintegration/<chore>branch, gather{ name, issueOpen, integrationBranchExists, openPrTargetsIntegrationOrMain }— the chore
issue's state (gh issue view <n> --json state), whether the branch exists
(git branch -r --list origin/integration/<chore>), and whether any open PR targets it or
targetsmainfrom it (gh pr list --state open --json baseRefName,headRefName).nencomputes
the AND per chore and the "none live" verdict; point the file at[]to assert none are live.
Verified live against the real <reference-repo> backlog (docs/ab/getsuga.md § 2.2 —
zero open critical issues, RELEASE_HOLD read as false, sixteen historical integration/*
branches gathered and fed through, one of them — 879-g2-gate-definition — carrying a genuinely
open chore issue but no PR targeting it or main, so the three-part AND still reads not live):
every row the table used to check by separate hand-run command now comes back in one report.
Finding this port filed against
v0.1.0, closed by nenv0.2.0(#63, closes zheref/nen#23):RELEASE_HOLDis now parsed for truthiness, not mere presence. At the port,<reference-repo>'sRELEASE_HOLDVariable — literally the string"false", meaning not held under the oldscripts/tag_cut.shsemantics — reportedFAIL RELEASE_HOLD -- HELD: RELEASE_HOLD = 'false', so a
repo that had ever set the Variable tofalseread permanently HELD (docs/ab/getsuga.md§ 2.2.1).
Atv0.3.0the rule is the old script's own, stated innen release --help(verified): "Case-
insensitivetrue/1/yesreads as a recognized active hold;false/0/no(or an unset
variable) reads as not held; any OTHER non-empty value (an arbitrary hold message) is not recognized
and fails closed as an active hold." SoRELEASE_HOLD=falseno longer holds a release, and the
"delete the Variable rather than set it false" workaround is retired. Relay the row exactly asnen
prints it (per § 2's own instruction to honour a HELD row and say who set it); a HELD row on an
arbitrary message is the fail-closed branch doing its job, not the old defect. Not re-run live atv0.3.0(the row is agh variable getread); the--helptext and thev0.2.0changelog are the
evidence.
A second, safer divergence:
nen release preflight'sRELEASE_HOLDread fails CLOSED on an
unreachablegh, where the oldscripts/tag_cut.shfailed OPEN. Verified live (docs/ab/getsuga.md
§ 2.2.2): withghmade unreachable,nenreportsFAIL RELEASE_HOLD -- could not be read: Executable not found in $PATH: "gh"and the whole table exits1— aghoutage refuses the
release rather than silently waving it through. The old script's ownhold_value="$(gh variable get RELEASE_HOLD 2>/dev/null || true)"(scripts/tag_cut.sh, read at the frozenv0.11.3
snapshot) instead swallows the failure into an empty string, whichhold_active("")reads as
inactive — aghoutage under the old mechanics let a release proceed unheld. Stated for
awareness, not as a rule to follow: this divergence needs no operational instruction, since the
safer behavior is also the one that fires automatically.
Maintainer-awareness note, not a routed path: the old
scripts/tag_cut.sh --mode markerescape
hatch (CON-7) has nonenequivalent. Nogetsugapath — this port, or any real cut recorded
indocs/ab/getsuga.md— has ever used it; stated here only so its absence is a known gap, not a
silent one, should the maintainer ever reach for it.
| Precondition | On failure |
|---|---|
RELEASE_HOLD |
Honour it as printed. Stop and say who set it. (false/0/no/unset is not held since nen v0.2.0; any other non-empty value fails closed as held) |
Open critical issues |
Stop. A release shipping past an open critical is the failure the severity exists to name |
CON-36 live chores |
Hold, unless none has partial scope on main — and that reading is a G5 ask for the maintainer, never your call (§ 5) |
changelog.d/ empty at the cut point |
Collate — § 3 (nen/decisions.json row release-precondition-red: reconciled in the same chunk, never a stop) |
CON-33(c) reconciled |
Back-fill — § 3 |
| Tag does not already exist | Stop. Re-tagging is never the fix |
Check the cut point, not the tip. --range's second half and --changelog must both name the
cut point, not main's current tip — main moves while a release is assembled (RR-IS-#683 has
run six laps on exactly this), and a precondition verified against a tip that has since moved was
never verified at all.
3. The release unit — fold everything that can fold into ONE PR
In phase-lattice terms, and changing no mechanics below: the release unit is built through
`hatsu:susanoo` — the declaredarchive, run locally, uploading nothing —
which landed at Hatsuv0.6.0. Where a repository declares noarchive, or declares it as a
seat, § 6's off-main path builds through the repository's own declared verbs exactly as it already
says, and that seat's own reason is the report.kagutsuchi(non-production upload)
andmugetsu(publication, G3) never run inside this skill, from any path: each is the
maintainer's own per-target call — typed by name, or as a later futonthenstep on an advance go
(mugetsu § 3), run by that skill after this one's tag exists — and § 7a already prints a deploy
plan and stops for that reason.
CON-33(b) wants a release PR merged before the tag cuts — this skill's own, and from the
maintainer's ruling of 2026-09-26 this skill merges it itself (§ 3a; `docs/ROSTER.md` § Rulings of 2026-09-26/27/28). One PR carries all of it:
Collate every
changelog.d/fragment into a dated### vX.Y.Z — <theme>section:nen changelog collate --version <vX.Y.Z> --theme "<text>" \ --changelog <path to CHANGELOG.md> --fragment-dir changelog.d [--write]Without
--write, this only previews — the rendered result, with neitherCHANGELOG.mdnorchangelog.d/touched. With--write, it rewritesCHANGELOG.mdin place and deletes every
collated fragment — verified live against a constructed fixture (docs/ab/getsuga.md§ 2.3):
never hand-write the section, never leave a fragment behind.RETIRED at nen
0.7: the manifest that disagreed with the writeThe printed manifest and the written section now agree, and the manifest is the one that
moved. Through the pinnedv0.6.0the manifest was printed inreaddirSyncorder while the
section was rendered by the fragment sort — newest-first by the leading<n>-prefix,CON-33(b)'s own convention — so the two disagreed by construction, at any fragment count.
Verified live, the same fixture through both binaries (docs/ab/getsuga.md
§ Retired at nen 0.7):v0.6.0 collated 3 fragment(s) … 0.7.0 collated 3 fragment(s) … 10-a.md 30-c.md 20-b.md 20-b.md 30-c.md 10-a.mdwhile both wrote
FRAG-30, FRAG-20, FRAG-10into the section.--json'sfragments[]is
read off the same sorted list, so the machine form carries the fix too — it was the consumer
least able to notice the disagreement.Nothing was ever dropped, duplicated or misattributed and the written content was always
right; what was wrong is the record a caller cross-checks the section against, and getsuga
relays this manifest to the maintainer as the answer to "did my fragment land where I
expected". So relay the manifest as the record it now is — and § Hard limits' rule stands
unchanged for the other reason it was written: never re-order the written section by eye,
because the written order was correct all along and hand-reordering it would corrupt a real
changelog into oldest-first.CON-33(c)— cited in an earlier draft of this finding — governs the completeness check's own
ascending numeric comparison (§ 3.3 below), not this body-rendering order; the clause that
governs the written section's newest-first placement isCON-33(b).Back-fill anything stranded. A fragment present at the previous tag but never collated into
that tag's section belongs in the previous section, not this one — decide it withgit ls-tree -r <prevTag> -- changelog.d, never with file timestamps. Nonenverb owns this
placement decision — it stays a directgitread plus judgment (residue,docs/ab/getsuga.md
§ 3).The
CON-33(c)release unit — every PR merged in<vPrev>..<cut-point>, numeric order:nen changelog completeness --range <vPrev>..<cut-point> \ --changelog <path to CHANGELOG.md> --owner-repo <owner/name> [--fragment-dir changelog.d]Verified live against the real
<reference-repo>rangev0.11.2..v0.11.3
(docs/ab/getsuga.md§ 2.4): same verdict as the oldscripts/changelog_release_completeness_check.sh v0.11.2 v0.11.3run read-only side by side —
both report every merged PR reconciled, exit0. Since nenv0.2.0(#83)--fragment-dirdefaults
tochangelog.dhere and inrelease preflight— the two verbs reconcile the same range and
can no longer disagree about where fragments live — and an empty value, or a path that is not a
directory, is refused; a directory that does not exist contributes no fragments. Omit the flag or
pass a real directory; never"".Bump
latestin the registry —nen/repos.json, which at the pinned build is the ONLY
file nen reads it from. Nonenverb owns this write — residue, a direct edit — so make
sure it lands in the file nen reads:nen schema check --repo <path>prints the row
(ok nen/repos.json …, or a FAIL naming the migration where a copy sits only underschemas/).
RETIRED at nen0.5: there is no shadowed leftover FAIL any more, because there is no read
to protect —nen/is the only file anything reads. Aschemas/copy sitting beside a loadednen/one is awarnleftover naming thegit rmthat clears it, and whether its bytes
still agree no longer changes the verdict. Editnen/repos.json; delete the stale duplicate.Bump the version manifest — same reasoning as the old skill: a cached plugin would report
consumers current while they sit a tag behind. Nonenverb owns this write either — residue, a
direct edit; prefer the bump to ano plugin bump:opt-out. Where the surface stamp followsplugin.jsonand the delivery PR carries the bump — hatsu, by the maintainer's ruling of
2026-09-28 (`docs/ROSTER.md` § Rulings of 2026-09-26/27/28) — this step is a no-op: the release PR
touches the changelog only, andrelease.unitPathssays exactly that.
What cannot fold: the CON-22 repin PRs. They target other repositories. One PR per affected
consumer, after the tag, § 7.
Self-enumeration is a fact to check, not a habit.
nen release self-check --repo <path> --pr-merge-sha <sha> --previous-tag <vPrev> --cut-point <vNew>True iff the release PR's own merge commit is reachable from --cut-point and not already
reachable from --previous-tag — a git-mechanical fact, never a judgement. Verified live against
the real v0.11.3 release PR (RR-PR-#916): nen reports it should list ITSELF againstv0.11.2..v0.11.3 (correct — that PR's merge is the cut point), and should NOT list itself
against v0.11.3..v0.11.3 (correct — already reachable from its own tag). Four laps got this wrong
by hand in <reference-repo> (RR-PR-#651, RR-PR-#679, RR-PR-#682, RR-PR-#691) before this existed.
Collation manufactures contradictions — look for them. Two fragments written weeks apart are
each true when written and become simultaneous claims in one dated section. Before opening the
PR, read the assembled section for entries that contradict each other. Reconcile in place; never
delete the superseded entry. State the net effect so nobody has to reconcile two entries
themselves. This stays judgment — no verb reads intent.
3a. The release-proposal PR merges itself — bounded
Maintainer's ruling of 2026-09-26 (`docs/ROSTER.md` § Rulings of 2026-09-26/27/28): "If I call getsuga, I don't expect to be called just to
approve and merge a PR that is only touching hatsu-related changes … I expect getsuga to handle
that itself and then get to the actual tag completely autonomously."
So this skill merges one PR and one only: the release-proposal PR it opened in this run, and
it merges it through the one bounded merge nen performs, never a raw gh pr merge:
nen pr merge <n|owner/name#n> --release-unit --requirements-from <path> --repo <path> # the plan
nen pr merge <n|owner/name#n> --release-unit --requirements-from <path> --repo <path> --run # the mergeThe verb bounds the diff; this skill bounds which PR. Record, when § 3 opens the proposal, its
PR number and head SHA — that pair, and no other object, is what § 3a may merge. The verb then runs
five gates in order, every one regardless of an earlier failure, and prints every verdict line
verbatim — quote those lines in the report, never a summary of them:
pr ready—CON-32readiness, naming the exact commit it judged.- head pin — the PR's head still matches that judged commit (both SHAs printed on a mismatch);
the merge argv carries--match-head-commit, so GitHub itself refuses a head that moved after the
read. pr body-check— against the PR's live body and the repository's own--requirements-from(`tensho` § 6's{name, pattern}array).release unit-check— every changed path inside the release unit,release.unitPathsas
read at the PR's base commit (never this checkout's file, never a policy the PR itself edits):
the paths § 3 writes, declared once there and restated nowhere.outside: <path>lines name what
fell out; an undeclared key is this gate failing, not a usage error.- whose pr — the PR is not from a fork and its author is the
ghviewer.
Go to § 4 only on the verb's merged: line — GitHub's own MERGED state, re-read after the run.queued (auto-merge or merge queue): or anything unconfirmed → re-read the PR's state and wait; never
cut until MERGED, else fall back. Exit 1 — a gate did not pass: fall back to the stop this
section used to be, the declaration-gate prompt for the maintainer's merge, naming the failing gate
by its own line. Exit 2 — usage: a missing --release-unit or --requirements-from, a ref that
is not --repo's own origin, or --admin/--auto, which the verb does not declare — there is no
bypass to reach for. Exit 5 — gh refused the merge (branch protection, a required approving
review this session cannot give itself): a G5 with the stderr and the exact command nen printed,
handed over, never routed around. Exit 6 — gh could not run at all: a G5 handing over the
same command. The main ruleset is admin-bypassable, so the verb's gates are the real bound, not
the branch protection. Never self-reviews, never approves its own PR (claude/agents/kurapika.md),
and only this PR: a delivery PR — futon's, build's, anything a then chain drove — still waits
for the maintainer at its gate; --release-unit is the verb's own refusal of anything else.
A fall-back stops at the declaration gate (nen/decisions.json row canon-merge in a canon
repository, merge in a consumer) with the banner and the board — G4 in a canon repository
(nen repo classify → role: canon, read from nen/repos.json's maintained_tools, never a list
remembered here), G2 in a consumer one, because a release proposal in a repository whose product is not the process
is that repository's own configuration:
# resolve <gate> from the TARGET's role first — G4 in a canon repository, G2 in a consumer one
nen stop --who kurapika --gate <G4|G2> <efforts.md>--gate is not a constant here. A hard-coded G4 prints a canon-repository banner over a
consumer's release proposal, which is the misclassification the 2026-09-18 ruling exists to remove
(`docs/ROSTER.md` § Rulings of 2026-09-18 — G4 is the repository's role,
not the file's kind). Outside § 3a's gates the maintainer merges, and Kurapika never mergesmain — the 2026-09-18 ruling moves the gate, and the ruling of 2026-09-26 (`docs/ROSTER.md` § Rulings of 2026-09-26/27/28)
opens exactly one PR's worth of it, at merge and at canon-merge alike, throughnen pr merge --release-unit and nothing else.
4. The cut
After the release PR merges — by § 3a, or by the maintainer where § 3a fell back — re-fetch and cut
with nen tag cut, which enforces reachability itself:
nen tag cut --repo <path> --name <vX.Y.Z> --at <the release PR's merge SHA> \
--trunk <branch.base> [--message "<text>"] --push--trunk <branch.base> is required here for the same reason as in § 1: the verb's reachability check
defaults to main, so on a repository whose trunk is develop the cut would refuse every commit.
⚠️ Pin the commit explicitly — never
HEAD.--atis required and never defaulted;nen tag cut --helpstates this outright. "Cut frommain" is not an instruction the verb can
follow: ifmainadvanced between the merge and the cut,HEADis a later commit, and the tag
then covers PRs whose fragments are still uncollated. Check out the release PR's merge SHA and
cut there, after re-verifying § 2 at that SHA — exactly how thev0.10.0window kept re-filling
(RR-IS-#683).
- Cut the reconciled commit, not necessarily the tip. Verify § 2 again at that exact SHA before
cutting; if either thechangelog.d/orCON-33(c)row fails there, the cut needs another
collation lap. --atnot an ancestor oforigin/<branch.base>→ refused, never tagged off-trunk. The tag name
already existing, locally or on origin → refused, never re-tagged. Both verified live in a
disposable scratch repo, never against<reference-repo>orhatsu(docs/ab/getsuga.md§ 2.5):
cutting at an unreachable commit refuses withis not an ancestor of origin/<trunk> ... Use 'nen release resolve-target' first; re-cutting an existing name refuses withalready exists locally -- never re-tagged.- The tag is created LOCALLY ONLY unless
--pushis passed — verified live: without--push,git ls-remote --tags originon the scratch remote stayed empty after the cut; with--push, the
annotated tag reached origin and nothing else did. A tag getsuga cuts is meant to resolve for
consumers — pass--push, the same effective outcome the oldscripts/tag_cut.shalways
produced by pushing unconditionally; the verb just makes the push an explicit, visible step rather
than an implicit one (docs/ab/getsuga.md§ 2.5, § 3). - If the tag capability is refused, HALT and hand the maintainer the exact command. Never
improvise, never switch mechanisms — a REST tag-ref create is a bypass, not a workaround — and
never writelatestor a dated CHANGELOG section for a tag that does not resolve (CON-14). - Never re-tag, never move a tag, never force a tag —
nen tag cutitself refuses all three;
there is no flag that overrides it.
5. The one judgement this skill must not make alone
CON-36 clause 4 says a tag proceeds "only when no such branch is live for the change being
released", and also that liveness is "detected mechanically (no judgement call)". Whennen release preflight's mechanical three-part test reports live chores but none has partial scope
on main, those two halves disagree. That is a G5 ask, put through the harness's question
interface, with the chores named, what each carries, and a recommendation — never a call getsuga
makes on its own and mentions afterwards. Record the ruling in the release PR body.
6. An off-main target — build it, then tag it
The maintainer's ruling: an unreachable target is driven to main first, not refused.
- Say what is about to happen — this is now a delivery, and it stops at their gate partway.
- Get it onto a PR. An existing open PR for that branch is used as-is. A branch with no PR
needs one — check that branch out first, then hand it to
`hatsu:tensho`, which works on the current checkout, not on a branch
named as an argument. Its § 2 (nen wc classify) then sees "on a branch, clean" and opens the PR. - Drive it to
CON-32readiness. Hand it to `hatsu:sharingan` — its full
engine, not a substitute for one: the first-blocking-condition diagnosis, thread stewardship,
the wake channel, the adversarial confirmation pass and the escalation ladder are all that
skill's, and getsuga does not restate or reimplement any of them. Invoke it ashatsu:sharingan <CODE>#<N> to <G2|G4>against the gatenen gate derivenamed, and take its
stop-at-the-gate report as the readiness call.
Fallback only ifsharingancannot run at all (an unresolvable code, say): the readiness call by
itself is `hatsu:pr-state`'s verb —nen pr ready <ref> --explain, withGH_TOKENexported, quoted verbatim. The identities come from `sharingan`
§ 4's rule, never from memory: the target's ownnen/gates.jsonfirst, with no--gatesat all
— theschemas/gates.jsonlocation is not a fallback at the pinned build, REMOVED atv0.4.0, so a gates file only there is refused exactly like one carrying none;--gates "$hatsu_root/contracts/reference.gates.json"(absolute —$hatsu_rootSET IN THAT SAME SHELL by the
explicit-input linehatsu_root='<the absolute path § 0 printed>', the value
`ten` § 0 prints already single-quoted with any'escaped, pasted
quotes included, or bypr-state§ 2's resolver; never a variable carried from another shell, never a
raw path embedded in the command text, never$CLAUDE_PLUGIN_ROOTalone, which is Claude Code's) ONLY
where the target is frozen<reference-repo>itself; every other target that ships no gates file gets--reviewersplus explicit--approverssupplied by hand — reviewer identities from itsCODEOWNERSor the PR's own requested
reviewers, approval policy from an authoritative target declaration or maintainer ruling. A repository is never judged
by another repository's reviewers. On that hand-supplied path, Nen 0.10.0 defaults omitted--approversto the reviewer set. Pass the target's declared approvers explicitly, or--approvers ""only for a declaredreview-round-onlypolicy, and state the policy source on the
page — whilecontracts/reference.gates.jsoncarries its owndefault_approvers(sasuke,tenma), so the--gatesform is a real approver check even with no--approversflag. That is
a check, not a drive: it reports where the PR stands and nothing moves it. - Stop at G2/G4. The maintainer merges.
- Re-resolve the target — it is now a commit on
main, and a different commit than the
branch tip was (a merge commit). Tag that. Then § 2 from the top, because every precondition must
hold at the new cut point.
7. Fan-out (CON-22), after the tag
Compute the affected set with one verb:
nen --repo <path> fanout compute --range <vPrev>..<newTag>changed-workflows(vPrev..newTag) intersected against each registered consumer's consumes fromnen/repos.json — every consumer comes back AFFECTED
with the workflow basenames that hit it, or an implicit N/A. Verified live against the real range v0.11.2..v0.11.3
(docs/ab/getsuga.md § 2.6): nen's output reproduces the historical CON-22 determination
recorded by hand at that release's own registry entry (RR-PR-#916) exactly — <product-repo-A>
affected via the same five files, <product-repo-B> via the same four, <scaffold-repo> via the
same one. Record every unaffected consumer as an explicit N/A with its basis — an
unstated N/A is indistinguishable from an unswept repo.
An empty affected set is a real and common result. Say so with the basis; do not go looking for
something to repin.
nen fanout record --range <vPrev>..<newTag> [--ledger <path>] appends the same computation to an
audit ledger — it never opens a repin PR itself, per its own --help; this is a mutating write
(a local ledger file, not GitHub) and was A/B'd by contract inspection only, never exercised against
a live repo (docs/ab/getsuga.md § 3). Opening the repin PR in each affected consumer remains
this skill's own action — it targets other repositories, which no nen verb here does.
7a. A deploy — the plan is printed at G3; the run is the maintainer's
A tag is not a release, and a release is not a deploy. Where the repository being cut declares adeploy verb and a named destination in its nen/contract.json (project.verbs.<lane>.deploy plus a
key of project.targets — nen v0.3.0), the G3 stop carries the plan beside the preflight table:
nen shu deploy --repo <path> --lane <lane> --target <name> # the plan: exit 0, nothing sentWithout --run the verb prints the fully resolved plan — the destination's args substituted into the
lane's own argv, every precondition and requiresEnv variable asserted (never read or printed),
each step as would run: — and spawns nothing, at exit 0; --target is required with no default,
even for a single declared destination, and --run --dry-run together is exit 2. Exit 0 is the plan
on a host that could send it: an unsatisfied precondition — the lane's own, or a variable the target'srequiresEnv names that is not set here — is exit 2 with the report (the plan still prints, the
failing rows marked FAIL, and the last line says how many preconditions are unmet; verified live atv0.3.0: "1 precondition on lane 'site' is not satisfied. nen ASSERTS a precondition and never performs
it"), and a --target naming no declared destination is exit 2 listing the declared ones. A G3 stop
carries whichever of those the host produced, never a plan rewritten to look sendable. --run is the
maintainer's word at G3 (CON-6), per target, recorded in the release PR body — never this skill's. A
lane whose deploy is a seat answers exit 4 with the declaration's own reason whatever --target
says (verified live at v0.3.0 on a scaffolded nextjs tree: "'deploy' is unsupported on lane
'nextjs' … The declaration's own reason: PROPOSED SEAT — replace it …"), and that is the whole report
for such a repository: it declares no deploy. A repository with no declaration at all is not deployed
through nen and this section does not apply. <reference-repo> is machinery and declares none.
8. Authority
- Permitted: the tag cut (
CON-33(b)/CON-41— Kurapika's own duty, not aCON-25
delegation), the release PR, the repin PRs, and printing a declared deploy's plan (§ 7a). - No routing and no release delegation — none, including while driving. Driving an off-
main
target runs entirely through `hatsu:tensho` and
`hatsu:sharingan`, and neither of those releases anything (CON-25, fourth
carve-out). If a target genuinely needs an issue routed or released to reachmain, that is
`hatsu:build`'s job and its own invocation — say so and stop, rather
than borrowing its authority from inside this run. - Never: merge
main— save its own release-proposal PR through § 3a'snen pr merge --release-unit, never a delivery PR; publish a GitHub Release or authorize a release (G3 is the
maintainer's,CON-6, and the phase itself is `mugetsu`'s, per target);
runnen shu deploy --run, in any spelling — that is `kagutsuchi`'s
phase, per target, and equally the maintainer's own call; move or delete a tag; writelatestfor
a tag that does not resolve; apply a G1 mode label.
9. Hard limits
- Never tags a commit unreachable from
origin/<branch.base>—nen release resolve-targetandnen tag cut's own--atcheck both enforce this; verified live in scratch, never against<reference-repo>orhatsu. - Never tags past an open
critical, an activeRELEASE_HOLD, or an unreconciledCON-33(c)—
and read theRELEASE_HOLDrow exactly as printed: since nenv0.2.0true/1/yesis held,false/0/no/unset is not, and any other non-empty value fails closed as held (§ 2). - Never deploys. § 7a's plan is the most this skill prints;
--runis the maintainer's at G3,
through `kagutsuchi` (non-production) or
`mugetsu` (production), per target, never from here — a futon chain's later
step is that skill's own run on the advance go, not this skill's. - Never merges past § 3a: not a PR it did not open in this run (the recorded number and head SHA),
never by a rawgh pr merge, never on anen pr mergethat read anything butmerged:, never
around a protection rule, and never with a review or approval of its own. - Never rules on
CON-36clause 4 itself — that isG5. - Never deletes a superseded CHANGELOG entry to resolve a contradiction.
- Never publishes the release. Preparing it is the job; G3 is not — publication is
mugetsu's
phase, on the maintainer's recorded per-target go. - Never routes around a refused capability.
- Never re-orders
nen changelog collate --write's written section by eye. The written body is
already correct (newest-first,CON-33(b)) — verified live against the old script's own written
output, byte-for-byte. RETIRED at nen0.7: the printed manifest no longer disagrees with the
write (§ 3), so relay it as the record of what landed rather than as a defect to note — and the
rule against re-ordering the section stands on its own, unchanged.