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.