Skip to content

Instantly share code, notes, and snippets.

@saturnflyer
Created August 14, 2025 19:55
Show Gist options
  • Save saturnflyer/77d9ff81b7e14d1036f36471dcf2721d to your computer and use it in GitHub Desktop.
Save saturnflyer/77d9ff81b7e14d1036f36471dcf2721d to your computer and use it in GitHub Desktop.
Create a git worktree for feature work with Agent OS support
#!/usr/bin/env bash
# Git Worktree Feature Command
# Manages git worktrees for feature development with interactive interface
# Now supports reading from Agent OS specs
set -e
# Color codes for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
MAGENTA='\033[0;35m'
NC='\033[0m' # No Color
# Configuration
WORKTREE_BASE=".agent-os/worktrees"
SPECS_BASE=".agent-os/specs"
TEST_COMMAND="rails test"
MAIN_BRANCH="main"
VERBOSE=false
# Helper functions
error() {
echo -e "${RED}Error: $1${NC}" >&2
exit 1
}
success() {
echo -e "${GREEN}✓ $1${NC}"
}
info() {
echo -e "${BLUE}→ $1${NC}"
}
warning() {
echo -e "${YELLOW}⚠ $1${NC}"
}
debug() {
if [[ "$VERBOSE" == "true" ]]; then
echo -e "${CYAN}[DEBUG] $1${NC}" >&2
fi
}
# Check if we're in a git repository
check_git_repo() {
debug "Checking if we're in a git repository"
if ! git rev-parse --git-dir > /dev/null 2>&1; then
error "Not in a git repository"
fi
debug "Git repository confirmed"
}
# Check if git worktree command is available
check_worktree_support() {
debug "Checking git worktree support"
if ! git worktree list > /dev/null 2>&1; then
error "Git worktree command not available. Please upgrade git to version 2.5 or higher"
fi
debug "Git worktree command available"
}
# Get the date in YYYY-MM-DD format
get_date() {
date +%Y-%m-%d
}
# Convert string to kebab-case
to_kebab_case() {
echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' _' '-' | sed 's/[^a-z0-9-]//g' | sed 's/--*/-/g' | sed 's/^-//' | sed 's/-$//'
}
# List available Agent OS specs
list_specs() {
local specs=()
debug "Listing specs in $SPECS_BASE"
if [[ -d "$SPECS_BASE" ]]; then
for spec_dir in "$SPECS_BASE"/*; do
if [[ -d "$spec_dir" && -f "$spec_dir/spec.md" ]]; then
local spec_name=$(basename "$spec_dir")
debug "Found spec: $spec_name"
specs+=("$spec_name")
fi
done
fi
echo "${specs[@]}"
}
# Extract feature name from spec directory name
extract_feature_from_spec() {
local spec_dir="$1"
# Remove date prefix (YYYY-MM-DD-) and return the rest
echo "$spec_dir" | sed 's/^[0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}-//'
}
# Get spec overview from spec.md file
get_spec_overview() {
local spec_dir="$1"
local spec_file="$SPECS_BASE/$spec_dir/spec.md"
if [[ -f "$spec_file" ]]; then
# Extract the overview section (first paragraph after ## Overview)
awk '/^## Overview/{flag=1; next} /^##/{flag=0} flag && NF' "$spec_file" | head -n 3
else
echo "No overview available"
fi
}
# Check task completion status
get_spec_task_status() {
local spec_dir="$1"
local tasks_file="$SPECS_BASE/$spec_dir/tasks.md"
if [[ -f "$tasks_file" ]]; then
local total=$(grep -c "^- \[.\]" "$tasks_file" 2>/dev/null || echo "0")
local completed=$(grep -c "^- \[x\]" "$tasks_file" 2>/dev/null || echo "0")
echo "$completed/$total tasks completed"
else
echo "No tasks defined"
fi
}
# List existing worktrees in the worktrees directory
list_worktrees() {
local worktrees=()
local branches=()
debug "Listing worktrees in $WORKTREE_BASE"
# Get all worktrees
while IFS= read -r line; do
debug "Processing worktree line: $line"
if [[ "$line" == *"$WORKTREE_BASE"* ]]; then
local path=$(echo "$line" | awk '{print $1}')
local branch=$(echo "$line" | awk '{print $3}' | sed 's/\[//' | sed 's/\]//')
if [[ -d "$path" ]]; then
debug "Found worktree: $path (branch: $branch)"
worktrees+=("$path")
branches+=("$branch")
fi
fi
done < <(git worktree list)
echo "${worktrees[@]}"
}
# Interactive spec selection
spec_selection_mode() {
echo -e "${MAGENTA}Available Agent OS Specs${NC}"
echo -e "${MAGENTA}========================${NC}"
echo
local specs=($(list_specs))
if [[ ${#specs[@]} -eq 0 ]]; then
warning "No Agent OS specs found in $SPECS_BASE"
echo
read -p "Enter feature name manually: " feature_name
create_feature "$feature_name"
return
fi
echo "Select a spec to create a feature from:"
echo
local i=1
for spec in "${specs[@]}"; do
local feature_name=$(extract_feature_from_spec "$spec")
local task_status=$(get_spec_task_status "$spec")
echo -e "${CYAN}$i)${NC} $feature_name ${YELLOW}($task_status)${NC}"
# Show overview if verbose
if [[ "$VERBOSE" == "true" ]]; then
local overview=$(get_spec_overview "$spec" | sed 's/^/ /')
echo -e "${BLUE}$overview${NC}"
echo
fi
((i++))
done
echo
echo "0) Cancel"
echo
read -p "Select spec number: " choice
if [[ "$choice" == "0" || -z "$choice" ]]; then
exit 0
elif [[ "$choice" =~ ^[0-9]+$ ]] && [[ $choice -ge 1 && $choice -le ${#specs[@]} ]]; then
local index=$((choice - 1))
local selected_spec="${specs[$index]}"
local feature_name=$(extract_feature_from_spec "$selected_spec")
info "Creating feature from spec: $selected_spec"
create_feature_from_spec "$selected_spec" "$feature_name"
else
warning "Invalid selection"
exit 1
fi
}
# Interactive menu for worktree management
interactive_mode() {
echo -e "${CYAN}Git Worktree Feature Manager${NC}"
echo -e "${CYAN}=============================${NC}"
echo
local worktrees=($(list_worktrees))
if [[ ${#worktrees[@]} -eq 0 ]]; then
echo "No existing feature worktrees found."
echo
echo "1) Create new feature from spec"
echo "2) Create new feature (manual)"
echo "0) Exit"
echo
read -p "Select an option: " choice
case $choice in
1)
spec_selection_mode
;;
2)
read -p "Enter feature name: " feature_name
create_feature "$feature_name"
;;
0|"")
exit 0
;;
*)
warning "Invalid option"
exit 1
;;
esac
else
echo "Existing feature worktrees:"
echo
local i=1
for worktree in "${worktrees[@]}"; do
local name=$(basename "$worktree")
echo "$i) $name"
((i++))
done
echo
echo "Actions:"
echo "s#) Switch to worktree (e.g., s1)"
echo "f#) Finish worktree (e.g., f1)"
echo "n) Create new feature from spec"
echo "m) Create new feature (manual)"
echo "0) Exit"
echo
read -p "Select an option: " choice
if [[ "$choice" == "0" || -z "$choice" ]]; then
exit 0
elif [[ "$choice" == "n" ]]; then
spec_selection_mode
elif [[ "$choice" == "m" ]]; then
read -p "Enter feature name: " feature_name
create_feature "$feature_name"
elif [[ "$choice" =~ ^s([0-9]+)$ ]]; then
local index=$((${BASH_REMATCH[1]} - 1))
if [[ $index -ge 0 && $index -lt ${#worktrees[@]} ]]; then
switch_to_worktree "${worktrees[$index]}"
else
warning "Invalid worktree number"
exit 1
fi
elif [[ "$choice" =~ ^f([0-9]+)$ ]]; then
local index=$((${BASH_REMATCH[1]} - 1))
if [[ $index -ge 0 && $index -lt ${#worktrees[@]} ]]; then
finish_feature "${worktrees[$index]}"
else
warning "Invalid worktree number"
exit 1
fi
else
warning "Invalid option"
exit 1
fi
fi
}
# Switch to an existing worktree
switch_to_worktree() {
local worktree_path="$1"
if [[ ! -d "$worktree_path" ]]; then
error "Worktree directory does not exist: $worktree_path"
fi
info "Switching to worktree: $(basename "$worktree_path")"
# Since we can't change the parent shell's directory directly,
# we'll output the command for the user to run
echo
success "Worktree ready. Run this command to change directory:"
echo -e "${GREEN}cd $worktree_path${NC}"
}
# Create a new feature worktree
create_feature() {
local feature_name="$1"
debug "Creating feature with name: $feature_name"
if [[ -z "$feature_name" ]]; then
error "Feature name is required"
fi
# Convert to kebab case and limit to 5 words
local kebab_name=$(to_kebab_case "$feature_name" | cut -d'-' -f1-5)
local date_prefix=$(get_date)
local dir_name="${date_prefix}-${kebab_name}"
local branch_name="${kebab_name}"
local worktree_path="${WORKTREE_BASE}/${dir_name}"
debug "Kebab name: $kebab_name"
debug "Directory name: $dir_name"
debug "Branch name: $branch_name"
debug "Worktree path: $worktree_path"
info "Creating feature: $kebab_name"
info "Directory: $worktree_path"
info "Branch: $branch_name"
# Check if branch already exists
debug "Checking if branch '$branch_name' exists"
if git show-ref --verify --quiet "refs/heads/$branch_name"; then
error "Branch '$branch_name' already exists"
fi
# Check if worktree already exists
debug "Checking if worktree directory exists: $worktree_path"
if [[ -d "$worktree_path" ]]; then
error "Worktree directory already exists: $worktree_path"
fi
# Create the worktree base directory if it doesn't exist
mkdir -p "$WORKTREE_BASE"
# Create the worktree and branch
info "Creating worktree and branch..."
debug "Running: git worktree add -b $branch_name $worktree_path $MAIN_BRANCH"
git worktree add -b "$branch_name" "$worktree_path" "$MAIN_BRANCH"
success "Feature worktree created successfully"
# Output the cd command
echo
success "Worktree ready. Run these commands:"
echo -e "${GREEN}cd $(pwd)/$worktree_path${NC}"
echo -e "${GREEN}claude${NC}"
}
# Create a feature worktree from an Agent OS spec
create_feature_from_spec() {
local spec_dir="$1"
local feature_name="$2"
debug "Creating feature from spec: $spec_dir"
debug "Feature name: $feature_name"
# Use the existing create_feature function
create_feature "$feature_name"
# Get the newly created worktree path
local kebab_name=$(to_kebab_case "$feature_name" | cut -d'-' -f1-5)
local date_prefix=$(get_date)
local dir_name="${date_prefix}-${kebab_name}"
local worktree_path="${WORKTREE_BASE}/${dir_name}"
# Display spec information
echo
echo -e "${MAGENTA}Agent OS Spec Information${NC}"
echo -e "${MAGENTA}========================${NC}"
echo
echo -e "${CYAN}Spec:${NC} $spec_dir"
echo -e "${CYAN}Location:${NC} @$SPECS_BASE/$spec_dir/"
local task_status=$(get_spec_task_status "$spec_dir")
echo -e "${CYAN}Progress:${NC} $task_status"
echo
echo -e "${CYAN}Overview:${NC}"
get_spec_overview "$spec_dir"
echo
echo -e "${YELLOW}To work on this feature with Claude:${NC}"
echo -e "${GREEN}cd $(pwd)/$worktree_path${NC}"
echo -e "${GREEN}claude \"Let's work on the tasks in @$SPECS_BASE/$spec_dir/tasks.md\"${NC}"
# Create a reference file in the worktree
local ref_file="$worktree_path/.agent-os-spec-ref"
echo "$spec_dir" > "$ref_file"
debug "Created spec reference file: $ref_file"
}
# Finish a feature (rebase, test, merge, remove)
finish_feature() {
local worktree_path="$1"
debug "Finishing feature at worktree: $worktree_path"
local branch_name=$(cd "$worktree_path" && git branch --show-current)
debug "Branch name: $branch_name"
if [[ -z "$branch_name" ]]; then
error "Could not determine branch name for worktree"
fi
echo -e "${CYAN}Finishing feature: $branch_name${NC}"
echo
read -p "This will rebase, test, merge, and remove the worktree. Continue? (y/N): " confirm
if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then
warning "Cancelled"
exit 0
fi
# Save current directory
local original_dir=$(pwd)
# Step 1: Update main branch
info "Updating $MAIN_BRANCH branch..."
cd "$original_dir"
git checkout "$MAIN_BRANCH"
git pull origin "$MAIN_BRANCH" || warning "Could not pull from origin"
# Step 2: Rebase feature branch
info "Rebasing feature branch..."
cd "$worktree_path"
debug "Changed directory to: $worktree_path"
debug "Running: git rebase $MAIN_BRANCH"
if ! git rebase "$MAIN_BRANCH"; then
error "Rebase failed. Please resolve conflicts and try again"
fi
debug "Rebase successful"
# Step 3: Run tests
info "Running tests..."
if command -v rails &> /dev/null; then
if ! $TEST_COMMAND; then
error "Tests failed. Please fix the issues and try again"
fi
else
warning "Rails not found, skipping tests"
read -p "Continue without running tests? (y/N): " skip_tests
if [[ "$skip_tests" != "y" && "$skip_tests" != "Y" ]]; then
error "Aborted due to missing test runner"
fi
fi
# Step 4: Merge to main
info "Merging to $MAIN_BRANCH..."
cd "$original_dir"
git checkout "$MAIN_BRANCH"
git merge --no-ff "$branch_name" -m "Merge feature '$branch_name'"
# Step 5: Remove worktree
info "Removing worktree..."
debug "Running: git worktree remove $worktree_path"
git worktree remove "$worktree_path"
debug "Worktree removed"
# Step 6: Delete branch
info "Deleting branch..."
debug "Running: git branch -d $branch_name"
git branch -d "$branch_name"
debug "Branch deleted"
success "Feature '$branch_name' completed and merged successfully!"
echo
info "Remember to push your changes:"
echo -e "${GREEN}git push origin $MAIN_BRANCH${NC}"
}
# Show usage information
show_usage() {
cat << EOF
Usage: $(basename "$0") [OPTIONS] [feature-name]
Git Worktree Feature Manager with Agent OS Integration
Options:
(no args) Interactive mode - list and manage worktrees
<feature-name> Create a new feature worktree directly
--spec [name] Create feature from Agent OS spec (interactive if no name)
-h, --help Show this help message
-v, --verbose Enable verbose/debug output
Interactive Mode Commands:
s# Switch to worktree (e.g., s1 for first worktree)
f# Finish worktree (rebase, test, merge, remove)
n Create new feature from spec
m Create new feature (manual)
0 Exit
Examples:
$(basename "$0") # Enter interactive mode
$(basename "$0") user-auth # Create user-auth feature
$(basename "$0") "Password Reset" # Create password-reset feature
$(basename "$0") --spec # Select spec interactively
$(basename "$0") --spec workout-formats # Create from specific spec
$(basename "$0") -v --spec # Show spec overviews
EOF
}
# Main script logic
main() {
# Parse arguments for verbose flag and spec option
local args=()
local use_spec=false
local spec_name=""
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose)
VERBOSE=true
debug "Verbose mode enabled"
shift
;;
--spec)
use_spec=true
shift
if [[ $# -gt 0 && ! "$1" =~ ^- ]]; then
spec_name="$1"
shift
fi
;;
-h|--help)
show_usage
exit 0
;;
*)
args+=("$1")
shift
;;
esac
done
debug "Script started with arguments: $*"
debug "Worktree base: $WORKTREE_BASE"
debug "Specs base: $SPECS_BASE"
debug "Main branch: $MAIN_BRANCH"
debug "Use spec: $use_spec"
debug "Spec name: $spec_name"
# Check prerequisites
check_git_repo
check_worktree_support
# Handle spec option
if [[ "$use_spec" == "true" ]]; then
if [[ -n "$spec_name" ]]; then
# Try to find spec matching the name
local specs=($(list_specs))
local found_spec=""
for spec in "${specs[@]}"; do
local feature=$(extract_feature_from_spec "$spec")
if [[ "$feature" == "$spec_name" ]] || [[ "$spec" == *"$spec_name"* ]]; then
found_spec="$spec"
break
fi
done
if [[ -n "$found_spec" ]]; then
local feature_name=$(extract_feature_from_spec "$found_spec")
info "Found spec: $found_spec"
create_feature_from_spec "$found_spec" "$feature_name"
else
error "No spec found matching: $spec_name"
fi
else
# Interactive spec selection
spec_selection_mode
fi
elif [[ ${#args[@]} -eq 0 ]]; then
interactive_mode
else
create_feature "${args[*]}"
fi
}
# Run main function
main "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment