- name
- entwurf-release
- description
- Operate entwurf SemVer releases through four explicit modes: land, prepare, make, and publish. Use for pre-version exact-SHA CI landing, CHANGELOG and package preparation, static and LIVE gates, prepared-HEAD CI, exact artifact acceptance, tag and GitHub release creation, repair-dist-tag publication, and post-publish registry proof. Each mode is a separate authority boundary. Triggers: release land, prepare-release, make-release, publish release, release cut, prerelease, repair release.
- user_invocable
- true
# entwurf-release
Repository: `~/repos/gh/entwurf`.
This skill is the shared release-operation SSOT that replaces the former
`.pi/prompts/prepare-release.md` and `.pi/prompts/make-release.md` files.
Claude Code discovers it natively under `.claude/skills/`; pi discovers the same
file through `.pi/settings.json` and its `"skills": ["../.claude/skills"]`
entry.
## Invocation
```text
# Claude Code - stable example
/entwurf-release land 0.12.8
/entwurf-release prepare 0.12.8
/entwurf-release make 0.12.8
/entwurf-release publish 0.12.8 /absolute/path/to/candidate.tgz latest
# pi - prerelease/repair example
/skill:entwurf-release land 0.12.8-repair.1
/skill:entwurf-release prepare 0.12.8-repair.1
/skill:entwurf-release make 0.12.8-repair.1
/skill:entwurf-release publish 0.12.8-repair.1 /absolute/path/to/candidate.tgz repair
```
Natural-language requests map to the same four modes.
- `land` pushes an already reviewed pre-version HEAD and waits for the required
exact-SHA CI jobs. It never edits, versions, tags, or publishes.
- `prepare` edits release records, runs deterministic and LIVE gates, and creates
the release-prep commit. It never pushes, tags, or publishes.
- `make` pushes the prepared HEAD, waits for exact-SHA CI, creates and accepts one
preserved candidate, then tags, stamps, and creates the GitHub release. It
never runs `npm publish`.
- `publish` publishes only the already accepted preserved candidate under an
explicitly supplied dist-tag and proves the registry-installed result.
The invocation authorizes only the named mode. `prepare` is not `land`
authorization. `make` is not `publish` authorization. If the mode, version, or a
mode-specific required argument is missing, ask for it and stop.
## Shared version contract
Accept a normal SemVer release or prerelease. Reject a leading `v`.
```bash
VERSION="<user argument>"
case "$VERSION" in
"") echo "ABORT: version required (for example 0.12.8 or 0.12.8-repair.0)"; exit 1 ;;
v*) echo "ABORT: drop the leading 'v'"; exit 1 ;;
esac
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ ]]; then
echo "ABORT: version must be SemVer, optionally with a prerelease suffix"
exit 1
fi
```
Valid examples: `0.12.8`, `0.12.8-repair`, and `0.12.8-repair.0`.
`npm version` remains the final package-version validator.
## Exact-SHA CI oracle
The shared helper is the only release instruction that classifies the required
GitHub Actions run:
```bash
CI_ORACLE=".claude/skills/entwurf-release/scripts/verify-exact-ci.sh"
bash "$CI_ORACLE" "$(git rev-parse HEAD)" wait
```
It selects only a push-triggered `ci.yml` run whose `headSha` is the supplied
full SHA, waits when requested, and requires these exact jobs to conclude
`success`:
- `check`
- `install-surface`
- `artifact-consumer`
- `macos-install-surface`
Use mode `verify` instead of `wait` when a prior run must already be complete.
Never replace this with a branch-level green badge or the newest unrelated run.
---
# LAND
`land` exists for a narrower release contract that requires a pre-version
implementation HEAD to receive its own CI run before release metadata changes.
It is not required for every ordinary release.
A `land <version>` invocation is explicit authorization for one ordinary push of
`main`. It is not authorization for version edits, tags, GitHub releases, or npm
publication.
## L0. Establish the landing boundary
1. Read `AGENTS.md`, `NEXT.md`, and `VERIFY.md` completely.
2. Read the `commit` skill because its push and post-push stamp rules remain in
force.
3. Confirm that the current narrower contract actually requires a pre-version
CI checkpoint. If it does not, stop and direct the operator to `prepare`.
4. Inspect and require a clean, non-diverged `main`:
```bash
git status --short --branch
git diff-index --quiet HEAD --
test "$(git branch --show-current)" = main
git fetch origin main
read -r BEHIND AHEAD < <(git rev-list --left-right --count origin/main...HEAD)
test "$BEHIND" = 0
test "$AHEAD" -gt 0
```
Confirm from the diff and log that HEAD contains only the reviewed landing set.
Do not absorb an unrelated local commit into a release push.
For a required pre-version checkpoint, the package must not already equal the
target version:
```bash
test "$(node -p "require('./package.json').version")" != "$VERSION"
```
## L1. Prove pushability and push main
```bash
SHA="$(git rev-parse HEAD)"
git push --dry-run origin main
git push origin main
test "$(git ls-remote origin refs/heads/main | cut -f1)" = "$SHA"
```
Never force and never bypass verification.
## L2. Stamp the pushed commit
Stamp only after the push succeeds, following the `commit` skill. If the stamp
fails, report the exact error and stop; do not write the agenda target by hand.
## L3. Require exact-SHA CI
```bash
CI_ORACLE=".claude/skills/entwurf-release/scripts/verify-exact-ci.sh"
bash "$CI_ORACLE" "$SHA" wait
```
The oracle requires four axes on one run at that exact SHA: the workflow
conclusion, the FOUR required job conclusions (`check`, `install-surface`,
`artifact-consumer`, `macos-install-surface`), and the `check` job's
`./run.sh check-gate-qualification` step concluding `success`. A skipped body is
not evidence, so it fails the same way a red one does. `macos-install-surface`
joined the required set in 0.20.0 on its first green; a red macOS runner now
blocks a cut exactly like any other required job.
If the oracle names the qualification step -- absent or skipped -- the body did
not run at this SHA. Force it, wait, and re-run the oracle. `gh workflow run`
takes a branch, never a SHA, so the branch must still point at `$SHA` when it is
dispatched; the oracle re-checks `headSha` afterwards and refuses a run whose
branch moved.
`gh workflow run --ref` runs the REMOTE branch head, so check that head, not the
local one. And do not sleep: until the dispatch run is registered, the oracle's
newest-run rule would pick the already-finished push run and ABORT on the same
axis. Wait for the run to appear, then hand it to the oracle.
```bash
BRANCH="$(git rev-parse --abbrev-ref HEAD)"
git fetch origin "$BRANCH"
test "$(git rev-parse FETCH_HEAD)" = "$SHA"
count_dispatch_runs() {
gh run list --workflow ci.yml --event workflow_dispatch --commit "$SHA" \
--limit 20 --json databaseId --jq length
}
BEFORE="$(count_dispatch_runs)"
gh workflow run ci.yml --ref "$BRANCH" -f qualify=true
for _ in $(seq 1 12); do
sleep 5
if [ "$(count_dispatch_runs)" -gt "$BEFORE" ]; then break; fi
done
test "$(count_dispatch_runs)" -gt "$BEFORE" || {
echo "ABORT: no dispatch run appeared for $SHA within 60s" >&2
exit 1
}
bash "$CI_ORACLE" "$SHA" wait
```
Report the SHA, workflow URL, the run event, all FOUR required job conclusions
(`check`, `install-surface`, `artifact-consumer`, `macos-install-surface`), and the
qualification-step conclusion. End with:
```text
Landing checkpoint complete. Ready for /entwurf-release prepare <version>.
```
---
# PREPARE
`prepare` edits, verifies, and commits. It does not push, tag, create a GitHub
release, stamp a release, notify, or publish.
## P0. Establish the release boundary
1. Read `AGENTS.md`, `NEXT.md`, and `VERIFY.md` completely. A narrower current
release contract in those files overrides a generic instruction in this
skill.
2. Read the `commit` skill before creating any commit.
3. Inspect the current state:
```bash
git status --short --branch
git diff --check
```
Do not mix pre-existing implementation or review fixes into the release-prep
commit. If a completed, clearly scoped fix is present and GLG has approved its
commit, close it as a separate atomic commit first. If scope is ambiguous or
unrelated, stop and ask.
If the current contract requires a pre-version landing checkpoint, verify it
before making any edit:
```bash
git fetch origin main
SHA="$(git rev-parse HEAD)"
test "$(git rev-parse origin/main)" = "$SHA"
bash .claude/skills/entwurf-release/scripts/verify-exact-ci.sh "$SHA" verify
```
A missing landing run is not a prepare failure to work around. Stop with the
exact next command: `/entwurf-release land <version>`.
Prepare may commit only release-prep files such as `CHANGELOG.md`,
`package.json`, `pnpm-lock.yaml`, and an evidence handoff explicitly required by
the current release contract.
Forbidden in prepare mode:
- pushes
- tags
- GitHub releases
- release agenda stamps
- notifications
- npm publication
- final candidate creation for a contract that requires post-commit CI first
## P1. Audit changes since the last release
```bash
LAST_TAG=$(git tag --sort=-version:refname | head -1)
printf 'baseline=%s\n' "$LAST_TAG"
git log "${LAST_TAG}..HEAD" --oneline
```
Compare the commit range and closed `NEXT.md` work with the existing
`CHANGELOG.md` `## Unreleased` section. Record only verified changes. Do not
rewrite historical release sections.
## P2. Promote the release section
Use the current KST date and transform the top of the changelog into this shape:
```text
## Unreleased
## <VERSION> - YYYY-MM-DD
```
Keep a fresh empty `## Unreleased` section above the promoted release body.
Preserve the repository's existing heading punctuation if it uses an em dash.
Release-gate paths and summaries may live in the release section or in an
explicit durable operator handoff, following the repository's current
convention. The paths and actual MUST/BEHAVIOR counts must not be lost.
## P3. Update package version and lockfile
```bash
npm version "$VERSION" --no-git-tag-version
pnpm install --lockfile-only
```
Inspect the resulting diff. Do not manufacture a lockfile change when the
resolver produced none.
## P4. Run the deterministic floor
```bash
pnpm run check:full
```
The FULL tier is the candidate floor; the everyday `pnpm check` core alone is
never release evidence. Do not summarize the aggregate as a fixed number of
gates. The current `package.json` check:* scripts are the SSOT. If any check
fails, stop at that axis, fix it, and rerun the complete aggregate.
The check chains carry only the qualification HEAD (`check-gate-manifests`) and
deliberately exclude the mutant-executing body `check-gate-qualification`: the
LIVE release gate (P5) runs the body as its own MUST step, and the exact-SHA CI
`check` job (M2) requires it on the release commit. Do not add a manual
qualification rerun here.
Pushing the release tag creates no run, so the exact-SHA oracle never reads a
tag run. It reads whichever run at that commit carries the body: the branch push
when the push touched the qualification surface, otherwise the dispatch run the
recovery above creates.
## P5. Run the LIVE release gate from fresh scratch
Use the `tmux` skill because this command is long-running. Preserve the scratch
directory and complete log.
```bash
SCRATCH=$(mktemp -d "/tmp/entwurf-release-gate-${VERSION}.XXXXXX")
LOG="$SCRATCH/release-gate.log"
set -o pipefail
LIVE=1 ./run.sh release-gate "$SCRATCH" --cut 2>&1 | tee "$LOG"
```
The release gate has two tiers:
- `MUST` is release-blocking and owns the exit code. `FAIL` must be zero, and
`--cut` enforces the other half: any MUST `SKIP` makes the run red, so a
release run can no longer hide required LIVE work behind a skip. Each step is
invoked and reports its own outcome (exit 0 PASS / 97 SKIP / else FAIL); a
`[entwurf:skip]` line names the prerequisite that was missing. Drop `--cut`
only for an unattended diagnostic pass, which is not acceptance.
- `BEHAVIOR` is advisory model-in-loop evidence. A failure does not block the
release, but its PASS/FAIL counts and artifact path must be recorded.
Do not expect a fixed PASS count. Record actual output. Do not waive a MUST
failure without diagnosing and explicitly classifying the failing axis. Do not
hide a BEHAVIOR failure.
**Freeze HEAD, index, and working tree while the gate is running.** `check-gate-qualification` pins
the origin HEAD and aborts if that moves, but the other sequential P5 steps consume the candidate
checkout directly. A working-tree or index edit can therefore make one release-gate verdict describe
multiple candidates even when qualification's own snapshot stays pure. Queue every edit and commit
request (including one from GLG) until the gate reports its verdict; any movement voids that P5 receipt.
**When the gate reports its verdict, run P9 before doing anything else.** This is the
heaviest resource event in the whole release (the full floor, `check-gate-qualification`
and every LIVE smoke on one host), and it is where residue is BOTH largest and freshest,
so the prefix a leak carries still names the gate that produced it. Do it here, not only
at P8: by P8 the trail is cold. Report the P9 numbers together with the gate's own
verdict; a large residue at this point is a finding about a gate, not housekeeping.
## P6. Apply release-specific pre-commit acceptance
`NEXT.md` and `VERIFY.md` may require gates beyond `pnpm run check:full` and the LIVE
release gate. Apply every requirement that belongs before the release-prep
commit.
For #51-style repair releases, do not create the final candidate here. The exact
candidate must be created from the clean prepared HEAD only after that exact SHA
has been pushed and all FOUR required CI jobs are green. `make` owns that post-CI
acceptance. A checkout pack-once result is not release-artifact evidence.
Never claim an unrun gate as passed.
## P7. Create the release-prep commit
Stage only release-prep files. Never pull preceding implementation changes into
this commit.
```bash
git status --short
git diff --check
git diff --cached --check
git commit -m "chore(release): prepare v${VERSION}"
```
Do not bypass hooks. A commit request does not authorize a push.
## P8. Final preparation check
```bash
test "$(node -p "require('./package.json').version")" = "$VERSION"
grep -qE "^## ${VERSION}([[:space:]]|$)" CHANGELOG.md
git diff-index --quiet HEAD --
```
Report:
- prepared version and commit SHA
- `pnpm run check:full` result
- release-gate scratch, log, and artifact paths
- actual `MUST: PASS=n FAIL=0 SKIP=n` (includes the `check-gate-qualification` MUST step)
- actual `BEHAVIOR: PASS=n FAIL=n`
- release-specific work deliberately deferred to `make`
- clean-tree result
- host residue before and after P9, by prefix
## P9. Reclaim the host
Run this **twice**: once the moment P5's release gate reports its verdict (freshest
trail, largest residue), and again here at the end of `prepare`. `publish` calls it a
third time after U3. It is the same procedure each time.
The floor and the LIVE gates are the heaviest resource consumers this repo has, and
what they leave behind is invisible until a disk fills. Measure it on the host that
just ran them, and reclaim only what nothing is using.
A gate that ends RED ends by THROWING, so any teardown written as its last statement is
skipped. That is how `oracle` reached ~9,200 stale roots and 3.8G under `/tmp` before
2026-09-08. Gates now register their roots with `scripts/lib/reclaim-on-exit.ts`, so this
step should find LITTLE. A large number here is not routine housekeeping: it names a gate
that still reclaims on its last line, or a fixture child with no parent-death watchdog.
Report the prefix, do not just delete it.
**Census prefix-blind; delete by a prefix this repo can prove it owns.** The first
version of this step looked only at `entwurf-*` and reported a clean host while 5,329
roots sat under `psa-*`, `acp-*` and `omp-*`: a census that only counts what it already
suspects will always confirm the fix it was written for. So the report below counts every
directory this operator owns under `/tmp`, while the DELETE list is derived from the
gates' own `mkdtempSync` prefixes at run time. A hand-kept name list would rot on the
next gate; a prefix-blind `rm` would take `nix-shell`, editor and toolchain state that is
not ours.
Report (read-only, prefix-blind, and never counts a root some live process is using):
```bash
mapfile -t IN_USE < <({ ps -eo args --no-headers | grep -oE '/tmp/[A-Za-z0-9._-]+';
for l in /proc/[0-9]*/cwd; do readlink "$l" 2>/dev/null; done; } |
sed -E 's#(/tmp/[^/]+).*#\1#' | sort -u)
Ver en GitHub