--- 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:** ```sh PY=python3; { command -v python3 >/dev/null 2>&1 && python3 --version >/dev/null 2>&1; } || PY=python ``` **`command -v` alone is not enough** (LOW, #584): a Windows host commonly ships a `python3` *App Execution Alias* stub at `%LOCALAPPDATA%\Microsoft\WindowsApps\python3` that satisfies `command -v python3` with no Python actually installed — running it opens the Microsoft Store and exits non-zero. Only actually RUNNING it (`python3 --version`) tells the truth; `command -v` merely tells you a name resolves on `PATH`. `python3` wins whenever both it and `python` are genuinely present — the resolve-once order above tries it first and only falls back to `python` when it is absent or the stub — so a host with both interpreters gets the one this convention exists to prefer, not an arbitrary pick. Every helper invocation below is then spelled `"$PY" "{{PLUGIN_ROOT}}/hooks/