Last active
July 18, 2026 06:17
-
-
Save cniska/e9713720c1cbd82456e37e32be110a85 to your computer and use it in GitHub Desktop.
Git Worktree Script
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # wt — one task, one worktree, one tmux window, one agent. | |
| # | |
| # Convention: task worktrees live at <repo>/.claude/worktrees/<branch>, the same | |
| # path Claude Code's EnterWorktree and a manual `git worktree add` use. Set | |
| # WT_AGENT to launch something other than `claude`. | |
| # | |
| # On fresh creation, wt runs an optional repo hook — scripts/worktree-setup.sh — | |
| # so a new worktree arrives with dependencies installed. The hook is opt-in per | |
| # repo (skipped if absent) and non-fatal (a failed install still leaves a usable | |
| # worktree). Reusing an existing worktree skips the hook. | |
| set -euo pipefail | |
| agent=${WT_AGENT:-claude} | |
| die() { printf 'wt: %s\n' "$*" >&2; exit 1; } | |
| usage() { | |
| cat <<'EOF' | |
| wt — parallel-task worktrees for the orchestrator + tmux loop. | |
| Usage: | |
| wt <branch> create (or reuse) a worktree + tmux window, launch the agent | |
| wt ls list this repo's task worktrees | |
| wt path <branch> print the worktree path (for `cd "$(wt path x)"`) | |
| wt rm <branch> remove the worktree and kill its tmux window | |
| wt prune prune stale worktree admin entries | |
| Worktrees live at <repo>/.claude/worktrees/<branch>. On creation, wt runs the | |
| repo's scripts/worktree-setup.sh if present (dependency install, etc.). Override | |
| the agent with WT_AGENT (e.g. WT_AGENT=acolyte wt task-a). | |
| EOF | |
| } | |
| # Main working tree, even when invoked from inside a worktree. --git-common-dir | |
| # resolves to the shared .git; its parent is the primary checkout. | |
| repo_root() { | |
| local common | |
| common=$(git rev-parse --path-format=absolute --git-common-dir 2>/dev/null) \ | |
| || die "not inside a git repository" | |
| dirname "$common" | |
| } | |
| # Run the repo's worktree bootstrap so a fresh checkout is ready for the agent. | |
| # Opt-in (only if the tracked hook exists) and non-fatal (a failed install still | |
| # leaves a worktree to debug in), so wt stays generic across repos. | |
| run_bootstrap() { | |
| local path=$1 hook="$path/scripts/worktree-setup.sh" | |
| [ -x "$hook" ] || return 0 | |
| echo "wt: bootstrapping worktree via scripts/worktree-setup.sh" | |
| local rc=0 | |
| ( cd "$path" && "$hook" ) || rc=$? | |
| if [ "$rc" -eq 0 ]; then | |
| echo "wt: bootstrap complete" | |
| else | |
| echo "wt: bootstrap failed (exit $rc) — worktree created; fix and re-run the hook" >&2 | |
| fi | |
| } | |
| cmd_open() { | |
| local branch=$1 | |
| [ -n "$branch" ] || die "branch name required" | |
| local root path created=false | |
| root=$(repo_root) | |
| path="$root/.claude/worktrees/$branch" | |
| if [ -d "$path" ]; then | |
| echo "wt: reusing existing worktree $path" | |
| elif git -C "$root" show-ref --verify --quiet "refs/heads/$branch"; then | |
| git -C "$root" worktree add "$path" "$branch" | |
| created=true | |
| else | |
| git -C "$root" worktree add "$path" -b "$branch" | |
| created=true | |
| fi | |
| [ "$created" = true ] && run_bootstrap "$path" | |
| if [ -z "${TMUX:-}" ]; then | |
| echo "wt: not in a tmux session — worktree ready at:" | |
| echo " $path" | |
| echo " cd into it and run '$agent', or start tmux first." | |
| return | |
| fi | |
| # Reuse the window if it already exists, else open one and launch the agent. | |
| if tmux list-windows -F '#W' | grep -qxF "$branch"; then | |
| tmux select-window -t "$branch" | |
| else | |
| tmux new-window -c "$path" -n "$branch" | |
| tmux send-keys -t "$branch" "$agent" C-m | |
| fi | |
| } | |
| cmd_ls() { | |
| local root path | |
| root=$(repo_root) | |
| path="$root/.claude/worktrees" | |
| [ -d "$path" ] || { echo "wt: no task worktrees"; return; } | |
| git -C "$root" worktree list --porcelain \ | |
| | awk -v p="$path/" ' | |
| /^worktree / { wt = substr($0, 10) } | |
| /^branch / { br = substr($0, 8); sub("refs/heads/", "", br) | |
| if (index(wt, p) == 1) printf " %-24s %s\n", br, wt }' | |
| } | |
| cmd_path() { | |
| local branch=$1 | |
| [ -n "$branch" ] || die "branch name required" | |
| echo "$(repo_root)/.claude/worktrees/$branch" | |
| } | |
| cmd_rm() { | |
| local branch=$1 | |
| [ -n "$branch" ] || die "branch name required" | |
| local root path | |
| root=$(repo_root) | |
| path="$root/.claude/worktrees/$branch" | |
| [ -d "$path" ] || die "no worktree at $path" | |
| git -C "$root" worktree remove "$path" | |
| echo "wt: removed worktree $path" | |
| if [ -n "${TMUX:-}" ] && tmux list-windows -F '#W' | grep -qxF "$branch"; then | |
| tmux kill-window -t "$branch" | |
| echo "wt: killed tmux window $branch" | |
| fi | |
| echo "wt: branch '$branch' kept — delete with 'git -C $root branch -d $branch' once merged" | |
| } | |
| cmd_prune() { | |
| git -C "$(repo_root)" worktree prune | |
| echo "wt: pruned stale worktree entries" | |
| } | |
| case "${1:-}" in | |
| "" | -h | --help | help) usage ;; | |
| ls) cmd_ls ;; | |
| path) shift; cmd_path "${1:-}" ;; | |
| rm) shift; cmd_rm "${1:-}" ;; | |
| prune) cmd_prune ;; | |
| *) cmd_open "$1" ;; | |
| esac |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # Behavior tests for wt. Pure bash plus git; every case uses a throwaway repo. | |
| # Run: /usr/local/bin/wt.test.sh (exit 0 = all pass, 1 = failures) | |
| set -u | |
| HERE="$(cd "$(dirname "$0")" && pwd)" | |
| WT="$HERE/wt" | |
| pass=0; fail=0 | |
| assert(){ # desc, got, want | |
| if [ "$2" = "$3" ]; then pass=$((pass+1)) | |
| else fail=$((fail+1)); printf 'FAIL %s\n want: [%s]\n got: [%s]\n' "$1" "$3" "$2"; fi | |
| } | |
| contains(){ # desc, haystack, needle | |
| case "$2" in *"$3"*) pass=$((pass+1)) ;; | |
| *) fail=$((fail+1)); printf 'FAIL %s\n expected to contain: [%s]\n in: [%s]\n' "$1" "$3" "$2" ;; esac | |
| } | |
| TMP="$(cd "$(mktemp -d)" && pwd -P)"; REPO="$TMP/repo" | |
| trap 'rm -rf "$TMP"' EXIT | |
| git init -q -b main "$REPO" | |
| ( cd "$REPO" | |
| git config user.email test@example.com | |
| git config user.name test | |
| printf initial > README.md | |
| git add README.md | |
| git commit -qm initial ) | |
| run(){ ( cd "$REPO" && env -u TMUX WT_AGENT=true "$WT" "$@" ); } | |
| # path and ls: no worktree yet | |
| assert "path prints the conventional location" \ | |
| "$(run path task-a)" "$REPO/.claude/worktrees/task-a" | |
| assert "ls reports no task worktrees before creation" "$(run ls)" "wt: no task worktrees" | |
| # open: creates a branch/worktree, runs the optional hook, and does not require tmux. | |
| mkdir -p "$REPO/scripts" | |
| printf '%s\n' '#!/usr/bin/env bash' 'printf ready > .wt-bootstrap-ran' > "$REPO/scripts/worktree-setup.sh" | |
| chmod +x "$REPO/scripts/worktree-setup.sh" | |
| ( cd "$REPO" && git add scripts/worktree-setup.sh && git commit -qm bootstrap ) | |
| opened=$(run task-a) | |
| contains "open reports bootstrap" "$opened" "wt: bootstrapping worktree via scripts/worktree-setup.sh" | |
| contains "open reports a non-tmux checkout" "$opened" "wt: not in a tmux session" | |
| assert "open creates the worktree" "$([ -d "$REPO/.claude/worktrees/task-a" ] && echo yes || echo no)" yes | |
| assert "open runs the bootstrap hook only on creation" "$(cat "$REPO/.claude/worktrees/task-a/.wt-bootstrap-ran")" ready | |
| assert "open creates the requested branch" "$(git -C "$REPO" branch --show-current; git -C "$REPO/.claude/worktrees/task-a" branch --show-current | tail -1)" $'main\ntask-a' | |
| # reuse: must preserve the worktree and skip bootstrap. | |
| rm "$REPO/.claude/worktrees/task-a/.wt-bootstrap-ran" | |
| reused=$(run task-a) | |
| contains "existing worktree is reused" "$reused" "wt: reusing existing worktree" | |
| assert "reuse skips bootstrap" "$([ -e "$REPO/.claude/worktrees/task-a/.wt-bootstrap-ran" ] && echo yes || echo no)" no | |
| # ls and rm: only managed worktrees appear, rm keeps the branch for post-merge cleanup. | |
| contains "ls includes the managed branch" "$(run ls)" "task-a" | |
| contains "ls includes the managed path" "$(run ls)" "$REPO/.claude/worktrees/task-a" | |
| removed=$(run rm task-a) | |
| contains "rm reports removal" "$removed" "wt: removed worktree $REPO/.claude/worktrees/task-a" | |
| assert "rm removes the worktree" "$([ -d "$REPO/.claude/worktrees/task-a" ] && echo yes || echo no)" no | |
| assert "rm keeps the branch" "$(git -C "$REPO" show-ref --verify --quiet refs/heads/task-a; echo $?)" 0 | |
| # errors: missing arguments and calls outside a repository stay actionable. | |
| set +e | |
| missing=$(run path 2>&1); missing_rc=$? | |
| outside=$(cd "$TMP" && "$WT" ls 2>&1); outside_rc=$? | |
| set -e | |
| assert "path without a branch exits non-zero" "$missing_rc" 1 | |
| assert "path without a branch explains the problem" "$missing" "wt: branch name required" | |
| assert "outside a repo exits non-zero" "$outside_rc" 1 | |
| assert "outside a repo explains the problem" "$outside" "wt: not inside a git repository" | |
| total=$((pass + fail)) | |
| printf '\n%d/%d passed' "$pass" "$total" | |
| [ "$fail" -eq 0 ] && { printf ' — all green\n'; exit 0; } || { printf ' — %d FAILED\n' "$fail"; exit 1; } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment