Skip to content

Instantly share code, notes, and snippets.

@saturnflyer
Last active August 15, 2025 17:31
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 ruby
# Returns the port number for the current git branch
# Used by Procfile.dev to start the server on the correct port
require "yaml"
require "fileutils"
DEFAULT_PORT = 3000
CONFIG_PATH = File.join(Dir.pwd, ".branch-config.yml")
def current_git_branch
`git branch --show-current`.strip
rescue
"main"
end
def load_config
return {} unless File.exist?(CONFIG_PATH)
YAML.load_file(CONFIG_PATH) || {}
rescue
{}
end
def get_port_for_branch(branch)
config = load_config
# Return existing port if configured
if config.dig("branches", branch, "port")
return config.dig("branches", branch, "port")
end
# Default port for main branch
return DEFAULT_PORT if branch == "main"
# Find next available port for new branches
used_ports = config.dig("branches")&.values&.map { |data| data["port"] } || []
(3001..9999).each do |port|
unless used_ports.include?(port)
# Save the new port assignment
config["branches"] ||= {}
config["branches"][branch] = {
"port" => port,
"last_used" => Time.now.iso8601
}
File.write(CONFIG_PATH, config.to_yaml)
return port
end
end
# Fallback
DEFAULT_PORT
end
# Output the port for the current branch
branch = current_git_branch
port = get_port_for_branch(branch)
puts port
#!/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
# Create worktrees as siblings to the main directory
WORKTREE_BASE="../"
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/-$//'
}
# Get port for current branch using bin/branch-port
get_branch_port() {
local original_dir=$(pwd)
local worktree_dir="$1"
# Change to worktree directory to get the correct branch context
cd "$worktree_dir" 2>/dev/null || return 1
# Use bin/branch-port to get the port
local port=$("$original_dir/bin/branch-port" 2>/dev/null)
# Return to original directory
cd "$original_dir"
echo "$port"
}
# 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=()
local project_name=$(basename "$(pwd)")
debug "Listing worktrees for project: $project_name"
# Get all worktrees, looking for ones that match our project naming pattern
while IFS= read -r line; do
debug "Processing worktree line: $line"
local path=$(echo "$line" | awk '{print $1}')
local branch=$(echo "$line" | awk '{print $3}' | sed 's/\[//' | sed 's/\]//')
# Check if this worktree is a sibling directory with our project prefix
# Handle both relative and absolute paths
if [[ "$path" == *"/${project_name}-"* ]] && [[ "$path" != *"/${project_name}/"* ]] && [[ -d "$path" ]]; then
debug "Found worktree: $path (branch: $branch)"
worktrees+=("$path")
branches+=("$branch")
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))
local worktrees=($(list_worktrees))
# Option to finish existing worktrees if any exist
if [[ ${#worktrees[@]} -gt 0 ]]; then
echo -e "${YELLOW}Existing worktrees to finish:${NC}"
local i=1
for worktree in "${worktrees[@]}"; do
local name=$(basename "$worktree")
local branch=$(cd "$worktree" 2>/dev/null && git branch --show-current 2>/dev/null || echo "unknown")
# Check if worktree has associated spec
local associated_spec=""
for spec in "${specs[@]}"; do
local feature_name=$(extract_feature_from_spec "$spec")
if [[ "$branch" == "$feature_name" ]] || [[ "$name" == *"$feature_name"* ]]; then
associated_spec="$spec"
break
fi
done
if [[ -n "$associated_spec" ]]; then
local task_status=$(get_spec_task_status "$associated_spec")
echo -e " f$i) Finish: $name ${YELLOW}($task_status)${NC}"
else
echo -e " f$i) Finish: $name"
fi
((i++))
done
echo
fi
if [[ ${#specs[@]} -eq 0 ]]; then
warning "No Agent OS specs found in $SPECS_BASE"
echo
read -p "Enter feature name manually: " feature_name
handle_feature_creation "$feature_name" ""
return
fi
echo -e "${CYAN}Work on spec:${NC}"
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")
# Check if worktree already exists for this spec
local project_name=$(basename "$(pwd)")
local existing_worktree=""
for worktree_path in $(list_worktrees); do
if [[ "$worktree_path" == *"$feature_name"* ]]; then
existing_worktree="$worktree_path"
break
fi
done
if [[ -n "$existing_worktree" ]]; then
echo -e "${CYAN}$i)${NC} $feature_name ${YELLOW}($task_status)${NC} ${GREEN}[Worktree exists]${NC}"
else
echo -e "${CYAN}$i)${NC} $feature_name ${YELLOW}($task_status)${NC}"
fi
# 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 option: " choice
if [[ "$choice" == "0" || -z "$choice" ]]; then
exit 0
elif [[ "$choice" =~ ^f([0-9]+)$ ]]; then
# Handle finish option
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
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")
handle_feature_creation "$feature_name" "$selected_spec"
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) Work on existing spec"
echo "2) Create new feature from spec"
echo "3) Create new feature (manual)"
echo "0) Exit"
echo
read -p "Select an option: " choice
case $choice in
1)
spec_selection_mode
;;
2)
spec_selection_mode
;;
3)
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 "w) Work on existing spec"
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" == "w" ]]; then
spec_selection_mode
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
}
# Handle feature creation or switching - unified function
handle_feature_creation() {
local feature_name="$1"
local spec_dir="$2"
# Convert to kebab case for consistency
local kebab_name=$(to_kebab_case "$feature_name" | cut -d'-' -f1-5)
local project_name=$(basename "$(pwd)")
local branch_name="${kebab_name}"
# Check if a worktree already exists for this feature
local existing_worktree=""
for worktree_path in $(list_worktrees); do
if [[ "$worktree_path" == *"$kebab_name"* ]]; then
existing_worktree="$worktree_path"
break
fi
done
if [[ -n "$existing_worktree" ]]; then
info "Worktree already exists for feature: $kebab_name"
info "Switching to existing worktree..."
# Set spec reference if available
if [[ -n "$spec_dir" ]]; then
export AGENT_OS_SPEC_REF="$spec_dir"
fi
switch_to_worktree "$existing_worktree"
else
# Check if branch already exists (without worktree)
if git show-ref --verify --quiet "refs/heads/$branch_name"; then
info "Branch '$branch_name' already exists"
# Check if a worktree already exists for this branch
local date_prefix=$(get_date)
local dir_name="${project_name}-${date_prefix}-${kebab_name}"
local worktree_path="${WORKTREE_BASE}/${dir_name}"
# Check if worktree directory exists
if [[ -d "$worktree_path" ]]; then
info "Worktree already exists at: $worktree_path"
info "Switching to existing worktree..."
# Set spec reference if available
if [[ -n "$spec_dir" ]]; then
export AGENT_OS_SPEC_REF="$spec_dir"
fi
switch_to_worktree "$worktree_path"
else
info "Creating worktree for existing branch..."
# Set spec reference if available
if [[ -n "$spec_dir" ]]; then
export AGENT_OS_SPEC_REF="$spec_dir"
fi
# Create the worktree for the existing branch
git worktree add "$worktree_path" "$branch_name"
success "Worktree created for existing branch"
# Switch to the worktree
switch_to_worktree "$worktree_path"
fi
else
info "Creating new worktree for feature: $kebab_name"
# Set spec reference if available
if [[ -n "$spec_dir" ]]; then
export AGENT_OS_SPEC_REF="$spec_dir"
fi
create_feature "$feature_name"
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")"
# Change to the worktree directory
cd "$worktree_path" || error "Failed to change directory to $worktree_path"
# Get port for this branch using bin/branch-port
local port=$(bin/branch-port)
info "Using port $port for this worktree"
# Check if a Rails server is already running on this port
if lsof -Pi :$port -sTCP:LISTEN -t >/dev/null 2>&1; then
warning "Rails server already running on port $port"
else
# Start the Rails server in the background
info "Starting Rails server on port $port..."
rails server -p $port > /tmp/rails-server-$(basename "$worktree_path").log 2>&1 &
local server_pid=$!
sleep 2 # Give the server a moment to start
if kill -0 $server_pid 2>/dev/null; then
success "Rails server started (PID: $server_pid)"
else
warning "Rails server may have failed to start. Check /tmp/rails-server-$(basename "$worktree_path").log"
fi
fi
echo
success "Feature worktree ready!"
echo -e "${CYAN}Visit your application at:${NC} ${GREEN}http://localhost:$port${NC}"
echo
info "Starting Claude with spec context..."
echo
# Start Claude command with spec reference if available
if [[ -n "$AGENT_OS_SPEC_REF" ]]; then
# Get the spec overview for better context
local spec_overview=$(get_spec_overview "$AGENT_OS_SPEC_REF" | head -n 1)
exec claude "I'm ready to work on the '$AGENT_OS_SPEC_REF' spec. The tasks are in @$SPECS_BASE/$AGENT_OS_SPEC_REF/tasks.md
Please start by reviewing the tasks and begin with the first uncompleted task."
else
exec claude
fi
}
# 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 project_name=$(basename "$(pwd)")
local dir_name="${project_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
warning "Branch '$branch_name' already exists - this shouldn't happen in create_feature"
error "Branch '$branch_name' already exists. Use the interactive menu to work on existing specs."
fi
# Check if worktree already exists
debug "Checking if worktree directory exists: $worktree_path"
if [[ -d "$worktree_path" ]]; then
info "Worktree already exists at: $worktree_path"
info "Switching to existing worktree..."
switch_to_worktree "$worktree_path"
return # Exit the function after switching
fi
# 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"
# Change to the new worktree directory
cd "$worktree_path" || error "Failed to change directory to $worktree_path"
# Get port for this new branch using bin/branch-port (it will auto-assign)
local port=$(bin/branch-port)
info "Assigned port $port to branch $branch_name"
# Start the Rails server in the background
info "Starting Rails server on port $port..."
rails server -p $port > /tmp/rails-server-$(basename "$worktree_path").log 2>&1 &
local server_pid=$!
sleep 2 # Give the server a moment to start
if kill -0 $server_pid 2>/dev/null; then
success "Rails server started (PID: $server_pid)"
else
warning "Rails server may have failed to start. Check /tmp/rails-server-$(basename "$worktree_path").log"
fi
echo
success "Feature worktree ready!"
echo -e "${CYAN}Visit your application at:${NC} ${GREEN}http://localhost:$port${NC}"
echo
info "Starting Claude with spec context..."
echo
# Start Claude command with spec reference if available
if [[ -n "$AGENT_OS_SPEC_REF" ]]; then
exec claude "I'm ready to work on the '$AGENT_OS_SPEC_REF' spec. The tasks are in @$SPECS_BASE/$AGENT_OS_SPEC_REF/tasks.md
Please start by reviewing the tasks and begin with the first uncompleted task."
else
exec claude "I'm ready to work on this feature branch. What would you like me to help with?"
fi
}
# 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"
# Display spec information first
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
info "When Claude starts, you can reference: @$SPECS_BASE/$spec_dir/tasks.md"
echo
# Store spec_dir in an environment variable to pass to the modified create_feature
export AGENT_OS_SPEC_REF="$spec_dir"
# Use the existing create_feature function (which will exec claude)
create_feature "$feature_name"
}
# 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
# Save current directory
local original_dir=$(pwd)
# Get port for this branch before we lose access to it
local port=$(get_branch_port "$worktree_path")
debug "Branch is using port: $port"
# Check for uncommitted changes in worktree
cd "$worktree_path"
if ! git diff-index --quiet HEAD -- 2>/dev/null; then
warning "You have uncommitted changes in the worktree"
git status --short
echo
read -p "Do you want to continue anyway? (y/N): " continue_with_changes
if [[ "$continue_with_changes" != "y" && "$continue_with_changes" != "Y" ]]; then
warning "Please commit or stash your changes first"
exit 0
fi
fi
# Check if branch has any differences from main
cd "$original_dir"
git checkout "$MAIN_BRANCH" > /dev/null 2>&1
git pull origin "$MAIN_BRANCH" > /dev/null 2>&1 || true
# Check if there are any commits difference between branch and main
local commits_ahead=$(git rev-list --count "$MAIN_BRANCH".."$branch_name" 2>/dev/null || echo "0")
local commits_behind=$(git rev-list --count "$branch_name".."$MAIN_BRANCH" 2>/dev/null || echo "0")
if [[ "$commits_ahead" == "0" && "$commits_behind" == "0" ]]; then
info "Branch '$branch_name' has no differences from $MAIN_BRANCH"
# Check if the branch exists on remote
if git ls-remote --heads origin "$branch_name" | grep -q .; then
warning "Branch exists on remote but has no local changes"
read -p "Remove the worktree and local branch? (y/N): " remove_empty
if [[ "$remove_empty" == "y" || "$remove_empty" == "Y" ]]; then
info "Removing empty worktree and branch..."
git worktree remove "$worktree_path" --force 2>/dev/null || git worktree remove "$worktree_path"
git branch -D "$branch_name" 2>/dev/null || true
# Clean up branch port configuration
if [[ -f ".branch-config.yml" ]]; then
ruby -ryaml -e "
config = YAML.load_file('.branch-config.yml') || {}
config['branches']&.delete('$branch_name')
File.write('.branch-config.yml', config.to_yaml)
" 2>/dev/null || true
fi
success "Cleaned up empty worktree and branch"
exit 0
fi
else
info "Removing local worktree with no changes..."
git worktree remove "$worktree_path" --force 2>/dev/null || git worktree remove "$worktree_path"
git branch -D "$branch_name" 2>/dev/null || true
# Clean up branch port configuration
if [[ -f ".branch-config.yml" ]]; then
ruby -ryaml -e "
config = YAML.load_file('.branch-config.yml') || {}
config['branches']&.delete('$branch_name')
File.write('.branch-config.yml', config.to_yaml)
" 2>/dev/null || true
fi
success "Cleaned up empty worktree and branch"
exit 0
fi
fi
# Try to find associated spec for this branch
local kebab_name="${branch_name}"
local associated_spec=""
for spec in $(list_specs); do
local feature_name=$(extract_feature_from_spec "$spec")
if [[ "$feature_name" == "$kebab_name" ]]; then
associated_spec="$spec"
break
fi
done
echo -e "${CYAN}Finishing feature: $branch_name${NC}"
# Show spec task status if spec found
if [[ -n "$associated_spec" ]]; then
echo -e "${MAGENTA}Associated spec: $associated_spec${NC}"
local task_status=$(get_spec_task_status "$associated_spec")
echo -e "${YELLOW}Task status: $task_status${NC}"
# Check if all tasks are completed
local tasks_file="$SPECS_BASE/$associated_spec/tasks.md"
if [[ -f "$tasks_file" ]]; then
local incomplete=$(grep -c "^- \[ \]" "$tasks_file" 2>/dev/null || echo "0")
if [[ $incomplete -gt 0 ]]; then
warning "There are $incomplete incomplete tasks in the spec"
read -p "Continue anyway? (y/N): " continue_incomplete
if [[ "$continue_incomplete" != "y" && "$continue_incomplete" != "Y" ]]; then
warning "Cancelled - please complete all tasks first"
exit 0
fi
fi
fi
fi
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
# 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
warning "Rebase failed due to conflicts"
echo
echo "Options:"
echo "1) cd into worktree to resolve conflicts"
echo "2) Abort and exit"
echo
read -p "Select option (1-2): " rebase_choice
case "$rebase_choice" in
1)
info "Changing to worktree directory: $worktree_path"
echo
echo "To resolve conflicts:"
echo " 1. Fix the conflicted files"
echo " 2. git add <resolved files>"
echo " 3. git rebase --continue"
echo " 4. Run 'bin/feature' again to finish"
echo
cd "$worktree_path"
exec $SHELL
;;
2|*)
error "Rebase failed. Please resolve conflicts and try again"
;;
esac
fi
debug "Rebase successful"
# Step 3: Run tests
info "Running tests..."
if command -v rails &> /dev/null; then
if ! $TEST_COMMAND; then
warning "Tests failed"
echo
echo "Options:"
echo "1) cd into worktree to fix test failures"
echo "2) Skip tests and continue (not recommended)"
echo "3) Abort and exit"
echo
read -p "Select option (1-3): " test_choice
case "$test_choice" in
1)
info "Changing to worktree directory: $worktree_path"
echo
echo "To fix test failures:"
echo " 1. Run '$TEST_COMMAND' to see failures"
echo " 2. Fix the failing tests"
echo " 3. Run 'bin/feature' again to finish"
echo
cd "$worktree_path"
exec $SHELL
;;
2)
warning "Skipping tests - proceeding with merge"
;;
3|*)
error "Tests failed. Please fix the issues and try again"
;;
esac
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: Update spec status if needed
if [[ -n "$associated_spec" ]]; then
info "Updating spec status..."
local spec_file="$SPECS_BASE/$associated_spec/spec.md"
if [[ -f "$spec_file" ]]; then
# Update status to Complete
sed -i '' 's/> Status: .*/> Status: Complete/' "$spec_file"
success "Spec status updated to Complete"
fi
fi
# Step 5: Merge to main
info "Merging to $MAIN_BRANCH..."
cd "$original_dir"
git checkout "$MAIN_BRANCH"
if [[ -n "$associated_spec" ]]; then
git merge --no-ff "$branch_name" -m "Merge feature '$branch_name' - Complete spec: $associated_spec"
else
git merge --no-ff "$branch_name" -m "Merge feature '$branch_name'"
fi
# Step 6: Shut down any running server for this branch
if [[ -n "$port" ]]; then
info "Shutting down server on port $port..."
# Find and kill any process listening on this port
local server_pids=$(lsof -Pi :$port -sTCP:LISTEN -t 2>/dev/null)
if [[ -n "$server_pids" ]]; then
debug "Found server PIDs: $server_pids"
echo "$server_pids" | while read -r pid; do
if [[ -n "$pid" ]]; then
kill -TERM "$pid" 2>/dev/null || true
debug "Sent TERM signal to PID $pid"
fi
done
# Give processes a moment to shut down gracefully
sleep 1
# Force kill if still running
local remaining_pids=$(lsof -Pi :$port -sTCP:LISTEN -t 2>/dev/null)
if [[ -n "$remaining_pids" ]]; then
echo "$remaining_pids" | while read -r pid; do
if [[ -n "$pid" ]]; then
kill -KILL "$pid" 2>/dev/null || true
debug "Force killed PID $pid"
fi
done
fi
success "Server on port $port shut down"
else
debug "No server found running on port $port"
fi
fi
# Step 7: Remove worktree
info "Removing worktree..."
debug "Running: git worktree remove $worktree_path"
# Use --force if the normal remove fails (e.g., due to uncommitted changes we already warned about)
if ! git worktree remove "$worktree_path" 2>/dev/null; then
debug "Normal remove failed, trying with --force"
git worktree remove "$worktree_path" --force
fi
debug "Worktree removed"
# Step 8: Delete branch
info "Deleting branch..."
debug "Running: git branch -d $branch_name"
git branch -d "$branch_name"
debug "Branch deleted"
# Step 9: Clean up branch port configuration
info "Cleaning up branch port configuration..."
# Remove the branch from .branch-config.yml
if [[ -f ".branch-config.yml" ]]; then
ruby -ryaml -e "
config = YAML.load_file('.branch-config.yml') || {}
config['branches']&.delete('$branch_name')
File.write('.branch-config.yml', config.to_yaml)
" 2>/dev/null || warning "Could not clean up branch port config"
fi
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
Creates worktrees as siblings to the main project directory with the naming
pattern: projectname-YYYY-MM-DD-feature-name
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
Note: Worktrees are created as siblings to the current project directory.
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