zheref

getsuga

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>.

zheref 0 Updated 3w ago
GitHub

Install

npx skillscat add zheref/hatsu/getsuga

Install via the SkillsCat registry.

SKILL.md

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 by
ten 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 on main — 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's nen pr merge --release-unit, or the maintainer's where it fell
back → the post-merge tag (§ 4) → the CON-22 fan-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 by nen pr merge --release-unit inside § 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 reason
    CON-33(b) asks for the PR first.
  • The release unit is `susanoo`'s, not this skill's. Where the repository
    declares an archive, producing the distributable is that phase — the declared verb, run locally,
    uploading nothing — and getsuga consumes the unit rather than building it. A lane whose archive
    is a seat answers exit 4 with the declaration's own reason and that is the whole report for
    such a repository; verified live at 0.3.0 against this plugin's own plugin lane
    (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 main

The 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 whose
branch.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-fetched
origin/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 (the rev-parse above 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 full bankai:severity/critical, never the bare
    critical
    — a bare critical label does not exist in nen/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 all that
    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 candidate integration/<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
    targets main from it (gh pr list --state open --json baseRefName,headRefName). nen computes
    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 nen v0.2.0 (#63, closes zheref/nen#23):
RELEASE_HOLD is now parsed for truthiness, not mere presence.
At the port, <reference-repo>'s
RELEASE_HOLD Variable — literally the string "false", meaning not held under the old
scripts/tag_cut.sh semantics — reported FAIL RELEASE_HOLD -- HELD: RELEASE_HOLD = 'false', so a
repo that had ever set the Variable to false read permanently HELD (docs/ab/getsuga.md § 2.2.1).
At v0.3.0 the rule is the old script's own, stated in nen release --help (verified): "Case-
insensitive true/1/yes reads 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."
So RELEASE_HOLD=false no longer holds a release, and the
"delete the Variable rather than set it false" workaround is retired. Relay the row exactly as nen
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 at
v0.3.0 (the row is a gh variable get read); the --help text and the v0.2.0 changelog are the
evidence.

A second, safer divergence: nen release preflight's RELEASE_HOLD read fails CLOSED on an
unreachable gh, where the old scripts/tag_cut.sh failed OPEN.
Verified live (docs/ab/getsuga.md
§ 2.2.2): with gh made unreachable, nen reports FAIL RELEASE_HOLD -- could not be read: Executable not found in $PATH: "gh" and the whole table exits 1 — a gh outage refuses the
release rather than silently waving it through. The old script's own hold_value="$(gh variable get RELEASE_HOLD 2>/dev/null || true)" (scripts/tag_cut.sh, read at the frozen v0.11.3
snapshot) instead swallows the failure into an empty string, which hold_active("") reads as
inactive — a gh outage 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 marker escape
hatch (CON-7) has no nen equivalent.
No getsuga path — this port, or any real cut recorded
in docs/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 declared archive, run locally, uploading nothing —
which landed at Hatsu v0.6.0. Where a repository declares no archive, 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)
and mugetsu (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 futon then step 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:

  1. 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 neither CHANGELOG.md nor
    changelog.d/ touched. With --write, it rewrites CHANGELOG.md in 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 write

    The printed manifest and the written section now agree, and the manifest is the one that
    moved.
    Through the pinned v0.6.0 the manifest was printed in readdirSync order 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.md

    while both wrote FRAG-30, FRAG-20, FRAG-10 into the section. --json's fragments[] 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 is CON-33(b).

  2. 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 with
    git ls-tree -r <prevTag> -- changelog.d, never with file timestamps. No nen verb owns this
    placement decision
    — it stays a direct git read plus judgment (residue, docs/ab/getsuga.md
    § 3).

  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> range v0.11.2..v0.11.3
    (docs/ab/getsuga.md § 2.4): same verdict as the old
    scripts/changelog_release_completeness_check.sh v0.11.2 v0.11.3 run read-only side by side —
    both report every merged PR reconciled, exit 0. Since nen v0.2.0 (#83) --fragment-dir defaults
    to changelog.d here and in release 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 "".

  4. Bump latest in the registry — nen/repos.json, which at the pinned build is the ONLY
    file nen reads it from. No nen verb 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 under schemas/).
    RETIRED at nen 0.5: there is no shadowed leftover FAIL any more, because there is no read
    to protect — nen/ is the only file anything reads. A schemas/ copy sitting beside a loaded
    nen/ one is a warn leftover naming the git rm that clears it, and whether its bytes
    still agree no longer changes the verdict. Edit nen/repos.json; delete the stale duplicate.

  5. Bump the version manifest — same reasoning as the old skill: a cached plugin would report
    consumers current while they sit a tag behind. No nen verb owns this write either — residue, a
    direct edit; prefer the bump to a no plugin bump: opt-out. Where the surface stamp follows
    plugin.json and 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, and release.unitPaths says 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 against
v0.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 merge

The 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:

  1. pr ready — CON-32 readiness, naming the exact commit it judged.
  2. 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.
  3. pr body-check — against the PR's live body and the repository's own
    --requirements-from (`tensho` § 6's {name, pattern} array).
  4. release unit-check — every changed path inside the release unit, release.unitPaths as
    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.
  5. whose pr — the PR is not from a fork and its author is the gh viewer.

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 merges
main — 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, through
nen 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. --at is required and never defaulted;
nen tag cut --help states this outright. "Cut from main" is not an instruction the verb can
follow: if main advanced between the merge and the cut, HEAD is 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 the v0.10.0 window 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 the changelog.d/ or CON-33(c) row fails there, the cut needs another
    collation lap.
  • --at not an ancestor of origin/<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> or hatsu (docs/ab/getsuga.md § 2.5):
    cutting at an unreachable commit refuses with is not an ancestor of origin/<trunk> ... Use 'nen release resolve-target' first; re-cutting an existing name refuses with already exists locally -- never re-tagged.
  • The tag is created LOCALLY ONLY unless --push is passed — verified live: without --push,
    git ls-remote --tags origin on 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 old scripts/tag_cut.sh always
    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 write latest or 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 cut itself 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)". When
nen 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.

  1. Say what is about to happen — this is now a delivery, and it stops at their gate partway.
  2. 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.
  3. Drive it to CON-32 readiness. 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 as
    hatsu:sharingan <CODE>#<N> to <G2|G4> against the gate nen gate derive named, and take its
    stop-at-the-gate report as the readiness call.
    Fallback only if sharingan cannot run at all (an unresolvable code, say): the readiness call by
    itself is `hatsu:pr-state`'s verb — nen pr ready <ref> --explain, with
    GH_TOKEN exported, quoted verbatim. The identities come from `sharingan`
    § 4's rule, never from memory:
    the target's own nen/gates.json first, with no --gates at all
    — the schemas/gates.json location is not a fallback at the pinned build, REMOVED at
    v0.4.0, so a gates file only there is refused exactly like one carrying none; --gates "$hatsu_root/contracts/reference.gates.json" (absolute — $hatsu_root SET IN THAT SAME SHELL by the
    explicit-input line hatsu_root='<the absolute path § 0 printed>', the value
    `ten` § 0 prints already single-quoted with any ' escaped, pasted
    quotes included, or by pr-state § 2's resolver; never a variable carried from another shell, never a
    raw path embedded in the command text, never $CLAUDE_PLUGIN_ROOT alone, which is Claude Code's) ONLY
    where the target is frozen
    <reference-repo> itself; every other target that ships no gates file gets --reviewers plus explicit
    --approvers supplied by hand — reviewer identities from its CODEOWNERS or 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
    --approvers to the reviewer set
    . Pass the target's declared approvers explicitly, or
    --approvers "" only for a declared review-round-only policy, and state the policy source on the
    page — while contracts/reference.gates.json carries its own default_approvers (sasuke,
    tenma), so the --gates form is a real approver check even with no --approvers flag. That is
    a check, not a drive: it reports where the PR stands and nothing moves it.
  4. Stop at G2/G4. The maintainer merges.
  5. 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 from
nen/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 a
deploy 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 sent

Without --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's
requiresEnv 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 at
v0.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 a CON-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 reach main, 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's nen 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);
    run nen 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; write latest for
    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-target and
    nen tag cut's own --at check both enforce this; verified live in scratch, never against
    <reference-repo> or hatsu.
  • Never tags past an open critical, an active RELEASE_HOLD, or an unreconciled CON-33(c) —
    and read the RELEASE_HOLD row exactly as printed: since nen v0.2.0 true/1/yes is 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; --run is 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 raw gh pr merge, never on a nen pr merge that read anything but merged:, never
    around a protection rule, and never with a review or approval of its own.
  • Never rules on CON-36 clause 4 itself — that is G5.
  • 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 nen 0.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.