|
#!/bin/sh |
|
# Worktree agents: POSIX sh git worktree launchers for Claude, Codex, Agent, Agy, and Pi. |
|
|
|
set -eu |
|
|
|
WTA_LAUNCHER= |
|
WTA_TOOL= |
|
WTA_ROOT_ENV= |
|
WTA_ROOT_DEFAULT= |
|
tool_n=0 |
|
|
|
_wta_die() { |
|
printf '%s\n' "${WTA_LAUNCHER}: $*" >&2 |
|
exit 1 |
|
} |
|
|
|
_wta_print() { |
|
printf '%s\n' "$*" |
|
} |
|
|
|
_wta_print_stderr() { |
|
printf '%s\n' "$*" >&2 |
|
} |
|
|
|
_wta_configure() { |
|
case "$1" in |
|
claude) |
|
WTA_LAUNCHER=wclaude |
|
WTA_TOOL=claude |
|
WTA_ROOT_ENV=WCLAUDE_WORKTREE_ROOT |
|
WTA_ROOT_DEFAULT=.claude/worktrees |
|
;; |
|
codex) |
|
WTA_LAUNCHER=wcodex |
|
WTA_TOOL=codex |
|
WTA_ROOT_ENV=WCODEX_WORKTREE_ROOT |
|
WTA_ROOT_DEFAULT=.codex/worktrees |
|
;; |
|
agent) |
|
WTA_LAUNCHER=wagent |
|
WTA_TOOL=agent |
|
WTA_ROOT_ENV=WAGENT_WORKTREE_ROOT |
|
WTA_ROOT_DEFAULT=.agent/worktrees |
|
;; |
|
agy) |
|
WTA_LAUNCHER=wagy |
|
WTA_TOOL=agy |
|
WTA_ROOT_ENV=WAGY_WORKTREE_ROOT |
|
WTA_ROOT_DEFAULT=.agy/worktrees |
|
;; |
|
pi) |
|
WTA_LAUNCHER=wpi |
|
WTA_TOOL=pi |
|
WTA_ROOT_ENV=WPI_WORKTREE_ROOT |
|
WTA_ROOT_DEFAULT=.pi/worktrees |
|
;; |
|
*) |
|
printf '%s\n' "worktree-agent: unknown tool: $1" >&2 |
|
return 1 |
|
;; |
|
esac |
|
} |
|
|
|
_wta_usage() { |
|
cat <<EOF |
|
usage: |
|
${WTA_LAUNCHER} [name] [${WTA_TOOL} args...] |
|
${WTA_LAUNCHER} [options] [--] [${WTA_TOOL} args...] |
|
|
|
Create or reuse a git worktree, cd into it, then launch ${WTA_TOOL} there. |
|
|
|
If name is omitted, ${WTA_LAUNCHER} generates a random worktree/branch name. |
|
If the first argument is ${WTA_TOOL} option, or if you use --, the name is omitted |
|
and all remaining arguments are forwarded to ${WTA_TOOL}. |
|
|
|
examples: |
|
${WTA_LAUNCHER} |
|
${WTA_LAUNCHER} fix-login |
|
${WTA_LAUNCHER} fix-login --model gpt-5.5 "investigate failing CI" |
|
${WTA_LAUNCHER} --safe fix-login |
|
${WTA_LAUNCHER} --branch feat/fix-login -- "investigate failing CI" |
|
|
|
options: |
|
-n, --name NAME Worktree directory name. Defaults to a random name. |
|
-b, --branch BRANCH Branch to create/reuse. Defaults to NAME. |
|
--base REF Base ref for new branches. Defaults to HEAD. |
|
--root DIR Worktree root. Defaults to \$${WTA_ROOT_ENV} or |
|
<repo>/${WTA_ROOT_DEFAULT}. |
|
--mode MODE Launch mode: dangerous, safe, or auto. |
|
--dangerous Launch with the default dangerous/yolo flags. |
|
--safe Launch without dangerous/yolo flags. |
|
--auto Launch with each tool's auto-approval mode where available. |
|
--no-launch Create/reuse the worktree and print the path, but do |
|
not launch ${WTA_TOOL}. |
|
-h, --help Show this help. |
|
|
|
By default, launches in yolo/dangerous mode when supported: |
|
claude -> --dangerously-skip-permissions |
|
codex -> --dangerously-bypass-approvals-and-sandbox |
|
agent -> --force --sandbox disabled |
|
agy -> --dangerously-skip-permissions |
|
pi -> --approve |
|
Use --safe or pass the tool's own safety flags before the prompt to opt out. |
|
EOF |
|
} |
|
|
|
_wta_random_suffix() { |
|
if command -v openssl >/dev/null 2>&1; then |
|
openssl rand -hex 3 |
|
elif command -v uuidgen >/dev/null 2>&1; then |
|
uuidgen | tr '[:upper:]' '[:lower:]' | tr -d '-' | cut -c1-6 |
|
else |
|
printf '%06x\n' $(( ($(date +%s) + $$) % 16777216 )) |
|
fi |
|
} |
|
|
|
_wta_pick_word() { |
|
words=$1 |
|
# shellcheck disable=SC2086 |
|
set -- $words |
|
count=$# |
|
[ "$count" -gt 0 ] || return 1 |
|
idx=$(( ( ($(date +%s) + $$) % count ) + 1 )) |
|
i=1 |
|
for word in "$@"; do |
|
if [ "$i" -eq "$idx" ]; then |
|
printf '%s' "$word" |
|
return 0 |
|
fi |
|
i=$((i + 1)) |
|
done |
|
} |
|
|
|
_wta_random_name() { |
|
adjectives="active bold crisp direct eager fresh grand hardy ideal just keen lucid mild neat open plain quick rapid sharp tight upright vivid warm xenial young zesty" |
|
nouns="arc beam cipher deck ember forge grid hub ion jet key loop matrix node orbit patch query relay signal trace unit vector wire xenon yield zone" |
|
|
|
adjective=$(_wta_pick_word "$adjectives") || return 1 |
|
noun=$(_wta_pick_word "$nouns") || return 1 |
|
suffix=$(_wta_random_suffix) || return 1 |
|
|
|
printf '%s' "${adjective}-${noun}-${suffix}" |
|
} |
|
|
|
_wta_path_name_for() { |
|
name=$1 |
|
name=$(printf '%s' "$name" | tr '[:upper:]' '[:lower:]') |
|
name=$(printf '%s' "$name" | sed 's/\//__/g; s/[^a-z0-9._-]/-/g; s/^[-.]*//; s/[-.]*$//') |
|
|
|
if [ -z "$name" ]; then |
|
name=$(_wta_random_name) || return 1 |
|
fi |
|
|
|
printf '%s' "$name" |
|
} |
|
|
|
_wta_is_probable_prompt() { |
|
_wta_tab=$(printf '\t') |
|
case "$1" in |
|
*" "*|*"$_wta_tab"*) return 0 ;; |
|
*) return 1 ;; |
|
esac |
|
} |
|
|
|
_wta_tool_push() { |
|
tool_n=$((tool_n + 1)) |
|
eval "tool_$tool_n=\$1" |
|
} |
|
|
|
_wta_tool_push_all() { |
|
for arg in "$@"; do |
|
_wta_tool_push "$arg" |
|
done |
|
} |
|
|
|
_wta_tool_option_requires_value() { |
|
case "$WTA_TOOL" in |
|
claude) |
|
case "$1" in |
|
--add-dir|\ |
|
--allowedTools|\ |
|
--append-system-prompt|\ |
|
--disallowedTools|\ |
|
--input-format|\ |
|
--mcp-config|\ |
|
--model|\ |
|
--output-format|\ |
|
--permission-mode|\ |
|
--permission-prompt-tool|\ |
|
--settings|\ |
|
--session-id) |
|
return 0 |
|
;; |
|
esac |
|
;; |
|
codex) |
|
case "$1" in |
|
-a|--ask-for-approval|\ |
|
-c|--config|\ |
|
-C|--cd|\ |
|
-i|--image|\ |
|
-m|--model|\ |
|
-p|--profile|\ |
|
-s|--sandbox|\ |
|
--add-dir|\ |
|
--disable|\ |
|
--enable|\ |
|
--local-provider|\ |
|
--remote|\ |
|
--remote-auth-token-env) |
|
return 0 |
|
;; |
|
esac |
|
;; |
|
agent) |
|
case "$1" in |
|
--api-key|\ |
|
-H|--header|\ |
|
--output-format|\ |
|
--mode|\ |
|
--model|\ |
|
--sandbox|\ |
|
--workspace|\ |
|
--plugin-dir|\ |
|
--worktree-base) |
|
return 0 |
|
;; |
|
esac |
|
;; |
|
agy) |
|
case "$1" in |
|
--add-dir|\ |
|
--conversation|\ |
|
--log-file|\ |
|
--model|\ |
|
--print-timeout|\ |
|
--project|\ |
|
--prompt|\ |
|
--prompt-interactive) |
|
return 0 |
|
;; |
|
esac |
|
;; |
|
pi) |
|
case "$1" in |
|
--provider|\ |
|
--model|\ |
|
--api-key|\ |
|
--system-prompt|\ |
|
--append-system-prompt|\ |
|
--mode|\ |
|
--session|\ |
|
--session-id|\ |
|
--fork|\ |
|
--session-dir|\ |
|
-n|--name|\ |
|
--models|\ |
|
-t|--tools|\ |
|
-xt|--exclude-tools|\ |
|
--thinking|\ |
|
-e|--extension|\ |
|
--skill|\ |
|
--prompt-template|\ |
|
--theme|\ |
|
--export|\ |
|
--list-models) |
|
return 0 |
|
;; |
|
esac |
|
;; |
|
esac |
|
return 1 |
|
} |
|
|
|
_wta_abs_path() { |
|
path=$1 |
|
if [ -d "$path" ]; then |
|
( CDPATH='' cd -- "$path" && pwd ) |
|
else |
|
mkdir -p "$path" |
|
( CDPATH='' cd -- "$path" && pwd ) |
|
fi |
|
} |
|
|
|
_wta_resolve_root() { |
|
repo_root=$1 |
|
root=$2 |
|
|
|
case $root in |
|
"~") root=$HOME ;; |
|
esac |
|
if [ "${root#~/}" != "$root" ]; then |
|
root=$HOME/${root#~/} |
|
fi |
|
|
|
if [ "${root#/}" != "$root" ]; then |
|
: |
|
else |
|
root=$repo_root/$root |
|
fi |
|
|
|
_wta_abs_path "$root" |
|
} |
|
|
|
_wta_root_default() { |
|
eval "value=\${$WTA_ROOT_ENV-}" |
|
if [ -z "$value" ]; then |
|
value=$WTA_ROOT_DEFAULT |
|
fi |
|
printf '%s' "$value" |
|
} |
|
|
|
_wta_ensure_excluded_if_inside_repo() { |
|
repo_root=$1 |
|
worktree_root=$2 |
|
|
|
case $worktree_root in |
|
"$repo_root"/*) ;; |
|
*) return 0 ;; |
|
esac |
|
|
|
rel=${worktree_root#"$repo_root"/} |
|
case $rel in |
|
""|.git|.git/*) return 0 ;; |
|
esac |
|
|
|
git_dir=$(git -C "$repo_root" rev-parse --git-dir 2>/dev/null) || return 0 |
|
if [ "${git_dir#/}" != "$git_dir" ]; then |
|
: |
|
else |
|
git_dir=$repo_root/$git_dir |
|
fi |
|
|
|
exclude_file=$git_dir/info/exclude |
|
mkdir -p "$(dirname "$exclude_file")" || return 1 |
|
|
|
if [ -f "$exclude_file" ]; then |
|
if grep -qxF "$rel" "$exclude_file" || grep -qxF "$rel/" "$exclude_file"; then |
|
return 0 |
|
fi |
|
fi |
|
|
|
{ |
|
printf '\n' |
|
printf '# worktree agents (%s)\n' "$WTA_LAUNCHER" |
|
printf '%s/\n' "$rel" |
|
} >>"$exclude_file" |
|
} |
|
|
|
_wta_create_or_reuse_worktree() { |
|
repo_root=$1 |
|
worktree_path=$2 |
|
branch=$3 |
|
base_ref=$4 |
|
|
|
mkdir -p "$(dirname "$worktree_path")" || return 1 |
|
|
|
if [ -e "$worktree_path/.git" ]; then |
|
_wta_print_stderr "${WTA_LAUNCHER}: reusing worktree: $worktree_path" |
|
return 0 |
|
fi |
|
|
|
if [ -e "$worktree_path" ]; then |
|
_wta_die "target path exists but is not a git worktree: $worktree_path" |
|
fi |
|
|
|
if git -C "$repo_root" show-ref --verify --quiet "refs/heads/$branch"; then |
|
_wta_print_stderr "${WTA_LAUNCHER}: adding worktree for existing branch '$branch'" |
|
git -C "$repo_root" worktree add "$worktree_path" "$branch" |
|
else |
|
_wta_print_stderr "${WTA_LAUNCHER}: creating branch '$branch' from '$base_ref'" |
|
git -C "$repo_root" worktree add -b "$branch" "$worktree_path" "$base_ref" |
|
fi |
|
} |
|
|
|
_wta_tool_has_any() { |
|
pattern=$1 |
|
shift |
|
|
|
i=1 |
|
while [ "$i" -le "$tool_n" ]; do |
|
eval "arg=\$tool_$i" |
|
if [ "$arg" = "$pattern" ]; then |
|
return 0 |
|
fi |
|
case $arg in |
|
"${pattern}"=*) return 0 ;; |
|
esac |
|
i=$((i + 1)) |
|
done |
|
|
|
[ $# -eq 0 ] && return 1 |
|
_wta_tool_has_any "$@" |
|
} |
|
|
|
_wta_validate_mode() { |
|
case "$1" in |
|
dangerous|safe|auto) return 0 ;; |
|
*) _wta_die "invalid launch mode: $1" ;; |
|
esac |
|
} |
|
|
|
_wta_tool_exec() { |
|
set -- |
|
|
|
case "$launch_mode:$WTA_TOOL" in |
|
dangerous:claude) |
|
if ! _wta_tool_has_any --dangerously-skip-permissions --permission-mode; then |
|
set -- "$@" --dangerously-skip-permissions |
|
fi |
|
;; |
|
dangerous:codex) |
|
if ! _wta_tool_has_any --dangerously-bypass-approvals-and-sandbox --ask-for-approval -a; then |
|
set -- "$@" --dangerously-bypass-approvals-and-sandbox |
|
fi |
|
;; |
|
dangerous:agent) |
|
if ! _wta_tool_has_any --yolo --force -f --sandbox; then |
|
set -- "$@" --force --sandbox disabled |
|
fi |
|
;; |
|
dangerous:agy) |
|
if ! _wta_tool_has_any --dangerously-skip-permissions --sandbox; then |
|
set -- "$@" --dangerously-skip-permissions |
|
fi |
|
;; |
|
dangerous:pi) |
|
if ! _wta_tool_has_any --approve --no-approve; then |
|
set -- "$@" --approve |
|
fi |
|
;; |
|
safe:codex) |
|
if ! _wta_tool_has_any --dangerously-bypass-approvals-and-sandbox --ask-for-approval -a --sandbox -s; then |
|
set -- "$@" --ask-for-approval on-request --sandbox workspace-write |
|
fi |
|
;; |
|
safe:agent) |
|
if ! _wta_tool_has_any --yolo --force -f --sandbox; then |
|
set -- "$@" --sandbox enabled |
|
fi |
|
;; |
|
safe:pi) |
|
if ! _wta_tool_has_any --approve --no-approve; then |
|
set -- "$@" --no-approve |
|
fi |
|
;; |
|
auto:claude) |
|
if ! _wta_tool_has_any --dangerously-skip-permissions --permission-mode; then |
|
set -- "$@" --permission-mode auto |
|
fi |
|
;; |
|
auto:codex) |
|
if ! _wta_tool_has_any --dangerously-bypass-approvals-and-sandbox --ask-for-approval -a --sandbox -s; then |
|
set -- "$@" --ask-for-approval never --sandbox workspace-write |
|
fi |
|
;; |
|
auto:agent) |
|
if ! _wta_tool_has_any --yolo --force -f --sandbox --auto-review; then |
|
set -- "$@" --auto-review --sandbox enabled |
|
fi |
|
;; |
|
auto:agy) |
|
if ! _wta_tool_has_any --dangerously-skip-permissions --sandbox; then |
|
set -- "$@" --sandbox |
|
fi |
|
;; |
|
auto:pi) |
|
if ! _wta_tool_has_any --approve --no-approve; then |
|
set -- "$@" --approve |
|
fi |
|
;; |
|
esac |
|
|
|
i=1 |
|
while [ "$i" -le "$tool_n" ]; do |
|
eval "arg=\$tool_$i" |
|
set -- "$@" "$arg" |
|
i=$((i + 1)) |
|
done |
|
exec "$tool_bin" "$@" |
|
} |
|
|
|
_wta_main() { |
|
worktree_name= |
|
branch= |
|
base_ref=HEAD |
|
worktree_root_arg= |
|
no_launch=0 |
|
launch_mode=dangerous |
|
tool_n=0 |
|
|
|
while [ $# -gt 0 ]; do |
|
case "$1" in |
|
-h|--help) |
|
_wta_usage |
|
return 0 |
|
;; |
|
-n|--name) |
|
shift |
|
[ $# -gt 0 ] || _wta_die "--name requires a value" |
|
worktree_name=$1 |
|
shift |
|
;; |
|
--name=*) |
|
worktree_name=${1#--name=} |
|
shift |
|
;; |
|
-b|--branch) |
|
shift |
|
[ $# -gt 0 ] || _wta_die "--branch requires a value" |
|
branch=$1 |
|
shift |
|
;; |
|
--branch=*) |
|
branch=${1#--branch=} |
|
shift |
|
;; |
|
--base) |
|
shift |
|
[ $# -gt 0 ] || _wta_die "--base requires a value" |
|
base_ref=$1 |
|
shift |
|
;; |
|
--base=*) |
|
base_ref=${1#--base=} |
|
shift |
|
;; |
|
--root) |
|
shift |
|
[ $# -gt 0 ] || _wta_die "--root requires a value" |
|
worktree_root_arg=$1 |
|
shift |
|
;; |
|
--root=*) |
|
worktree_root_arg=${1#--root=} |
|
shift |
|
;; |
|
--mode) |
|
shift |
|
[ $# -gt 0 ] || _wta_die "--mode requires a value" |
|
_wta_validate_mode "$1" |
|
launch_mode=$1 |
|
shift |
|
;; |
|
--mode=*) |
|
launch_mode=${1#--mode=} |
|
_wta_validate_mode "$launch_mode" |
|
shift |
|
;; |
|
--dangerous) |
|
launch_mode=dangerous |
|
shift |
|
;; |
|
--safe) |
|
launch_mode=safe |
|
shift |
|
;; |
|
--auto) |
|
launch_mode=auto |
|
shift |
|
;; |
|
--no-launch) |
|
no_launch=1 |
|
shift |
|
;; |
|
--) |
|
shift |
|
_wta_tool_push_all "$@" |
|
break |
|
;; |
|
-*) |
|
_wta_tool_push "$1" |
|
if [ "${1#*=}" = "$1" ] && _wta_tool_option_requires_value "$1"; then |
|
shift |
|
[ $# -gt 0 ] || _wta_die "${WTA_TOOL} option requires a value" |
|
_wta_tool_push "$1" |
|
fi |
|
shift |
|
;; |
|
*) |
|
if [ -z "$worktree_name" ] && [ -z "$branch" ] && ! _wta_is_probable_prompt "$1"; then |
|
worktree_name=$1 |
|
shift |
|
else |
|
_wta_tool_push_all "$@" |
|
break |
|
fi |
|
;; |
|
esac |
|
done |
|
|
|
repo_root=$(git rev-parse --show-toplevel 2>/dev/null) || |
|
_wta_die "not inside a git repository" |
|
|
|
if [ -z "$worktree_name" ] && [ -z "$branch" ]; then |
|
worktree_name=$(_wta_random_name) || _wta_die "failed to generate a random name" |
|
elif [ -z "$worktree_name" ]; then |
|
worktree_name=$(_wta_path_name_for "$branch") || _wta_die "failed to build worktree path name" |
|
fi |
|
|
|
[ -n "$branch" ] || branch=$worktree_name |
|
|
|
git -C "$repo_root" check-ref-format --branch "$branch" >/dev/null 2>&1 || |
|
_wta_die "invalid branch name: $branch" |
|
|
|
if [ -n "$worktree_root_arg" ]; then |
|
root_input=$worktree_root_arg |
|
else |
|
root_input=$(_wta_root_default) |
|
fi |
|
|
|
worktree_root=$(_wta_resolve_root "$repo_root" "$root_input") || |
|
_wta_die "failed to resolve worktree root" |
|
|
|
_wta_ensure_excluded_if_inside_repo "$repo_root" "$worktree_root" || |
|
_wta_die "failed to add worktree root to git info exclude" |
|
|
|
path_name=$(_wta_path_name_for "$worktree_name") || _wta_die "failed to build worktree path" |
|
|
|
worktree_path=$worktree_root/$path_name |
|
|
|
_wta_create_or_reuse_worktree "$repo_root" "$worktree_path" "$branch" "$base_ref" || |
|
_wta_die "failed to create or reuse worktree" |
|
|
|
if [ "$no_launch" -eq 1 ]; then |
|
_wta_print "$worktree_path" |
|
return 0 |
|
fi |
|
|
|
tool_bin=$(command -v "$WTA_TOOL") || _wta_die "${WTA_TOOL} executable not found in PATH" |
|
|
|
cd "$worktree_path" || _wta_die "failed to cd into worktree: $worktree_path" |
|
_wta_tool_exec |
|
} |
|
|
|
if [ "${0##*/}" = "_worktree-agent.sh" ]; then |
|
[ $# -gt 0 ] || { |
|
printf '%s\n' "worktree-agent: tool name required (claude, codex, agent, agy, pi)" >&2 |
|
exit 1 |
|
} |
|
_wta_configure "$1" || exit 1 |
|
shift |
|
_wta_main "$@" |
|
fi |