Skip to content

/release

The single permitted path to a version tag, for any target declared in the project's .codearbiter/release-targets.md. Routed to when the user invokes /release on a non-default branch with a green suite. Takes the declared target as its one argument, derives the SemVer bump from

shell
$ npx -y skills add arbiterForge/codeArbiter --skill release --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.
  • You can call itInvoke it directly when you want it.
  • Slash command/release
How auto-invocation works

Context preview

The summary Claude sees to decide when to auto-load this skill.

The single permitted path to a version tag, for any target declared in the project's .codearbiter/release-targets.md. Routed to when the user invokes /release on a non-default branch with a green suite. Takes the declared target as its one argument, derives the SemVer bump from

SKILL.md

release.SKILL.md
name: release
description: The single permitted path to a version tag, for any target declared in the project's .codearbiter/release-targets.md. Routed to when the user invokes /release on a non-default branch with a green suite. Takes the declared target as its one argument, derives the SemVer bump from Conventional-Commits history since that target's last tag, rolls the commits into that target's CHANGELOG, writes an annotated tag in that target's namespace, and on authorization publishes it as a GitHub Release with the changelog section as its notes. A release commit, if needed, routes through commit-gate; the tag and Release are never published without explicit authorization.

release

The single permitted path to a version tag. Routed to when the user invokes `/release [target]`. Derive the bump from the commit log, update the changelog, tag — nothing more.

**One command, any number of declared targets.** A project declares one or more release targets in `{{PROJECT_DIR}}/.codearbiter/release-targets.md` (grammar and parser contract: `{{PLUGIN_ROOT}}/hooks/_releaselib.py`'s module docstring). `/release` takes the target's name as its only argument. When `$TARGET` is omitted and the declared file names exactly one target, that target is used — a single-target project's bare `/release` behaves exactly as it always has. When more than one target is declared, `$TARGET` is required; STOP and ask rather than guessing which one a bare invocation meant. **Resolve the omitted-single-target case mechanically, never by assumption** (MEDIUM, adversarial review 2026-07-31: `tag-prefix` itself takes `$TARGET` as a REQUIRED positional argument and has no way to express "the implicit one", so naming it here was not itself enough — the mechanical step that turns an omitted target into a concrete name before `tag-prefix` is ever called has to be spelled out too): run `"$PY" "{{PLUGIN_ROOT}}/hooks/_releaselib.py" list-targets` first — the sanctioned enumeration, through the same tested grammar `tag-prefix` already reads, rather than a by-eye scan of the delimiter block. Exactly one printed line confirms which name `$TARGET` is; more than one is the multi-target STOP above, restated by the tool rather than assumed. There is deliberately no second command per target: N commands would be N public surfaces to govern, catalog, and carry, for one operation whose only difference is which declared row it reads.

Every phase below is written once, against that row. Nothing in this skill is per-target prose.

**Interpreter convention, stated once and applying to every helper invocation in this file** (A-3.6). `python3` is not universally present — a Windows consumer commonly has `python` on PATH and no `python3` at all, and a literal `python3` spelling fails on every invocation at once there.

**Resolve the interpreter ONCE, by presence, before the first invocation:**

PY=python3; command -v python3 >/dev/null 2>&1 || PY=python

Every helper invocation below is then spelled `"$PY" "{{PLUGIN_ROOT}}/hooks/<script>" <args>` literally, one spelling throughout — including the inline `"$PY" -c "…"` snippets. Two spellings for one thing invites reading the difference as meaningful (blind exercise run 14 flagged exactly that when only three steps used `"$PY"` and fifteen still said `python3`).

**Both quotes are load-bearing, and the second one is the easier to lose** (HIGH-1, blind exercise run 16). Quoting only the interpreter — `"$PY" {{PLUGIN_ROOT}}/hooks/<script>` — leaves the script path exposed to word splitting, and a plugin root containing a space is an ordinary Windows install (`C:\Users\First Last\.claude\plugins\…`, since an account name with a space is unremarkable). On such a host the path splits at the space, Python is handed a truncated filename, and EVERY step of this lane fails at once: target resolution, `last-tag`, `classify-window`, `check-manifests`, `classify`, `notes-match`. The operator's only diagnostic is `can't open file '…\First'`, which names nothing recognisable. The same applies to any `{{PROJECT_DIR}}`-rooted path passed as an argument. The one deliberate exception is `$PAYLOAD`, which is a git pathspec that MUST word-split — see its own note under "Targets".

**MUST NOT spell them `python3 "<script>" … || python "<script>" …`.** `||` branches on the EXIT CODE, and it cannot distinguish "no such interpreter" from "the helper ran and told you something". This lane's helpers answer in exit codes by design — `run-pre-tag` returns 5 for drift and 6 for a mutating check, `semver-greater` and `check-manifests` each separate "no" from "could not compare" — so the `||` form re-runs the whole command on every one of those answers and then reports the SECOND run's code. For `run-pre-tag` that means executing the project's declared pre-tag commands twice and losing the verdict of the first. The fallback must key on whether the interpreter EXISTS, which is what `command -v` tests, not on what it said.

Targets

Resolve `$TARGET`'s row from the declared file FIRST and use it throughout — never a hardcoded table. An unparseable declared file (any parser-contract violation on a file that DOES exist — including one that exists but carries no delimiter block at all, `FileExistsNoBlockError`) → STOP and surface the parse error; never guess a row's shape, and never treat an existing-but-broken file as an opportunity to back-fill it (see "Back-fill" below for why that distinction is mechanical, not a judgment call). A genuinely ABSENT declared file — nothing on disk at all, the one state `AbsentBlockError` alone names — enters the "Back-fill" lane below instead of stopping outright; that lane never runs against a file that already exists, in any state. Resolve `$TAG_PREFIX` through the shared mechanism, never typed from memory: `TAG_PREFIX=$("$PY" "{{PLUGIN_ROOT}}/hooks/_releaselib.py" tag-prefix $TARGET)`. Where a hosted publish lane's own namespace resolution is ALSO wired to read

Read more
Read it on GitHub ↗

Showing the first part of this file.

Ships withcodearbiter

When you can't trust yourself with your code base, trust Arbiter.

Get the whole plugin, auto-invoked
Stats
138
Stars
0
Views
7
Forks
Active
Maintenance
Python
Language
AGPL-3.0
License
6h ago
Last commit
2mo ago
Created

Repo: arbiterForge/codeArbiter

Other skills on codearbiter.