Created
August 14, 2025 19:55
-
-
Save saturnflyer/77d9ff81b7e14d1036f36471dcf2721d to your computer and use it in GitHub Desktop.
Create a git worktree for feature work with Agent OS support
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 | |
# 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