From a765f1c302b91c66f33dcc6893f856364ebc4f60 Mon Sep 17 00:00:00 2001 From: Jay Hesselberth Date: Wed, 26 Aug 2026 06:14:34 -0600 Subject: [PATCH] feat(claude): ship a git-workflow skill alongside bodhi-compute MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Git work happens in sinteractive sessions, and an agent starting one has no standing guidance about how it should be done — so it re-derives the conventions every time, or guesses. bodhi-compute already solved the same problem for cluster etiquette; this rides the rails it established and reaches every session on every node with no per-session setup. The skill is deliberately general: semantic versioning with annotated vX.Y.Z tags, Conventional Commit messages, one worktree per branch under .claude/worktrees/, landing through a pull request rather than committing to main, and running the repo's own CI gates before pushing. It names no project and defers to a repository that documents something stricter of its own, so it is useful in whatever checkout the session happens to be sitting in. Adding it meant the installer could no longer name what it ships. --install-claude now copies every skills/*/SKILL.md found beside the script, and the Makefile installs the whole tree from a $(SKILLS) wildcard rather than one hardcoded path per target. skills/bodhi-compute appeared in six places across the script, install-user, install-system and both branches of the nodes fan-out; any one of them missed on the next skill would have shipped a partial set to the compute nodes, where the gap is invisible until someone runs --install-claude from inside a session and gets the older half. find_claude_assets still probes for skills/bodhi-compute rather than skills/, so an empty skills/ from a half-finished install does not pass for a complete one. Co-Authored-By: Claude Opus 5 (1M context) --- .gitignore | 1 + CHANGELOG.md | 29 ++++++++ Makefile | 37 ++++++---- README.md | 12 ++-- docs/scripting.md | 26 +++++-- docs/usage.md | 2 +- man/sinteractive.1 | 17 +++-- sinteractive | 32 ++++++--- skills/git-workflow/SKILL.md | 132 +++++++++++++++++++++++++++++++++++ 9 files changed, 247 insertions(+), 41 deletions(-) create mode 100644 skills/git-workflow/SKILL.md diff --git a/.gitignore b/.gitignore index 2ecbd34..41c3f40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ site/ .pixi/ +.claude/worktrees/ diff --git a/CHANGELOG.md b/CHANGELOG.md index fa0a2c2..331d173 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,35 @@ and this project adheres to ## [Unreleased] +### Added + +- A second Claude Code skill, `git-workflow`, installed alongside + `bodhi-compute` by `--install-claude`. Where `bodhi-compute` is about the + cluster, this one is about the repository open in the session: semantic + versioning with annotated `vX.Y.Z` tags, Conventional Commit messages, one + worktree per branch under `.claude/worktrees/`, landing work through a pull + request rather than committing to `main`, and running the repo's own CI + gates before pushing. It is deliberately general — it names no project, and + defers to a repository that documents something stricter of its own. + + Sessions are where this work actually happens, and an agent that starts with + no standing guidance re-derives the conventions every time, or guesses. The + skill rides the rails `bodhi-compute` already established, so it reaches + every session on every node with no per-session setup. + +### Changed + +- The installer no longer names the skills it ships. `--install-claude` copies + every `skills/*/SKILL.md` found beside the script, and `make install`, + `make install-system` and `make nodes` install the whole `skills/` tree + rather than one path each. Adding a skill is now dropping a directory into + `skills/`, with no install target, fan-out recipe, or copy loop to update in + step — the previous shape hardcoded `skills/bodhi-compute` in six places, + and any one of them missed would have shipped a partial set to the nodes. + + `make nodes-check` reports assets present based on the `skills/` directory + rather than `skills/bodhi-compute`, so it stays true as the set grows. + ## [0.3.0] - 2026-08-26 ### Changed diff --git a/Makefile b/Makefile index 05b56e6..35f9ef9 100644 --- a/Makefile +++ b/Makefile @@ -5,12 +5,18 @@ MANDIR ?= ~/.local/share/man/man1 # $XDG_DATA_DIRS (which includes /usr/local/share) for # bash-completion/completions/. COMPDIR ?= ~/.local/share/bash-completion/completions -# Claude Code assets (skill + hooks) ship beside the script so that +# Claude Code assets (skills + hooks) ship beside the script so that # `sinteractive --install-claude` works from an installed copy and not only # from a git checkout. Mirrors the repo layout, so the lookup in the script # has one code path for both. SHAREDIR ?= ~/.local/share/sinteractive +# Every skill under skills/ ships, discovered rather than listed: adding one +# is a matter of dropping a directory in, with no install target to update. +# The paths are relative and mirror the repo layout, so `install -D` into a +# destination root reproduces skills//SKILL.md underneath it. +SKILLS := $(wildcard skills/*/SKILL.md) + # When run as root, install system-wide: sinteractive to /usr/local/bin and # its man page to /usr/local/share/man. UID := $(shell id -u) @@ -31,26 +37,29 @@ install-user: cp man/sinteractive.1 $(MANDIR)/sinteractive.1 mkdir -p $(COMPDIR) cp completions/sinteractive.bash $(COMPDIR)/sinteractive - mkdir -p $(SHAREDIR)/claude/hooks $(SHAREDIR)/skills/bodhi-compute + mkdir -p $(SHAREDIR)/claude/hooks install -m 0755 claude/hooks/*.sh $(SHAREDIR)/claude/hooks/ install -m 0644 claude/settings-snippet.json $(SHAREDIR)/claude/ - install -m 0644 skills/bodhi-compute/SKILL.md $(SHAREDIR)/skills/bodhi-compute/ + for s in $(SKILLS); do install -D -m 0644 $$s $(SHAREDIR)/$$s; done install-system: install -m 0755 sinteractive /usr/local/bin/sinteractive install -D -m 0644 man/sinteractive.1 /usr/local/share/man/man1/sinteractive.1 install -D -m 0644 completions/sinteractive.bash /usr/local/share/bash-completion/completions/sinteractive - install -d -m 0755 /usr/local/share/sinteractive/claude/hooks /usr/local/share/sinteractive/skills/bodhi-compute + install -d -m 0755 /usr/local/share/sinteractive/claude/hooks install -m 0755 claude/hooks/*.sh /usr/local/share/sinteractive/claude/hooks/ install -m 0644 claude/settings-snippet.json /usr/local/share/sinteractive/claude/ - install -m 0644 skills/bodhi-compute/SKILL.md /usr/local/share/sinteractive/skills/bodhi-compute/ + for s in $(SKILLS); do install -D -m 0644 $$s /usr/local/share/sinteractive/$$s; done # --------------------------------------------------------------------------- # Claude Code integration. Two parts: # -# - the bodhi-compute skill, which teaches an agent cluster etiquette: the -# login node and an sinteractive session are both orchestration shells, -# and real work goes into its own allocation; +# - skills, which teach an agent how work is done here. bodhi-compute +# covers cluster etiquette: the login node and an sinteractive session are +# both orchestration shells, and real work goes into its own allocation. +# git-workflow covers the git conventions — semantic versioning, +# Conventional Commits, a worktree per branch, landing through a pull +# request — and is about the repository in the session, not the cluster; # - two hooks for an agent running INSIDE a session, which tell it at # startup where it is and how big the allocation is, and warn it when the # session is running out of walltime. @@ -170,26 +179,26 @@ nodes: require-root && mv /usr/local/bin/sinteractive.new /usr/local/bin/sinteractive \ && install -D -m 0644 $(CURDIR)/man/sinteractive.1 /usr/local/share/man/man1/sinteractive.1 \ && install -D -m 0644 $(CURDIR)/completions/sinteractive.bash /usr/local/share/bash-completion/completions/sinteractive \ - && install -d -m 0755 /usr/local/share/sinteractive/claude/hooks /usr/local/share/sinteractive/skills/bodhi-compute \ + && install -d -m 0755 /usr/local/share/sinteractive/claude/hooks \ && install -m 0755 $(CURDIR)/claude/hooks/*.sh /usr/local/share/sinteractive/claude/hooks/ \ && install -m 0644 $(CURDIR)/claude/settings-snippet.json /usr/local/share/sinteractive/claude/ \ - && install -m 0644 $(CURDIR)/skills/bodhi-compute/SKILL.md /usr/local/share/sinteractive/skills/bodhi-compute/ \ + && for s in $(SKILLS); do install -D -m 0644 $(CURDIR)/$$s /usr/local/share/sinteractive/$$s || exit 1; done \ && echo ok'; \ else \ for n in $(NODES); do \ printf '==> %s: ' "$$n"; \ tar cf - sinteractive man/sinteractive.1 completions/sinteractive.bash \ - claude/hooks claude/settings-snippet.json skills/bodhi-compute/SKILL.md \ + claude/hooks claude/settings-snippet.json $(SKILLS) \ | ssh $(SSH_USER)@$$n \ 'set -e; d=$$(mktemp -d); trap "rm -rf $$d" EXIT; tar xf - -C "$$d"; \ install -m 0755 "$$d/sinteractive" /usr/local/bin/sinteractive.new; \ mv /usr/local/bin/sinteractive.new /usr/local/bin/sinteractive; \ install -D -m 0644 "$$d/man/sinteractive.1" /usr/local/share/man/man1/sinteractive.1; \ install -D -m 0644 "$$d/completions/sinteractive.bash" /usr/local/share/bash-completion/completions/sinteractive; \ - install -d -m 0755 /usr/local/share/sinteractive/claude/hooks /usr/local/share/sinteractive/skills/bodhi-compute; \ + install -d -m 0755 /usr/local/share/sinteractive/claude/hooks; \ install -m 0755 "$$d"/claude/hooks/*.sh /usr/local/share/sinteractive/claude/hooks/; \ install -m 0644 "$$d/claude/settings-snippet.json" /usr/local/share/sinteractive/claude/; \ - install -m 0644 "$$d/skills/bodhi-compute/SKILL.md" /usr/local/share/sinteractive/skills/bodhi-compute/; \ + cd "$$d" && for s in skills/*/SKILL.md; do install -D -m 0644 "$$s" "/usr/local/share/sinteractive/$$s"; done; \ echo ok' \ || echo "FAILED"; \ done; \ @@ -211,7 +220,7 @@ nodes-check: v=$$(sed -n "s/^VERSION=.\(.*\)./\1/p" /usr/local/bin/sinteractive 2>/dev/null | head -1); \ [ -e /usr/local/bin/sinteractive ] || v=missing; \ [ -n "$$v" ] || v=unknown; \ - a=no; [ -d /usr/local/share/sinteractive/skills/bodhi-compute ] && a=yes; \ + a=no; [ -d /usr/local/share/sinteractive/skills ] && a=yes; \ t=$$(/usr/local/bin/tmux -V 2>/dev/null) || t="tmux missing"; \ echo "sinteractive=$$v assets=$$a $$t"' 2>/dev/null \ || echo "unreachable"; \ diff --git a/README.md b/README.md index c590765..f2c08f7 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ sinteractive [OPTIONS] [SBATCH_ARGS...] | `--ensure NAME` | Reuse the session named NAME, or launch it if absent (implies `--detach`) | | | `--cancel TARGET` | Cancel a session by JOBID or NAME | | | `--agent-context` | Brief a coding agent on the session it is running inside | | -| `--install-claude` | Install the Claude Code skill and hooks, and register them | | +| `--install-claude` | Install the Claude Code skills and hooks, and register them | | | `-l`, `--list` | List running sinteractive sessions | | | `-h`, `--help` | Show help message | | @@ -279,10 +279,12 @@ and new panes, but shells already running keep their original `SINTERACTIVE_NAME`. > [!TIP] -> This repo ships a [Claude Code skill](https://code.claude.com/docs/en/skills) -> plus two hooks, for agents that run **inside** a session. The skill teaches -> cluster etiquette; the hooks brief the agent on which session it is in at -> startup, and warn it when the session is running out of wall time. +> This repo ships two [Claude Code skills](https://code.claude.com/docs/en/skills) +> plus two hooks, for agents that run **inside** a session. `bodhi-compute` +> teaches cluster etiquette; `git-workflow` teaches the git conventions — +> semantic versioning, Conventional Commits, a worktree per branch, landing +> through a pull request. The hooks brief the agent on which session it is in +> at startup, and warn it when the session is running out of wall time. > > ```bash > sinteractive --install-claude # from any installed copy diff --git a/docs/scripting.md b/docs/scripting.md index 6dd1d95..3e49c8c 100644 --- a/docs/scripting.md +++ b/docs/scripting.md @@ -107,15 +107,18 @@ and new panes, but shells already running keep their original ## Claude Code integration -Install the skill and hooks: +Install the skills and hooks: ```bash sinteractive --install-claude # from any installed copy make claude-install # equivalent, from a checkout ``` -Both write `~/.claude/skills/bodhi-compute` and `~/.claude/hooks/`, then -register the hooks in `~/.claude/settings.json`. That file is yours and +Both write every skill under `~/.claude/skills/` and the hooks to +`~/.claude/hooks/`, then register the hooks in `~/.claude/settings.json`. +Skills are discovered from what ships beside the script rather than named in +the installer, so a new one arrives with an upgrade and needs no new flag. +That settings file is yours and usually already has hooks in it, so the merge is done by `jq` and only by `jq` — string surgery on it in bash could silently disable every setting in the file. What the merge guarantees: @@ -145,10 +148,19 @@ need them too, since running `--install-claude` from inside a session runs the node's copy of the script. Point `SINTERACTIVE_SHARE` at a checkout to override, and `make nodes-check` to see which nodes actually have them. -**The [skill](https://code.claude.com/docs/en/skills)** teaches agents cluster -etiquette: neither the login node nor an sinteractive session is a compute -target, real work goes into an allocation sized for it, reuse sessions rather -than piling them up, check the time budget before long jobs, and clean up. +**Two [skills](https://code.claude.com/docs/en/skills)** teach agents how work +is done here. + +`bodhi-compute` covers cluster etiquette: neither the login node nor an +sinteractive session is a compute target, real work goes into an allocation +sized for it, reuse sessions rather than piling them up, check the time budget +before long jobs, and clean up. + +`git-workflow` covers the git conventions, and is about the repository open in +the session rather than the cluster: semantic versioning with annotated +`vX.Y.Z` tags, Conventional Commit messages, one worktree per branch under +`.claude/worktrees/`, landing work through a pull request rather than +committing to `main`, and running the repo's own CI gates before pushing. **`sinteractive --agent-context`** prints a briefing on the current session — job, node, partition, allocation size, walltime remaining, and the rules diff --git a/docs/usage.md b/docs/usage.md index d3362c7..22f8fd3 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -23,7 +23,7 @@ sinteractive [OPTIONS] [SBATCH_ARGS...] | `--ensure NAME` | Reuse the session named NAME, or launch it if absent (implies `--detach`) | | | `--cancel TARGET` | Cancel a session by JOBID or NAME | | | `--agent-context` | Brief a coding agent on the session it is running inside | | -| `--install-claude` | Install the Claude Code skill and hooks, and register them | | +| `--install-claude` | Install the Claude Code skills and hooks, and register them | | | `-l`, `--list` | List running sinteractive sessions | | | `-h`, `--help` | Show help message | | | `-V`, `--version` | Show version | | diff --git a/man/sinteractive.1 b/man/sinteractive.1 index 45bc294..52dc870 100644 --- a/man/sinteractive.1 +++ b/man/sinteractive.1 @@ -260,7 +260,7 @@ hook (see but useful by hand to see exactly what an agent was told. .TP .B \-\-install\-claude -Install the Claude Code skill and hooks into +Install the Claude Code skills and hooks into .I ~/.claude (or .BR $CLAUDE_CONFIG_DIR ), @@ -486,9 +486,14 @@ run from inside one create their own allocations rather than steps of the session's job. .SS Claude Code hooks .B \-\-agent\-context -prints this briefing for the current session. A skill and two hooks wire it -up \(em one briefing the agent at session start, one warning it when the -session is running out of wall time \(em installed with +prints this briefing for the current session. Two skills and two hooks wire +it up \(em +.I bodhi\-compute +for cluster etiquette and +.I git\-workflow +for the git conventions, plus one hook briefing the agent at session start +and one warning it when the session is running out of wall time \(em +installed with .B sinteractive \-\-install\-claude from any installed copy, or .B make claude\-install @@ -619,8 +624,8 @@ status-bar countdown keeps running from the last known end time. .I ~/.claude Written by .BR \-\-install\-claude : -the skill under -.IR skills/bodhi\-compute/ , +the skills under +.IR skills/ , the hooks under .IR hooks/ , and their registration merged into diff --git a/sinteractive b/sinteractive index 215b44e..7f9e2ad 100755 --- a/sinteractive +++ b/sinteractive @@ -93,7 +93,7 @@ Options: --cancel TARGET Cancel a session by JOBID or NAME --refresh [TARGET] Re-check the time budget now (after scontrol) --agent-context Brief a coding agent on the session it is running in - --install-claude Install the Claude Code skill and hooks, and register them + --install-claude Install the Claude Code skills and hooks, and register them -l, --list List your running sessions -h, --help Show this help message -V, --version Show version @@ -1131,7 +1131,7 @@ function pane_border_format_string { # with its own start. ASCII only: the slice below is bash substring # expansion, which counts bytes under a C locale, and a multibyte character # cut in half renders as garbage. - local msg='Claude Code: run sinteractive --install-claude to enable the skill and hooks | ' + local msg='Claude Code: run sinteractive --install-claude to enable the skills and hooks | ' ((offset %= ${#msg})) local window="${msg}${msg}" printf '%s' "#[align=centre]#[fg=yellow,bold] ${window:offset:HINT_WIDTH} #[default]" @@ -1172,6 +1172,9 @@ function find_claude_assets { local root for root in "${candidates[@]}"; do + # bodhi-compute is the probe rather than skills/ itself: an empty skills/ + # directory left behind by a half-finished install would otherwise pass, + # and this one has shipped since the assets existed. if [[ -d "${root}/claude/hooks" && -d "${root}/skills/bodhi-compute" ]]; then printf '%s' "$root" return 0 @@ -1289,7 +1292,7 @@ function register_claude_hooks { return 0 } -# Install the Claude Code skill and hooks into the user's ~/.claude, from +# Install the Claude Code skills and hooks into the user's ~/.claude, from # wherever this copy of sinteractive keeps its assets. Skills and hooks are # per-user, so this is the same work whoever runs it. # @@ -1312,17 +1315,30 @@ function install_claude { fi local claude_dir="${CLAUDE_CONFIG_DIR:-${HOME}/.claude}" - mkdir -p "${claude_dir}/skills/bodhi-compute" "${claude_dir}/hooks" || exit 1 - - cp "${assets}/skills/bodhi-compute/SKILL.md" \ - "${claude_dir}/skills/bodhi-compute/SKILL.md" || exit 1 + mkdir -p "${claude_dir}/hooks" || exit 1 + + # Every skill shipped beside the script, rather than a list to keep in sync + # here: adding one is then a matter of dropping a directory into skills/. + local skill name names=() + for skill in "${assets}"/skills/*/SKILL.md; do + [[ -e "$skill" ]] || continue + name=$(basename "$(dirname "$skill")") + mkdir -p "${claude_dir}/skills/${name}" || exit 1 + cp "$skill" "${claude_dir}/skills/${name}/SKILL.md" || exit 1 + names+=("$name") + done local hook for hook in "${assets}"/claude/hooks/*.sh; do [[ -e "$hook" ]] || continue install -m 0755 "$hook" "${claude_dir}/hooks/" || exit 1 done - echo "Installed the bodhi-compute skill and hooks into ${claude_dir}" + # Name what landed rather than counting it: the point of the line is to say + # which skills this copy of sinteractive carries, which is what differs + # between a current install and a stale one. + local list='' + ((${#names[@]})) && printf -v list ', %s' "${names[@]}" + echo "Installed the Claude Code hooks and skills (${list:2}) into ${claude_dir}" echo " from ${assets}" echo '' diff --git a/skills/git-workflow/SKILL.md b/skills/git-workflow/SKILL.md new file mode 100644 index 0000000..14c9087 --- /dev/null +++ b/skills/git-workflow/SKILL.md @@ -0,0 +1,132 @@ +--- +name: git-workflow +description: Git conventions for this user's repositories — semantic versioning, Conventional Commit messages, one worktree per branch, and landing work through pull requests. Use whenever a task involves branching, committing, opening or reviewing a pull request, cutting a release, tagging a version, or writing a changelog entry. +--- + +# Git workflow + +These are the user's standing preferences. They apply to whatever repository +is open, not to any one project. Where a repository documents something +stricter of its own — a `CONTRIBUTING.md`, a release checklist — that wins. + +## Branch in a worktree, never on `main` + +One worktree per line of work. The convention is +`/.claude/worktrees/`, which is what the `EnterWorktree` tool +creates; prefer it over `git worktree add` so the session's working directory +follows the worktree instead of being left behind in the main checkout. + +The base commit comes from the `worktree.baseRef` setting: `fresh` (the +default) branches from `origin/`, so the work starts from what +is actually on the remote rather than from whatever the local checkout has +drifted to; `head` branches from local `HEAD`, for work that genuinely builds +on uncommitted local history. + +`.claude/worktrees/` is a byproduct of the workflow, not source. If the +repository does not already ignore it, add it to `.gitignore` — or to +`.git/info/exclude` when the ignore file is shared and the convention is not. + +Leave with `ExitWorktree`: `keep` while the branch is still in flight, +`remove` once the pull request has merged. A worktree outliving its branch is +a checkout of something that no longer exists. + +Branch names are short kebab-case topics describing the change, reading much +the way the subject line does: `fix-stale-time-budget`, `ci-validate-shell`, +`install-claude-from-anywhere`. + +## Land through a pull request + +```bash +git push -u origin HEAD +gh pr create --fill # then edit the body to say why +gh pr checks --watch # let CI go green before merging +gh pr merge +``` + +Nothing goes onto `main` directly — not a typo fix, not a version bump, not a +one-line revert. The pull request is where CI runs and where the reasoning is +recorded; a change that skips it has neither, and the gap only surfaces later, +when somebody asks why a line is the way it is. + +Never force-push a branch someone else may have checked out, and never rewrite +history that is already on the remote. + +## Conventional Commits + +``` +type(scope): subject + +Body explaining why, wrapped at 72. + +BREAKING CHANGE: what callers must now do differently. +``` + +Types: `feat`, `fix`, `docs`, `chore`, `refactor`, `test`, `ci`, `perf`, +`build`, `revert`. The scope is optional and names the area touched +(`fix(nodes):`, `feat(claude):`); leave it off when the change is repo-wide. + +The subject is imperative, lowercase, no trailing period, and completes the +sentence "this commit will …". The body explains *why* — the failure mode +being fixed, the alternative that was rejected and what was wrong with it — +because the diff already says what changed and nothing else records the +reasoning. A mechanical change needs no body; a judgment call always does. + +Mark anything forcing a major version bump with either a `BREAKING CHANGE:` +footer or a `!` before the colon (`feat(api)!:`). + +Where a repository's recent history plainly follows a different convention, +match the repository rather than switching styles mid-log — and never rewrite +existing commits to conform. + +## Semantic versioning + +`MAJOR.MINOR.PATCH`: MAJOR when existing usage breaks, MINOR for +backwards-compatible additions, PATCH for fixes that change no interface. +Before 1.0.0 the guarantee shifts down a place — MINOR is where breaking +changes go, and users should expect them there. + +Tags are `v`-prefixed and **annotated**: + +```bash +git tag -a v1.4.0 -m 'Release v1.4.0' +``` + +A lightweight tag is a bare pointer with no tagger, date, or message, so a +release cut that way leaves no record of when it was made or by whom. +Annotate every one. + +## Releasing + +**Find every place the version is written before changing any of them.** It is +routinely more than one: a `VERSION=` in a script, `pyproject.toml`, +`package.json`, a `DESCRIPTION`, the `.TH` line of a man page, a docs config. +Grep for the current version string across the repository and bump the whole +set in one commit — a stale copy is invisible until a user reports that +`--version` disagrees with the tag. + +Then, for a repository keeping a changelog in Keep a Changelog form: rename +`## [Unreleased]` to `## [X.Y.Z] - YYYY-MM-DD`, open a fresh empty +`[Unreleased]` above it, and update the comparison links at the foot of the +file — `[Unreleased]` moves to `compare/vX.Y.Z...HEAD`, and a new `[X.Y.Z]` +link points at `compare/vPREV...vX.Y.Z`. + +```bash +git commit -m 'chore(release): v1.4.0' +git tag -a v1.4.0 -m 'Release v1.4.0' +git push --follow-tags +gh release create v1.4.0 --generate-notes +``` + +A release is still a pull request. Tag the merge commit on `main`, not the +branch. + +## Run the repository's own checks before pushing + +Read `.github/workflows/*.yml` and run what CI runs, locally, first. The gates +are usually a linter, a formatter check, and a test suite, and they take +seconds by hand; discovering them from a red pull request costs a round trip +and leaves a failed run in the history for nothing. + +When those checks are heavy enough to be real compute — a full test suite, a +build — they belong in their own Slurm allocation rather than in the session +shell. See the `bodhi-compute` skill.