diff --git a/.agents/skills/git-branch/SKILL.md b/.agents/skills/git-branch/SKILL.md index 85ab3e0..fa6fb7c 100644 --- a/.agents/skills/git-branch/SKILL.md +++ b/.agents/skills/git-branch/SKILL.md @@ -21,6 +21,52 @@ When an issue ID is provided, use one `gh issue view --json title,body,number` call for naming context. If it fails, continue only when the request is enough and report that the issue was not verified. +## Base + +This section is the single source of truth for base selection. Other skills +(`git-worktree`) reference it and copy the snippet below instead of restating a +second rule set. + +Resolve the base in exactly this order, validating every candidate with a ref +check and falling through to the next one when it does not resolve: + +1. the repository's documented base — the default branch the repository itself + states (for example in `CONTRIBUTING`, `README`, or `AGENTS`); skip this step + when nothing is documented; +2. `origin/HEAD` — the remote's default branch; +3. the branch the user explicitly asked for (`--base ` or a branch named + in the request); +4. `main` as the last resort. + +Never continue with a base that does not resolve. If no candidate resolves, list +the available remote branches, ask the user to pass `--base `, and exit +non-zero — do not silently fall back to a `main` that does not exist. + +```bash +ref_exists() { + git rev-parse --verify --quiet "origin/$1" >/dev/null 2>&1 \ + || git rev-parse --verify --quiet "refs/heads/$1" >/dev/null 2>&1 +} + +resolve_base() { + local documented="$1" user_base="$2" origin_head cand + # 1) documented base + if [ -n "$documented" ] && ref_exists "$documented"; then echo "$documented"; return 0; fi + # 2) origin/HEAD + origin_head=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##') + [ -n "$origin_head" ] || origin_head=$(git remote show origin 2>/dev/null | sed -n 's/^ *HEAD branch: *//p') + # 3) user choice 4) main fallback + for cand in "$origin_head" "$user_base" main; do + if [ -n "$cand" ] && ref_exists "$cand"; then echo "$cand"; return 0; fi + done + return 1 +} +``` + +The order is deliberate: `origin/HEAD` is checked before the user choice, so an +explicit `--base` does not override a resolvable `origin/HEAD`. Surface that +conflict to the user instead of silently reordering the rule. + ## Create Check the current worktree, branch, and target branch first. Stop if the @@ -31,17 +77,31 @@ branch is unsafe. Validate the name with: git check-ref-format --branch ``` -Use the repository's documented base, otherwise `main`. In the same repository -prefer `origin/` over `upstream/`; use `upstream` only for a fork or -explicit guidance. Fetch or compare freshness only when it can change the -choice of base. If creating from a stale local base, confirm before updating or -use a freshly fetched remote base and report that the local base was unchanged. +Resolve the base with the snippet from the Base section, then prefer the remote +ref and use the local ref only when the remote one is missing: ```bash -git switch -c -git switch --no-track -c origin/ +base=$(resolve_base "$documented_base" "$user_base") || { + echo "No usable base branch. Candidates:" >&2 + git branch -r >&2 + echo "Pass --base to choose one." >&2 + exit 1 +} +if git rev-parse --verify --quiet "origin/$base" >/dev/null; then + base_ref="origin/$base" +else + base_ref="$base" +fi + +git switch --no-track -c "$base_ref" ``` +In the same repository prefer `origin/` over `upstream/`; use +`upstream` only for a fork or explicit guidance. Fetch or compare freshness only +when it can change the choice of base. If creating from a stale local base, +confirm before updating or use a freshly fetched remote base and report that the +local base was unchanged. + Verify the resulting branch. Do not switch to an existing branch, overwrite, reset, stash, delete, or force anything without explicit user intent. Do not create protected/shared base branches. diff --git a/.agents/skills/git-worktree/SKILL.md b/.agents/skills/git-worktree/SKILL.md index 5f7b113..8da9036 100644 --- a/.agents/skills/git-worktree/SKILL.md +++ b/.agents/skills/git-worktree/SKILL.md @@ -9,35 +9,127 @@ Use this when work should not disturb the current worktree. The new worktree does not copy uncommitted changes, and it does not change the user's shell directory. -## Naming and base +## Repository root + +Resolve the repository root first and build every path from it. Never rely on +the caller's working directory: in a monorepo this skill is invoked from +arbitrary subdirectories, and a relative path would nest `.worktrees/` under the +subdirectory instead of the repository root. + +```bash +repo_root=$(git rev-parse --show-toplevel) || { + echo "Not inside a Git repository; run this from a repository checkout." >&2 + exit 1 +} +target="$repo_root/.worktrees/" +``` + +Stop with a non-zero exit when `git rev-parse --show-toplevel` fails — do not +invent a relative path as a fallback. + +## Naming Use the `git-branch` naming rules: issue work is `/-`, other work uses a user-provided name, and no issue ID is invented. When an issue ID is provided, use one `gh issue view` call for naming context and continue from the request if it fails. Store the -worktree at `.worktrees/` so branch path segments are preserved. -The repository should ignore `.worktrees/`. +worktree at `"$repo_root/.worktrees/"` so branch path segments are +preserved and a subdirectory call still lands under the repository root. The +repository should ignore `.worktrees/`. + +## Base + +Base selection has exactly one source of truth: the `## Base` section of the +`git-branch` skill. Do not restate or re-order it here — call `resolve_base` +from that section and use the value it returns. For reference, the order is +documented base → `origin/HEAD` → user choice → `main` fallback, and every +candidate is validated with a ref check before it is used. + +Copy both helpers from `git-branch` verbatim (keep them identical when that +section changes): + +```bash +ref_exists() { + git rev-parse --verify --quiet "origin/$1" >/dev/null 2>&1 \ + || git rev-parse --verify --quiet "refs/heads/$1" >/dev/null 2>&1 +} + +resolve_base() { + local documented="$1" user_base="$2" origin_head cand + # 1) documented base + if [ -n "$documented" ] && ref_exists "$documented"; then echo "$documented"; return 0; fi + # 2) origin/HEAD + origin_head=$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##') + [ -n "$origin_head" ] || origin_head=$(git remote show origin 2>/dev/null | sed -n 's/^ *HEAD branch: *//p') + # 3) user choice 4) main fallback + for cand in "$origin_head" "$user_base" main; do + if [ -n "$cand" ] && ref_exists "$cand"; then echo "$cand"; return 0; fi + done + return 1 +} +``` -Default the base to `main`; same-repository work prefers `origin/`, then -local ``, and uses `upstream` only for fork workflows or explicit -guidance. Fetch/check freshness only when it affects the result. If the local -base is stale, confirm before using it or create from a fetched remote base and -report that the local base was unchanged. +Create from the remote ref first and fall back to the local ref only when the +remote one is missing: + +```bash +base=$(resolve_base "$documented_base" "$user_base") || { + echo "No usable base branch. Candidates:" >&2 + git branch -r >&2 + echo "Pass --base to choose one." >&2 + exit 1 +} +if git rev-parse --verify --quiet "origin/$base" >/dev/null; then + base_ref="origin/$base" +else + base_ref="$base" +fi +``` + +Same-repository work prefers `origin/`; use `upstream` only for fork +workflows or explicit guidance. Fetch or compare freshness only when it can +change the choice of base. If the local base is stale, confirm before using it +or create from a fetched remote base and report that the local base was +unchanged. Never continue with a base that does not resolve, and never fall back +to a `main` branch that does not exist. ## Create Before creating, check current status, existing worktrees/branches, and the -target path. Stop if any target already exists or dirty-work intent is unclear. -Validate the branch with `git check-ref-format --branch `, create -parent directories as needed, then run the equivalent of: +target path. Stop if the target already exists: never overwrite, reset, or +reuse an existing worktree directory. Validate the branch with +`git check-ref-format --branch `, resolve the base as shown above, +then run the equivalent of: + +```bash +if [ -e "$target" ]; then + echo "Worktree target already exists: $target" >&2 + echo "Pick another branch name, or remove the existing worktree yourself; this skill never overwrites." >&2 + exit 1 +fi + +if ! git check-ignore -q "$target"; then + echo "warning: $target is not covered by .gitignore" >&2 + echo "Add '.worktrees/' to the repository root .gitignore before creating the worktree." >&2 +fi + +mkdir -p "$(dirname "$target")" +git worktree add --no-track -b "$target" "$base_ref" +``` + +The ignore check only reports: this skill does not edit `.gitignore` itself. + +## Report + +Verify the new worktree's branch, status, and `pwd`. Report its absolute path, +the base, whether dirty changes were excluded, and the user's next step: ```bash -git worktree add --no-track -b .worktrees/ +cd "$repo_root/.worktrees/" ``` -Verify the new worktree's branch, status, and `pwd`. Report its path, base, -whether dirty changes were excluded, and the user's next step: -`cd .worktrees/`. +The worktree always lives at the repository root, no matter which subdirectory +of the repository the skill was invoked from. Do not remove, prune, overwrite, reset, stash, push, or force anything unless explicitly asked. Do not create protected/shared base branches as targets.