Last active
October 9, 2025 04:40
-
-
Save RichStone/794299dff9b5c8419e2ad21fc54d73ea to your computer and use it in GitHub Desktop.
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 ruby | |
# parallel-agent: Run multiple Claude Code agents in parallel using git worktrees | |
# | |
# WHY THIS SCRIPT EXISTS: | |
# 1. Run multiple Claude Code agents simultaneously for different tasks | |
# 2. Execute long-running agent tasks in a non-blocking way | |
# 3. Work on multiple features/branches without switching contexts | |
# 4. Isolate agent work to prevent conflicts between concurrent tasks | |
# | |
# HOW IT WORKS: | |
# - Creates a new git worktree for the specified (or current) branch | |
# - Launches Claude Code in that worktree with an optional prompt | |
# - Each worktree gets a unique directory name to allow multiple instances | |
# | |
# EXAMPLE USAGE: | |
# # Start long-running implementation task in background | |
# bin/parallel-agent -p "Implement PLAN.md" | |
# | |
# # Work on a bug fix while the above runs | |
# bin/parallel-agent -b bugfix-123 -p "Fix the authentication issue" | |
# | |
# # Launch agent for code review on another branch | |
# bin/parallel-agent -b feature-xyz -p "Review and refactor the API endpoints" | |
# | |
# # Simple launch with current branch, no specific prompt | |
# bin/parallel-agent | |
# | |
# # Skip permissions prompt for trusted operations | |
# bin/parallel-agent -y -p "Run automated tests" | |
# | |
# # Create a new branch if it doesn't exist | |
# bin/parallel-agent -c -b new-feature -p "Start new feature" | |
require "optparse" | |
require "fileutils" | |
require "shellwords" | |
class ParallelAgent | |
def initialize | |
@options = {} | |
parse_options | |
end | |
def run | |
validate_git_repository | |
set_branch_name | |
set_repo_folder | |
worktree_path = find_available_worktree_path | |
create_worktree(worktree_path) | |
if @options[:prompt] | |
launch_claude_code(worktree_path) | |
else | |
puts "Worktree created at: #{worktree_path}" | |
puts "No prompt provided - worktree ready for manual use" | |
end | |
rescue => e | |
puts "Error: #{e.message}" | |
puts e.backtrace | |
exit 1 | |
end | |
private | |
def parse_options | |
OptionParser.new do |opts| | |
opts.banner = "Usage: parallel-agent [options]\n\n" + | |
"Run multiple Claude Code agents in parallel using git worktrees.\n" + | |
"Perfect for long-running tasks or working on multiple features simultaneously.\n\n" + | |
"Examples:\n" + | |
" parallel-agent -p 'Implement PLAN.md'\n" + | |
" parallel-agent -b feature-xyz -p 'Add new API endpoint'\n" + | |
" parallel-agent -b bugfix-123\n" + | |
" parallel-agent -y -p 'Run automated tests' # Skip permissions prompt\n" + | |
" parallel-agent -c -b new-feature -p 'Start new feature' # Create branch if needed\n" | |
opts.on("-b", "--branch BRANCH", "Branch name for the worktree (defaults to current branch)") do |branch| | |
@options[:branch] = branch | |
end | |
opts.on("-p", "--prompt PROMPT", "Custom prompt to pass to Claude Code") do |prompt| | |
@options[:prompt] = prompt | |
end | |
opts.on("-y", "--yes", "Skip permissions prompt with --dangerously-skip-permissions") do | |
@options[:skip_permissions] = true | |
end | |
opts.on("-c", "--create", "Create the branch if it doesn't exist") do | |
@options[:create_branch] = true | |
end | |
opts.on("-h", "--help", "Show this help message") do | |
puts opts | |
exit | |
end | |
end.parse! | |
end | |
def validate_git_repository | |
unless system("git rev-parse --git-dir > /dev/null 2>&1") | |
raise "Not in a git repository" | |
end | |
end | |
def set_branch_name | |
@branch_name = @options[:branch] || current_branch | |
# Validate branch exists | |
unless branch_exists?(@branch_name) | |
if @options[:create_branch] | |
create_branch(@branch_name) | |
else | |
# Show similar branches to help user | |
similar_branches = find_similar_branches(@branch_name) | |
error_msg = "Branch '#{@branch_name}' does not exist" | |
unless similar_branches.empty? | |
error_msg += "\n\nDid you mean one of these?\n" | |
similar_branches.each { |b| error_msg += " #{b}\n" } | |
end | |
error_msg += "\n\nUse -c flag to create the branch" | |
raise error_msg | |
end | |
end | |
end | |
def current_branch | |
branch = `git branch --show-current`.strip | |
raise "Could not determine current branch" if branch.empty? | |
branch | |
end | |
def branch_exists?(branch) | |
# Check local branch | |
return true if system("git show-ref --verify --quiet refs/heads/#{branch}") | |
# Check remote branch | |
return true if system("git show-ref --verify --quiet refs/remotes/origin/#{branch}") | |
# Check if branch exists in any form (handles cases like origin/branch) | |
!`git branch -a | grep -E "(^|/)#{Regexp.escape(branch)}$"`.strip.empty? | |
end | |
def find_similar_branches(branch_name) | |
all_branches = `git branch -a`.split("\n").map(&:strip) | |
all_branches.map! { |b| b.sub(/^\*?\s*/, "").sub(/^remotes\/origin\//, "") } | |
all_branches.uniq! | |
# Find branches that contain the search term | |
similar = all_branches.select { |b| b.include?(branch_name) || branch_name.include?(b) } | |
# If no matches, try partial matches | |
if similar.empty? | |
parts = branch_name.split(/[\/\-_]/) | |
similar = all_branches.select do |b| | |
parts.any? { |part| b.include?(part) && part.length > 3 } | |
end | |
end | |
similar.take(5) | |
end | |
def create_branch(branch_name) | |
puts "Creating new branch: #{branch_name}" | |
# Create branch from current HEAD | |
unless system("git branch #{Shellwords.escape(branch_name)}") | |
raise "Failed to create branch '#{branch_name}'" | |
end | |
puts "Branch '#{branch_name}' created successfully" | |
end | |
def set_repo_folder | |
@repo_folder = File.basename(Dir.pwd) | |
end | |
def find_available_worktree_path | |
base_name = "#{@repo_folder}-#{@branch_name}" | |
parent_dir = File.expand_path("..") | |
# Check base name first | |
candidate = File.join(parent_dir, base_name) | |
return candidate unless Dir.exist?(candidate) | |
# Find next available number | |
counter = 1 | |
loop do | |
candidate = File.join(parent_dir, "#{base_name}-#{counter}") | |
return candidate unless Dir.exist?(candidate) | |
counter += 1 | |
end | |
end | |
def create_worktree(path) | |
puts "Creating worktree at: #{path}" | |
# Check if worktree already exists for this branch | |
existing_worktrees = `git worktree list --porcelain`.split("\n\n") | |
existing_worktrees.each do |worktree| | |
if worktree.include?("branch refs/heads/#{@branch_name}") | |
worktree_path = worktree.match(/^worktree (.+)$/)[1] | |
raise "Worktree already exists for branch '#{@branch_name}' at: #{worktree_path}" | |
end | |
end | |
# Try to create worktree with the branch | |
if system("git worktree add #{Shellwords.escape(path)} #{Shellwords.escape(@branch_name)} 2>/dev/null") | |
puts "Worktree created successfully" | |
return | |
end | |
# If that fails, try with origin/ prefix | |
if system("git worktree add #{Shellwords.escape(path)} origin/#{Shellwords.escape(@branch_name)} 2>/dev/null") | |
puts "Worktree created successfully from origin/#{@branch_name}" | |
return | |
end | |
# If both fail, show error | |
raise "Failed to create worktree for branch '#{@branch_name}'" | |
end | |
def launch_claude_code(worktree_path) | |
Dir.chdir(worktree_path) do | |
cmd = "claude" | |
cmd += " --dangerously-skip-permissions" if @options[:skip_permissions] | |
cmd += " --prompt #{Shellwords.escape(@options[:prompt])}" if @options[:prompt] | |
puts "Launching Claude Code in: #{worktree_path}" | |
puts "Command: #{cmd}" if @options[:prompt] || @options[:skip_permissions] | |
exec(cmd) | |
end | |
end | |
end | |
# Run the script | |
ParallelAgent.new.run |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment