claude_swarm 0.2.1 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +35 -0
- data/CLAUDE.md +2 -7
- data/README.md +5 -1
- data/lib/claude_swarm/claude_code_executor.rb +35 -30
- data/lib/claude_swarm/cli.rb +28 -6
- data/lib/claude_swarm/commands/ps.rb +4 -4
- data/lib/claude_swarm/commands/show.rb +3 -3
- data/lib/claude_swarm/mcp_generator.rb +46 -1
- data/lib/claude_swarm/openai/executor.rb +20 -13
- data/lib/claude_swarm/orchestrator.rb +58 -45
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm.rb +6 -0
- data/team.yml +213 -300
- metadata +1 -1
@@ -5,12 +5,13 @@ module ClaudeSwarm
|
|
5
5
|
include SystemUtils
|
6
6
|
RUN_DIR = File.expand_path("~/.claude-swarm/run")
|
7
7
|
|
8
|
-
def initialize(configuration, mcp_generator, vibe: false, prompt: nil, stream_logs: false, debug: false,
|
8
|
+
def initialize(configuration, mcp_generator, vibe: false, prompt: nil, interactive_prompt: nil, stream_logs: false, debug: false,
|
9
9
|
restore_session_path: nil, worktree: nil, session_id: nil)
|
10
10
|
@config = configuration
|
11
11
|
@generator = mcp_generator
|
12
12
|
@vibe = vibe
|
13
|
-
@
|
13
|
+
@non_interactive_prompt = prompt
|
14
|
+
@interactive_prompt = interactive_prompt
|
14
15
|
@stream_logs = stream_logs
|
15
16
|
@debug = debug
|
16
17
|
@restore_session_path = restore_session_path
|
@@ -26,7 +27,7 @@ module ClaudeSwarm
|
|
26
27
|
@start_time = nil
|
27
28
|
|
28
29
|
# Set environment variable for prompt mode to suppress output
|
29
|
-
ENV["CLAUDE_SWARM_PROMPT"] = "1" if @
|
30
|
+
ENV["CLAUDE_SWARM_PROMPT"] = "1" if @non_interactive_prompt
|
30
31
|
end
|
31
32
|
|
32
33
|
def start
|
@@ -34,7 +35,7 @@ module ClaudeSwarm
|
|
34
35
|
@start_time = Time.now
|
35
36
|
|
36
37
|
if @restore_session_path
|
37
|
-
unless @
|
38
|
+
unless @non_interactive_prompt
|
38
39
|
puts "🔄 Restoring Claude Swarm: #{@config.swarm_name}"
|
39
40
|
puts "😎 Vibe mode ON" if @vibe
|
40
41
|
puts
|
@@ -44,12 +45,12 @@ module ClaudeSwarm
|
|
44
45
|
session_path = @restore_session_path
|
45
46
|
@session_path = session_path
|
46
47
|
ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
|
47
|
-
ENV["
|
48
|
+
ENV["CLAUDE_SWARM_ROOT_DIR"] = ClaudeSwarm.root_dir
|
48
49
|
|
49
50
|
# Create run symlink for restored session
|
50
51
|
create_run_symlink
|
51
52
|
|
52
|
-
unless @
|
53
|
+
unless @non_interactive_prompt
|
53
54
|
puts "📝 Using existing session: #{session_path}/"
|
54
55
|
puts
|
55
56
|
end
|
@@ -65,12 +66,12 @@ module ClaudeSwarm
|
|
65
66
|
|
66
67
|
# Regenerate MCP configurations with session IDs for restoration
|
67
68
|
@generator.generate_all
|
68
|
-
unless @
|
69
|
+
unless @non_interactive_prompt
|
69
70
|
puts "✓ Regenerated MCP configurations with session IDs"
|
70
71
|
puts
|
71
72
|
end
|
72
73
|
else
|
73
|
-
unless @
|
74
|
+
unless @non_interactive_prompt
|
74
75
|
puts "🐝 Starting Claude Swarm: #{@config.swarm_name}"
|
75
76
|
puts "😎 Vibe mode ON" if @vibe
|
76
77
|
puts
|
@@ -78,9 +79,9 @@ module ClaudeSwarm
|
|
78
79
|
|
79
80
|
# Generate and set session path for all instances
|
80
81
|
session_path = if @provided_session_id
|
81
|
-
SessionPath.generate(working_dir:
|
82
|
+
SessionPath.generate(working_dir: ClaudeSwarm.root_dir, session_id: @provided_session_id)
|
82
83
|
else
|
83
|
-
SessionPath.generate(working_dir:
|
84
|
+
SessionPath.generate(working_dir: ClaudeSwarm.root_dir)
|
84
85
|
end
|
85
86
|
SessionPath.ensure_directory(session_path)
|
86
87
|
@session_path = session_path
|
@@ -89,12 +90,12 @@ module ClaudeSwarm
|
|
89
90
|
@session_id = File.basename(session_path)
|
90
91
|
|
91
92
|
ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
|
92
|
-
ENV["
|
93
|
+
ENV["CLAUDE_SWARM_ROOT_DIR"] = ClaudeSwarm.root_dir
|
93
94
|
|
94
95
|
# Create run symlink for new session
|
95
96
|
create_run_symlink
|
96
97
|
|
97
|
-
unless @
|
98
|
+
unless @non_interactive_prompt
|
98
99
|
puts "📝 Session files will be saved to: #{session_path}/"
|
99
100
|
puts
|
100
101
|
end
|
@@ -109,7 +110,7 @@ module ClaudeSwarm
|
|
109
110
|
if @needs_worktree_manager
|
110
111
|
cli_option = @worktree_option.is_a?(String) && !@worktree_option.empty? ? @worktree_option : nil
|
111
112
|
@worktree_manager = WorktreeManager.new(cli_option, session_id: @session_id)
|
112
|
-
puts "🌳 Setting up Git worktrees..." unless @
|
113
|
+
puts "🌳 Setting up Git worktrees..." unless @non_interactive_prompt
|
113
114
|
|
114
115
|
# Get all instances for worktree setup
|
115
116
|
# Note: instances.values already includes the main instance
|
@@ -117,7 +118,7 @@ module ClaudeSwarm
|
|
117
118
|
|
118
119
|
@worktree_manager.setup_worktrees(all_instances)
|
119
120
|
|
120
|
-
unless @
|
121
|
+
unless @non_interactive_prompt
|
121
122
|
puts "✓ Worktrees created with branch: #{@worktree_manager.worktree_name}"
|
122
123
|
puts
|
123
124
|
end
|
@@ -125,7 +126,7 @@ module ClaudeSwarm
|
|
125
126
|
|
126
127
|
# Generate all MCP configuration files
|
127
128
|
@generator.generate_all
|
128
|
-
unless @
|
129
|
+
unless @non_interactive_prompt
|
129
130
|
puts "✓ Generated MCP configurations in session directory"
|
130
131
|
puts
|
131
132
|
end
|
@@ -136,7 +137,7 @@ module ClaudeSwarm
|
|
136
137
|
|
137
138
|
# Launch the main instance (fetch after worktree setup to get modified paths)
|
138
139
|
main_instance = @config.main_instance_config
|
139
|
-
unless @
|
140
|
+
unless @non_interactive_prompt
|
140
141
|
puts "🚀 Launching main instance: #{@config.main_instance}"
|
141
142
|
puts " Model: #{main_instance[:model]}"
|
142
143
|
if main_instance[:directories].size == 1
|
@@ -153,14 +154,14 @@ module ClaudeSwarm
|
|
153
154
|
end
|
154
155
|
|
155
156
|
command = build_main_command(main_instance)
|
156
|
-
if @debug && !@
|
157
|
+
if @debug && !@non_interactive_prompt
|
157
158
|
puts "🏃 Running: #{format_command_for_display(command)}"
|
158
159
|
puts
|
159
160
|
end
|
160
161
|
|
161
162
|
# Start log streaming thread if in non-interactive mode with --stream-logs
|
162
163
|
log_thread = nil
|
163
|
-
log_thread = start_log_streaming if @
|
164
|
+
log_thread = start_log_streaming if @non_interactive_prompt && @stream_logs
|
164
165
|
|
165
166
|
# Write the current process PID (orchestrator) to a file for easy access
|
166
167
|
main_pid_file = File.join(@session_path, "main_pid")
|
@@ -171,27 +172,32 @@ module ClaudeSwarm
|
|
171
172
|
# Execute before commands if specified
|
172
173
|
before_commands = @config.before_commands
|
173
174
|
if before_commands.any? && !@restore_session_path
|
174
|
-
unless @
|
175
|
+
unless @non_interactive_prompt
|
175
176
|
puts "⚙️ Executing before commands..."
|
176
177
|
puts
|
177
178
|
end
|
178
179
|
|
179
180
|
success = execute_before_commands?(before_commands)
|
180
181
|
unless success
|
181
|
-
puts "❌ Before commands failed. Aborting swarm launch." unless @
|
182
|
+
puts "❌ Before commands failed. Aborting swarm launch." unless @non_interactive_prompt
|
182
183
|
cleanup_processes
|
183
184
|
cleanup_run_symlink
|
184
185
|
cleanup_worktrees
|
185
186
|
exit(1)
|
186
187
|
end
|
187
188
|
|
188
|
-
unless @
|
189
|
+
unless @non_interactive_prompt
|
189
190
|
puts "✓ Before commands completed successfully"
|
190
191
|
puts
|
191
192
|
end
|
192
193
|
end
|
193
194
|
|
194
|
-
|
195
|
+
# Execute main Claude instance with unbundled environment to avoid bundler conflicts
|
196
|
+
# This ensures the main instance runs in a clean environment without inheriting
|
197
|
+
# Claude Swarm's BUNDLE_* environment variables
|
198
|
+
Bundler.with_unbundled_env do
|
199
|
+
system!(*command)
|
200
|
+
end
|
195
201
|
end
|
196
202
|
|
197
203
|
# Clean up log streaming thread
|
@@ -207,14 +213,14 @@ module ClaudeSwarm
|
|
207
213
|
after_commands = @config.after_commands
|
208
214
|
if after_commands.any? && !@restore_session_path
|
209
215
|
Dir.chdir(main_instance[:directory]) do
|
210
|
-
unless @
|
216
|
+
unless @non_interactive_prompt
|
211
217
|
puts
|
212
218
|
puts "⚙️ Executing after commands..."
|
213
219
|
puts
|
214
220
|
end
|
215
221
|
|
216
222
|
success = execute_after_commands?(after_commands)
|
217
|
-
if !success && !@
|
223
|
+
if !success && !@non_interactive_prompt
|
218
224
|
puts "⚠️ Some after commands failed"
|
219
225
|
puts
|
220
226
|
end
|
@@ -242,7 +248,7 @@ module ClaudeSwarm
|
|
242
248
|
|
243
249
|
# Execute the command and capture output
|
244
250
|
begin
|
245
|
-
puts "Debug: Executing command #{index + 1}/#{commands.size}: #{command}" if @debug && !@
|
251
|
+
puts "Debug: Executing command #{index + 1}/#{commands.size}: #{command}" if @debug && !@non_interactive_prompt
|
246
252
|
|
247
253
|
# Use system with output capture
|
248
254
|
output = %x(#{command} 2>&1)
|
@@ -259,18 +265,18 @@ module ClaudeSwarm
|
|
259
265
|
end
|
260
266
|
|
261
267
|
# Show output if in debug mode or if command failed
|
262
|
-
if (@debug || !success) && !@
|
268
|
+
if (@debug || !success) && !@non_interactive_prompt
|
263
269
|
puts "Command #{index + 1} output:"
|
264
270
|
puts output
|
265
271
|
puts "Exit status: #{$CHILD_STATUS.exitstatus}"
|
266
272
|
end
|
267
273
|
|
268
274
|
unless success
|
269
|
-
puts "❌ Before command #{index + 1} failed: #{command}" unless @
|
275
|
+
puts "❌ Before command #{index + 1} failed: #{command}" unless @non_interactive_prompt
|
270
276
|
return false
|
271
277
|
end
|
272
278
|
rescue StandardError => e
|
273
|
-
puts "Error executing before command #{index + 1}: #{e.message}" unless @
|
279
|
+
puts "Error executing before command #{index + 1}: #{e.message}" unless @non_interactive_prompt
|
274
280
|
if @session_path
|
275
281
|
File.open(log_file, "a") do |f|
|
276
282
|
f.puts "Error: #{e.message}"
|
@@ -298,7 +304,7 @@ module ClaudeSwarm
|
|
298
304
|
|
299
305
|
# Execute the command and capture output
|
300
306
|
begin
|
301
|
-
puts "Debug: Executing after command #{index + 1}/#{commands.size}: #{command}" if @debug && !@
|
307
|
+
puts "Debug: Executing after command #{index + 1}/#{commands.size}: #{command}" if @debug && !@non_interactive_prompt
|
302
308
|
|
303
309
|
# Use system with output capture
|
304
310
|
output = %x(#{command} 2>&1)
|
@@ -315,18 +321,18 @@ module ClaudeSwarm
|
|
315
321
|
end
|
316
322
|
|
317
323
|
# Show output if in debug mode or if command failed
|
318
|
-
if (@debug || !success) && !@
|
324
|
+
if (@debug || !success) && !@non_interactive_prompt
|
319
325
|
puts "After command #{index + 1} output:"
|
320
326
|
puts output
|
321
327
|
puts "Exit status: #{$CHILD_STATUS.exitstatus}"
|
322
328
|
end
|
323
329
|
|
324
330
|
unless success
|
325
|
-
puts "❌ After command #{index + 1} failed: #{command}" unless @
|
331
|
+
puts "❌ After command #{index + 1} failed: #{command}" unless @non_interactive_prompt
|
326
332
|
all_succeeded = false
|
327
333
|
end
|
328
334
|
rescue StandardError => e
|
329
|
-
puts "Error executing after command #{index + 1}: #{e.message}" unless @
|
335
|
+
puts "Error executing after command #{index + 1}: #{e.message}" unless @non_interactive_prompt
|
330
336
|
if @session_path
|
331
337
|
File.open(log_file, "a") do |f|
|
332
338
|
f.puts "Error: #{e.message}"
|
@@ -345,13 +351,13 @@ module ClaudeSwarm
|
|
345
351
|
config_copy_path = File.join(session_path, "config.yml")
|
346
352
|
FileUtils.cp(@config.config_path, config_copy_path)
|
347
353
|
|
348
|
-
# Save the
|
349
|
-
|
350
|
-
File.write(
|
354
|
+
# Save the root directory
|
355
|
+
root_dir_file = File.join(session_path, "root_directory")
|
356
|
+
File.write(root_dir_file, ClaudeSwarm.root_dir)
|
351
357
|
|
352
358
|
# Save session metadata
|
353
359
|
metadata = {
|
354
|
-
"
|
360
|
+
"root_directory" => ClaudeSwarm.root_dir,
|
355
361
|
"timestamp" => Time.now.utc.iso8601,
|
356
362
|
"start_time" => @start_time.utc.iso8601,
|
357
363
|
"swarm_name" => @config.swarm_name,
|
@@ -374,7 +380,7 @@ module ClaudeSwarm
|
|
374
380
|
# Execute after commands if configured
|
375
381
|
main_instance = @config.main_instance_config
|
376
382
|
after_commands = @config.after_commands
|
377
|
-
if after_commands.any? && !@restore_session_path && !@
|
383
|
+
if after_commands.any? && !@restore_session_path && !@non_interactive_prompt
|
378
384
|
Dir.chdir(main_instance[:directory]) do
|
379
385
|
puts
|
380
386
|
puts "⚙️ Executing after commands..."
|
@@ -438,7 +444,7 @@ module ClaudeSwarm
|
|
438
444
|
|
439
445
|
File.write(metadata_file, JSON.pretty_generate(metadata))
|
440
446
|
rescue StandardError => e
|
441
|
-
puts "⚠️ Error updating session metadata: #{e.message}" unless @
|
447
|
+
puts "⚠️ Error updating session metadata: #{e.message}" unless @non_interactive_prompt
|
442
448
|
end
|
443
449
|
|
444
450
|
def calculate_total_cost
|
@@ -487,7 +493,7 @@ module ClaudeSwarm
|
|
487
493
|
File.symlink(@session_path, symlink_path)
|
488
494
|
rescue StandardError => e
|
489
495
|
# Don't fail the process if symlink creation fails
|
490
|
-
puts "⚠️ Warning: Could not create run symlink: #{e.message}" unless @
|
496
|
+
puts "⚠️ Warning: Could not create run symlink: #{e.message}" unless @non_interactive_prompt
|
491
497
|
end
|
492
498
|
|
493
499
|
def cleanup_run_symlink
|
@@ -589,6 +595,7 @@ module ClaudeSwarm
|
|
589
595
|
end
|
590
596
|
end
|
591
597
|
|
598
|
+
# Always add instance prompt if it exists
|
592
599
|
if instance[:prompt]
|
593
600
|
parts << "--append-system-prompt"
|
594
601
|
parts << instance[:prompt]
|
@@ -608,12 +615,18 @@ module ClaudeSwarm
|
|
608
615
|
parts << "--mcp-config"
|
609
616
|
parts << mcp_config_path
|
610
617
|
|
611
|
-
|
618
|
+
# Handle different modes
|
619
|
+
if @non_interactive_prompt
|
620
|
+
# Non-interactive mode with -p
|
612
621
|
parts << "-p"
|
613
|
-
parts << @
|
614
|
-
|
615
|
-
|
622
|
+
parts << @non_interactive_prompt
|
623
|
+
elsif @interactive_prompt
|
624
|
+
# Interactive mode with initial prompt (no -p flag)
|
625
|
+
parts << @interactive_prompt
|
616
626
|
end
|
627
|
+
# else: Interactive mode without initial prompt - nothing to add
|
628
|
+
|
629
|
+
parts
|
617
630
|
end
|
618
631
|
|
619
632
|
def restore_worktrees_if_needed(session_path)
|
@@ -624,7 +637,7 @@ module ClaudeSwarm
|
|
624
637
|
worktree_data = metadata["worktree"]
|
625
638
|
return unless worktree_data && worktree_data["enabled"]
|
626
639
|
|
627
|
-
unless @
|
640
|
+
unless @non_interactive_prompt
|
628
641
|
puts "🌳 Restoring Git worktrees..."
|
629
642
|
puts
|
630
643
|
end
|
@@ -638,7 +651,7 @@ module ClaudeSwarm
|
|
638
651
|
all_instances = @config.instances.values
|
639
652
|
@worktree_manager.setup_worktrees(all_instances)
|
640
653
|
|
641
|
-
return if @
|
654
|
+
return if @non_interactive_prompt
|
642
655
|
|
643
656
|
puts "✓ Worktrees restored with branch: #{@worktree_manager.worktree_name}"
|
644
657
|
puts
|
data/lib/claude_swarm/version.rb
CHANGED