Skip to content

Commit 325ce1e

Browse files
committed
Add parallel agents script
1 parent 6ee9003 commit 325ce1e

File tree

4 files changed

+393
-0
lines changed

4 files changed

+393
-0
lines changed

bin/parallel-agents/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# The Vibe
2+
3+
This is mostly blind-vibe-coded, use with caution and only execute in safe environments since it makes use of Claude Code `--dangerously-skip-permissions`.
4+
5+
## Requirements
6+
7+
- Claude Code CLI installed.
8+
- Ghostty Terminal installed.
9+
- Longer running agentic workflows that you don't want to be blocking your worktree.
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "fileutils"
5+
6+
# Copy .gitignore-matched files between directories
7+
# Usage: copy_ignored_files <source-dir> <dest-dir>
8+
9+
source_dir = ARGV[0]
10+
dest_dir = ARGV[1]
11+
12+
if source_dir.nil? || dest_dir.nil?
13+
puts "Usage: #{$PROGRAM_NAME} <source-dir> <dest-dir>"
14+
puts "Example: #{$PROGRAM_NAME} /path/to/project /path/to/worktree"
15+
exit 1
16+
end
17+
18+
unless Dir.exist?(source_dir)
19+
puts "Error: Source directory does not exist: #{source_dir}"
20+
exit 1
21+
end
22+
23+
puts "Copying .gitignore-matched files from #{source_dir} to #{dest_dir}"
24+
25+
Dir.chdir(source_dir) do
26+
# Get list of all ignored files that exist
27+
ignored_files = `git ls-files --ignored --exclude-standard --others --directory`.lines.map(&:strip)
28+
29+
ignored_files.each do |file|
30+
# Skip directory markers (end with /)
31+
next if file.end_with?("/")
32+
33+
# Skip if file doesn't exist
34+
next unless File.exist?(file)
35+
36+
dest_file = File.join(dest_dir, file)
37+
dest_file_dir = File.dirname(dest_file)
38+
39+
# Create destination directory if needed
40+
FileUtils.mkdir_p(dest_file_dir)
41+
42+
if File.file?(file)
43+
puts "Copying: #{file}"
44+
FileUtils.cp(file, dest_file, preserve: true)
45+
elsif File.directory?(file)
46+
puts "Copying directory: #{file}"
47+
FileUtils.cp_r(file, dest_file, preserve: true)
48+
end
49+
end
50+
end
51+
52+
puts "Done copying ignored files!"

bin/parallel-agents/project_setup

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
# Agent Project Setup Script
5+
# This script is called by bin/parallel-agents/worktree_run_ghostty when using --setup-type full
6+
# Customize this script for your project's specific setup needs
7+
8+
puts "LAUNCHED!"
Lines changed: 324 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,324 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "optparse"
5+
require "fileutils"
6+
7+
# Configuration class to hold all settings
8+
class WorktreeConfig
9+
attr_accessor :num_worktrees, :setup_type, :branch_name, :ultra_mode, :ab_test_mode, :prompt
10+
11+
def initialize
12+
@num_worktrees = 1
13+
@setup_type = "plan"
14+
@branch_name = ""
15+
@ultra_mode = false
16+
@ab_test_mode = false
17+
@prompt = "Hey Claude, what's new with the code here?"
18+
end
19+
20+
def validate!
21+
raise "Number of worktrees must be between 1 and 10" unless (1..10).cover?(num_worktrees)
22+
raise "Setup type must be 'plan' or 'full'" unless %w[plan full].include?(setup_type)
23+
end
24+
25+
def ab_test?
26+
ab_test_mode
27+
end
28+
29+
def ultra?
30+
ultra_mode
31+
end
32+
end
33+
34+
# Main worktree manager class
35+
class WorktreeManager
36+
attr_reader :config, :current_branch, :repo_dir, :parent_dir, :repo_name
37+
38+
def initialize(config)
39+
@config = config
40+
@current_branch = `git branch --show-current`.strip
41+
@repo_dir = Dir.pwd
42+
@parent_dir = File.dirname(@repo_dir)
43+
@repo_name = File.basename(@repo_dir)
44+
end
45+
46+
def run
47+
validate_environment!
48+
49+
config.branch_name = current_branch if config.branch_name.empty?
50+
51+
# Override settings for A/B test mode
52+
config.num_worktrees = 2 if config.ab_test?
53+
54+
puts "Creating #{config.num_worktrees} worktree(s) with setup type '#{config.setup_type}'..."
55+
puts ""
56+
57+
created_worktrees = []
58+
59+
config.num_worktrees.times do |i|
60+
worktree_info = create_worktree(i + 1)
61+
created_worktrees << worktree_info
62+
sleep 0.5
63+
end
64+
65+
print_summary(created_worktrees)
66+
end
67+
68+
private
69+
70+
def validate_environment!
71+
# Check if we're in a git repository
72+
unless system("git rev-parse --is-inside-work-tree > /dev/null 2>&1")
73+
raise "Not in a git repository"
74+
end
75+
76+
# Check if Ghostty is installed
77+
unless system("command -v ghostty > /dev/null 2>&1")
78+
raise "Ghostty not found in PATH"
79+
end
80+
end
81+
82+
def existing_worktrees
83+
# Don't memoize - need fresh list after each worktree creation
84+
output = `git worktree list 2>/dev/null`
85+
output.lines
86+
.select { |line| line.include?("#{repo_name}-#{config.branch_name}-worktree-") }
87+
.map { |line| File.basename(line.split.first) }
88+
rescue
89+
[]
90+
end
91+
92+
def find_next_worktree_number(suffix = "")
93+
i = 1
94+
loop do
95+
candidate = "#{repo_name}-#{config.branch_name}-worktree-#{i}#{suffix}"
96+
return i unless existing_worktrees.include?(candidate)
97+
i += 1
98+
end
99+
end
100+
101+
def determine_ultra_mode(iteration)
102+
if config.ab_test?
103+
iteration == 1 ? false : true
104+
else
105+
config.ultra?
106+
end
107+
end
108+
109+
def determine_suffix(iteration)
110+
return "" unless config.ab_test?
111+
iteration == 1 ? "-regular" : "-ultra"
112+
end
113+
114+
def create_worktree(iteration)
115+
current_ultra_mode = determine_ultra_mode(iteration)
116+
suffix = determine_suffix(iteration)
117+
118+
worktree_num = find_next_worktree_number(suffix)
119+
worktree_name = "#{repo_name}-#{config.branch_name}-worktree-#{worktree_num}#{suffix}"
120+
worktree_branch = worktree_name
121+
worktree_path = File.join(parent_dir, worktree_name)
122+
123+
puts "Creating worktree: #{worktree_name}"
124+
125+
# Create git worktree
126+
unless system("git worktree add -b #{worktree_branch} #{worktree_path}")
127+
raise "Failed to create worktree: #{worktree_name}"
128+
end
129+
130+
# Copy ignored files
131+
copy_ignored_files_script = File.join(repo_dir, "bin", "parallel-agents", "copy_ignored_files")
132+
puts "Copying ignored files..."
133+
unless system("#{copy_ignored_files_script} #{repo_dir} #{worktree_path}")
134+
raise "Failed to copy ignored files to #{worktree_name}"
135+
end
136+
137+
# Build Claude command
138+
claude_cmd = if current_ultra_mode
139+
"claude --dangerously-skip-permissions \"*ultrathink #{config.prompt}\""
140+
else
141+
"claude --dangerously-skip-permissions \"#{config.prompt}\""
142+
end
143+
144+
# Build setup script
145+
setup_script = build_setup_script(worktree_path, claude_cmd)
146+
147+
# Launch Ghostty window
148+
puts "Launching Ghostty window for #{worktree_name}..."
149+
launch_ghostty(worktree_branch, setup_script)
150+
151+
puts ""
152+
153+
{ path: worktree_path, branch: worktree_branch }
154+
end
155+
156+
def build_setup_script(worktree_path, claude_cmd)
157+
base_commands = [
158+
"cd \"#{worktree_path}\"",
159+
"echo 'Setting up worktree environment...'",
160+
"echo 'Running mise trust...'",
161+
"mise trust"
162+
]
163+
164+
if config.setup_type == "full"
165+
base_commands += [
166+
"echo 'Running project setup...'",
167+
"(bin/parallel-agents/project_setup || true)"
168+
]
169+
end
170+
171+
base_commands += [
172+
"echo 'Launching Claude...'",
173+
claude_cmd
174+
]
175+
176+
base_commands.join(" && ")
177+
end
178+
179+
def launch_ghostty(title, setup_script)
180+
full_command = "#{setup_script}; exec bash"
181+
182+
# Use spawn to launch in background without blocking
183+
pid = spawn(
184+
"open", "-na", "Ghostty.app",
185+
"--args",
186+
"--title=#{title}",
187+
"-e", "bash", "-c", full_command
188+
)
189+
190+
Process.detach(pid)
191+
end
192+
193+
def print_summary(created_worktrees)
194+
puts "=" * 80
195+
puts "Created #{config.num_worktrees} worktree(s) with setup type '#{config.setup_type}':"
196+
puts ""
197+
198+
created_worktrees.each do |wt|
199+
puts " - #{wt[:path]} (branch: #{wt[:branch]})"
200+
end
201+
202+
puts ""
203+
puts "Each worktree has been launched in a new Ghostty window with the prompt:"
204+
puts " \"#{config.prompt}\""
205+
puts ""
206+
puts "Configuration:"
207+
puts " - Setup type: #{config.setup_type}"
208+
209+
if config.setup_type == "plan"
210+
puts " - mise trust only"
211+
else
212+
puts " - mise trust + bin/parallel-agents/project_setup"
213+
end
214+
215+
puts " - Ultrathink mode: #{config.ultra? ? 'yes' : 'no'}"
216+
puts " - A/B test mode: #{config.ab_test? ? 'yes' : 'no'}"
217+
218+
if config.ab_test?
219+
puts " - Creates 2 worktrees, one with ultrathink (-ultra suffix) and one without (-regular suffix)"
220+
end
221+
222+
puts ""
223+
puts "To view all active worktrees:"
224+
puts " git worktree list"
225+
puts ""
226+
puts "To clean up worktrees later, run:"
227+
puts " git worktree remove <worktree-path>"
228+
puts ""
229+
puts "Note: This command is always additive and never removes existing worktrees."
230+
puts "=" * 80
231+
end
232+
end
233+
234+
# Show help message
235+
def show_help
236+
puts <<~HELP
237+
Usage: worktree_run_ghostty [OPTIONS] [PROMPT]
238+
239+
Create git worktrees and launch Claude Code sessions in Ghostty windows.
240+
241+
OPTIONS:
242+
-n, --number <num> Number of worktrees to create (1-10) [default: 1]
243+
-s, --setup-type <type> Setup type: 'plan' or 'full' [default: plan]
244+
-b, --branch-name <name> Custom branch name prefix [default: current branch]
245+
-u, --ultra Enable ultrathink mode [default: disabled]
246+
-ab, --ab-test A/B test mode (creates 2 worktrees) [default: disabled]
247+
-h, --help Show this help message
248+
249+
SETUP TYPES:
250+
plan Minimal setup - runs mise trust only
251+
full Complete setup - runs mise trust + bin/parallel-agents/project_setup
252+
253+
EXAMPLES:
254+
# Create 1 worktree with plan setup and default prompt
255+
./bin/parallel-agents/worktree_run_ghostty
256+
257+
# Create 3 worktrees with full setup
258+
./bin/parallel-agents/worktree_run_ghostty -n 3 -s full "Fix the authentication bug"
259+
260+
# Create worktree with ultrathink enabled
261+
./bin/parallel-agents/worktree_run_ghostty -u "Refactor user model"
262+
263+
# A/B test mode - creates 2 worktrees (one regular, one with ultrathink)
264+
./bin/parallel-agents/worktree_run_ghostty -ab "write the shortest possible go hello world file"
265+
./bin/parallel-agents/worktree_run_ghostty --ab-test -s full "Implement new feature"
266+
267+
# Create worktrees with custom branch name
268+
./bin/parallel-agents/worktree_run_ghostty -b feature-x -n 2 "Add new feature"
269+
270+
NOTES:
271+
- Worktrees are created in parent directory with format: <repo>-<branch>-worktree-N[-suffix]
272+
- Worktrees are always additive (never removes existing ones)
273+
- In A/B test mode, -n and -u options are ignored
274+
- Use 'git worktree list' to view all active worktrees
275+
- Use 'git worktree remove <path>' to clean up worktrees
276+
277+
HELP
278+
end
279+
280+
# Main execution
281+
begin
282+
config = WorktreeConfig.new
283+
284+
parser = OptionParser.new do |opts|
285+
opts.on("-n", "--number NUM", Integer, "Number of worktrees (1-10)") do |n|
286+
config.num_worktrees = n
287+
end
288+
289+
opts.on("-s", "--setup-type TYPE", String, "Setup type: plan or full") do |type|
290+
config.setup_type = type
291+
end
292+
293+
opts.on("-b", "--branch-name NAME", String, "Custom branch name prefix") do |name|
294+
config.branch_name = name
295+
end
296+
297+
opts.on("-u", "--ultra", "Enable ultrathink mode") do
298+
config.ultra_mode = true
299+
end
300+
301+
opts.on("-ab", "--ab-test", "A/B test mode") do
302+
config.ab_test_mode = true
303+
end
304+
305+
opts.on("-h", "--help", "Show help") do
306+
show_help
307+
exit 0
308+
end
309+
end
310+
311+
parser.parse!
312+
313+
# Remaining arguments are the prompt
314+
config.prompt = ARGV.join(" ") unless ARGV.empty?
315+
316+
config.validate!
317+
318+
manager = WorktreeManager.new(config)
319+
manager.run
320+
321+
rescue => e
322+
puts "Error: #{e.message}"
323+
exit 1
324+
end

0 commit comments

Comments
 (0)