claude_swarm 0.3.5 → 0.3.7
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 +21 -0
- data/README.md +3 -0
- data/lib/claude_swarm/base_executor.rb +133 -0
- data/lib/claude_swarm/claude_code_executor.rb +19 -137
- data/lib/claude_swarm/claude_mcp_server.rb +2 -1
- data/lib/claude_swarm/cli.rb +1 -0
- data/lib/claude_swarm/mcp_generator.rb +3 -1
- data/lib/claude_swarm/openai/chat_completion.rb +15 -15
- data/lib/claude_swarm/openai/executor.rb +69 -161
- data/lib/claude_swarm/openai/responses.rb +27 -27
- data/lib/claude_swarm/orchestrator.rb +153 -171
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm.rb +1 -0
- metadata +16 -1
@@ -3,6 +3,9 @@
|
|
3
3
|
module ClaudeSwarm
|
4
4
|
class Orchestrator
|
5
5
|
include SystemUtils
|
6
|
+
|
7
|
+
attr_reader :config, :session_path, :session_log_path
|
8
|
+
|
6
9
|
RUN_DIR = File.expand_path("~/.claude-swarm/run")
|
7
10
|
["INT", "TERM", "QUIT"].each do |signal|
|
8
11
|
Signal.trap(signal) do
|
@@ -21,6 +24,7 @@ module ClaudeSwarm
|
|
21
24
|
@debug = debug
|
22
25
|
@restore_session_path = restore_session_path
|
23
26
|
@session_path = nil
|
27
|
+
@session_log_path = nil
|
24
28
|
@provided_session_id = session_id
|
25
29
|
# Store worktree option for later use
|
26
30
|
@worktree_option = worktree
|
@@ -40,24 +44,23 @@ module ClaudeSwarm
|
|
40
44
|
@start_time = Time.now
|
41
45
|
|
42
46
|
if @restore_session_path
|
43
|
-
|
47
|
+
non_interactive_output do
|
44
48
|
puts "🔄 Restoring Claude Swarm: #{@config.swarm_name}"
|
45
49
|
puts "😎 Vibe mode ON" if @vibe
|
46
|
-
puts
|
47
50
|
end
|
48
51
|
|
49
52
|
# Use existing session path
|
50
53
|
session_path = @restore_session_path
|
51
54
|
@session_path = session_path
|
55
|
+
@session_log_path = File.join(@session_path, "session.log")
|
52
56
|
ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
|
53
57
|
ENV["CLAUDE_SWARM_ROOT_DIR"] = ClaudeSwarm.root_dir
|
54
58
|
|
55
59
|
# Create run symlink for restored session
|
56
60
|
create_run_symlink
|
57
61
|
|
58
|
-
|
62
|
+
non_interactive_output do
|
59
63
|
puts "📝 Using existing session: #{session_path}/"
|
60
|
-
puts
|
61
64
|
end
|
62
65
|
|
63
66
|
# Initialize process tracker
|
@@ -68,25 +71,22 @@ module ClaudeSwarm
|
|
68
71
|
|
69
72
|
# Regenerate MCP configurations with session IDs for restoration
|
70
73
|
@generator.generate_all
|
71
|
-
|
74
|
+
non_interactive_output do
|
72
75
|
puts "✓ Regenerated MCP configurations with session IDs"
|
73
|
-
puts
|
74
76
|
end
|
75
77
|
else
|
76
|
-
|
78
|
+
non_interactive_output do
|
77
79
|
puts "🐝 Starting Claude Swarm: #{@config.swarm_name}"
|
78
80
|
puts "😎 Vibe mode ON" if @vibe
|
79
|
-
puts
|
80
81
|
end
|
81
82
|
|
82
83
|
# Generate and set session path for all instances
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
SessionPath.generate(working_dir: ClaudeSwarm.root_dir)
|
87
|
-
end
|
84
|
+
session_params = { working_dir: ClaudeSwarm.root_dir }
|
85
|
+
session_params[:session_id] = @provided_session_id if @provided_session_id
|
86
|
+
session_path = SessionPath.generate(**session_params)
|
88
87
|
SessionPath.ensure_directory(session_path)
|
89
88
|
@session_path = session_path
|
89
|
+
@session_log_path = File.join(@session_path, "session.log")
|
90
90
|
|
91
91
|
# Extract session ID from path (the timestamp part)
|
92
92
|
@session_id = File.basename(session_path)
|
@@ -97,9 +97,8 @@ module ClaudeSwarm
|
|
97
97
|
# Create run symlink for new session
|
98
98
|
create_run_symlink
|
99
99
|
|
100
|
-
|
100
|
+
non_interactive_output do
|
101
101
|
puts "📝 Session files will be saved to: #{session_path}/"
|
102
|
-
puts
|
103
102
|
end
|
104
103
|
|
105
104
|
# Initialize process tracker
|
@@ -109,7 +108,7 @@ module ClaudeSwarm
|
|
109
108
|
if @needs_worktree_manager
|
110
109
|
cli_option = @worktree_option.is_a?(String) && !@worktree_option.empty? ? @worktree_option : nil
|
111
110
|
@worktree_manager = WorktreeManager.new(cli_option, session_id: @session_id)
|
112
|
-
|
111
|
+
non_interactive_output { print("🌳 Setting up Git worktrees...") }
|
113
112
|
|
114
113
|
# Get all instances for worktree setup
|
115
114
|
# Note: instances.values already includes the main instance
|
@@ -117,17 +116,15 @@ module ClaudeSwarm
|
|
117
116
|
|
118
117
|
@worktree_manager.setup_worktrees(all_instances)
|
119
118
|
|
120
|
-
|
119
|
+
non_interactive_output do
|
121
120
|
puts "✓ Worktrees created with branch: #{@worktree_manager.worktree_name}"
|
122
|
-
puts
|
123
121
|
end
|
124
122
|
end
|
125
123
|
|
126
124
|
# Generate all MCP configuration files
|
127
125
|
@generator.generate_all
|
128
|
-
|
126
|
+
non_interactive_output do
|
129
127
|
puts "✓ Generated MCP configurations in session directory"
|
130
|
-
puts
|
131
128
|
end
|
132
129
|
|
133
130
|
# Save swarm config path for restoration
|
@@ -136,7 +133,7 @@ module ClaudeSwarm
|
|
136
133
|
|
137
134
|
# Launch the main instance (fetch after worktree setup to get modified paths)
|
138
135
|
main_instance = @config.main_instance_config
|
139
|
-
|
136
|
+
non_interactive_output do
|
140
137
|
puts "🚀 Launching main instance: #{@config.main_instance}"
|
141
138
|
puts " Model: #{main_instance[:model]}"
|
142
139
|
if main_instance[:directories].size == 1
|
@@ -149,13 +146,13 @@ module ClaudeSwarm
|
|
149
146
|
puts " Disallowed tools: #{main_instance[:disallowed_tools].join(", ")}" if main_instance[:disallowed_tools]&.any?
|
150
147
|
puts " Connections: #{main_instance[:connections].join(", ")}" if main_instance[:connections].any?
|
151
148
|
puts " 😎 Vibe mode ON for this instance" if main_instance[:vibe]
|
152
|
-
puts
|
153
149
|
end
|
154
150
|
|
155
151
|
command = build_main_command(main_instance)
|
156
|
-
if @debug
|
157
|
-
|
158
|
-
|
152
|
+
if @debug
|
153
|
+
non_interactive_output do
|
154
|
+
puts "🏃 Running: #{format_command_for_display(command)}"
|
155
|
+
end
|
159
156
|
end
|
160
157
|
|
161
158
|
# Start log streaming thread if in non-interactive mode with --stream-logs
|
@@ -171,23 +168,21 @@ module ClaudeSwarm
|
|
171
168
|
# Execute before commands if specified
|
172
169
|
before_commands = @config.before_commands
|
173
170
|
if before_commands.any? && !@restore_session_path
|
174
|
-
|
171
|
+
non_interactive_output do
|
175
172
|
puts "⚙️ Executing before commands..."
|
176
|
-
puts
|
177
173
|
end
|
178
174
|
|
179
175
|
success = execute_before_commands?(before_commands)
|
180
176
|
unless success
|
181
|
-
|
177
|
+
non_interactive_output { print("❌ Before commands failed. Aborting swarm launch.") }
|
182
178
|
cleanup_processes
|
183
179
|
cleanup_run_symlink
|
184
180
|
cleanup_worktrees
|
185
181
|
exit(1)
|
186
182
|
end
|
187
183
|
|
188
|
-
|
184
|
+
non_interactive_output do
|
189
185
|
puts "✓ Before commands completed successfully"
|
190
|
-
puts
|
191
186
|
end
|
192
187
|
end
|
193
188
|
|
@@ -195,7 +190,11 @@ module ClaudeSwarm
|
|
195
190
|
# This ensures the main instance runs in a clean environment without inheriting
|
196
191
|
# Claude Swarm's BUNDLE_* environment variables
|
197
192
|
Bundler.with_unbundled_env do
|
198
|
-
|
193
|
+
if @non_interactive_prompt
|
194
|
+
stream_to_session_log(*command)
|
195
|
+
else
|
196
|
+
system!(*command)
|
197
|
+
end
|
199
198
|
end
|
200
199
|
end
|
201
200
|
|
@@ -212,16 +211,15 @@ module ClaudeSwarm
|
|
212
211
|
after_commands = @config.after_commands
|
213
212
|
if after_commands.any? && !@restore_session_path
|
214
213
|
Dir.chdir(main_instance[:directory]) do
|
215
|
-
|
216
|
-
|
217
|
-
puts "⚙️ Executing after commands..."
|
218
|
-
puts
|
214
|
+
non_interactive_output do
|
215
|
+
print("⚙️ Executing after commands...")
|
219
216
|
end
|
220
217
|
|
221
218
|
success = execute_after_commands?(after_commands)
|
222
|
-
|
223
|
-
|
224
|
-
|
219
|
+
unless success
|
220
|
+
non_interactive_output do
|
221
|
+
puts "⚠️ Some after commands failed"
|
222
|
+
end
|
225
223
|
end
|
226
224
|
end
|
227
225
|
end
|
@@ -234,115 +232,19 @@ module ClaudeSwarm
|
|
234
232
|
|
235
233
|
private
|
236
234
|
|
237
|
-
def
|
238
|
-
|
239
|
-
|
240
|
-
commands.each_with_index do |command, index|
|
241
|
-
# Log the command execution to session log
|
242
|
-
if @session_path
|
243
|
-
File.open(log_file, "a") do |f|
|
244
|
-
f.puts "[#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}] Executing before command #{index + 1}/#{commands.size}: #{command}"
|
245
|
-
end
|
246
|
-
end
|
247
|
-
|
248
|
-
# Execute the command and capture output
|
249
|
-
begin
|
250
|
-
puts "Debug: Executing command #{index + 1}/#{commands.size}: #{command}" if @debug && !@non_interactive_prompt
|
251
|
-
|
252
|
-
# Use system with output capture
|
253
|
-
output = %x(#{command} 2>&1)
|
254
|
-
success = $CHILD_STATUS.success?
|
255
|
-
|
256
|
-
# Log the output
|
257
|
-
if @session_path
|
258
|
-
File.open(log_file, "a") do |f|
|
259
|
-
f.puts "Command output:"
|
260
|
-
f.puts output
|
261
|
-
f.puts "Exit status: #{$CHILD_STATUS.exitstatus}"
|
262
|
-
f.puts "-" * 80
|
263
|
-
end
|
264
|
-
end
|
265
|
-
|
266
|
-
# Show output if in debug mode or if command failed
|
267
|
-
if (@debug || !success) && !@non_interactive_prompt
|
268
|
-
puts "Command #{index + 1} output:"
|
269
|
-
puts output
|
270
|
-
puts "Exit status: #{$CHILD_STATUS.exitstatus}"
|
271
|
-
end
|
235
|
+
def non_interactive_output
|
236
|
+
return if @non_interactive_prompt
|
272
237
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
end
|
277
|
-
rescue StandardError => e
|
278
|
-
puts "Error executing before command #{index + 1}: #{e.message}" unless @non_interactive_prompt
|
279
|
-
if @session_path
|
280
|
-
File.open(log_file, "a") do |f|
|
281
|
-
f.puts "Error: #{e.message}"
|
282
|
-
f.puts "-" * 80
|
283
|
-
end
|
284
|
-
end
|
285
|
-
return false
|
286
|
-
end
|
287
|
-
end
|
238
|
+
yield
|
239
|
+
puts
|
240
|
+
end
|
288
241
|
|
289
|
-
|
242
|
+
def execute_before_commands?(commands)
|
243
|
+
execute_commands(commands, phase: "before", fail_fast: true)
|
290
244
|
end
|
291
245
|
|
292
246
|
def execute_after_commands?(commands)
|
293
|
-
|
294
|
-
all_succeeded = true
|
295
|
-
|
296
|
-
commands.each_with_index do |command, index|
|
297
|
-
# Log the command execution to session log
|
298
|
-
if @session_path
|
299
|
-
File.open(log_file, "a") do |f|
|
300
|
-
f.puts "[#{Time.now.strftime("%Y-%m-%d %H:%M:%S")}] Executing after command #{index + 1}/#{commands.size}: #{command}"
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
# Execute the command and capture output
|
305
|
-
begin
|
306
|
-
puts "Debug: Executing after command #{index + 1}/#{commands.size}: #{command}" if @debug && !@non_interactive_prompt
|
307
|
-
|
308
|
-
# Use system with output capture
|
309
|
-
output = %x(#{command} 2>&1)
|
310
|
-
success = $CHILD_STATUS.success?
|
311
|
-
|
312
|
-
# Log the output
|
313
|
-
if @session_path
|
314
|
-
File.open(log_file, "a") do |f|
|
315
|
-
f.puts "Command output:"
|
316
|
-
f.puts output
|
317
|
-
f.puts "Exit status: #{$CHILD_STATUS.exitstatus}"
|
318
|
-
f.puts "-" * 80
|
319
|
-
end
|
320
|
-
end
|
321
|
-
|
322
|
-
# Show output if in debug mode or if command failed
|
323
|
-
if (@debug || !success) && !@non_interactive_prompt
|
324
|
-
puts "After command #{index + 1} output:"
|
325
|
-
puts output
|
326
|
-
puts "Exit status: #{$CHILD_STATUS.exitstatus}"
|
327
|
-
end
|
328
|
-
|
329
|
-
unless success
|
330
|
-
puts "❌ After command #{index + 1} failed: #{command}" unless @non_interactive_prompt
|
331
|
-
all_succeeded = false
|
332
|
-
end
|
333
|
-
rescue StandardError => e
|
334
|
-
puts "Error executing after command #{index + 1}: #{e.message}" unless @non_interactive_prompt
|
335
|
-
if @session_path
|
336
|
-
File.open(log_file, "a") do |f|
|
337
|
-
f.puts "Error: #{e.message}"
|
338
|
-
f.puts "-" * 80
|
339
|
-
end
|
340
|
-
end
|
341
|
-
all_succeeded = false
|
342
|
-
end
|
343
|
-
end
|
344
|
-
|
345
|
-
all_succeeded
|
247
|
+
execute_commands(commands, phase: "after", fail_fast: false)
|
346
248
|
end
|
347
249
|
|
348
250
|
def save_swarm_config_path(session_path)
|
@@ -355,19 +257,21 @@ module ClaudeSwarm
|
|
355
257
|
File.write(root_dir_file, ClaudeSwarm.root_dir)
|
356
258
|
|
357
259
|
# Save session metadata
|
358
|
-
|
260
|
+
metadata_file = File.join(session_path, "session_metadata.json")
|
261
|
+
File.write(metadata_file, JSON.pretty_generate(build_session_metadata))
|
262
|
+
end
|
263
|
+
|
264
|
+
def build_session_metadata
|
265
|
+
{
|
359
266
|
"root_directory" => ClaudeSwarm.root_dir,
|
360
267
|
"timestamp" => Time.now.utc.iso8601,
|
361
268
|
"start_time" => @start_time.utc.iso8601,
|
362
269
|
"swarm_name" => @config.swarm_name,
|
363
270
|
"claude_swarm_version" => VERSION,
|
364
|
-
}
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
metadata_file = File.join(session_path, "session_metadata.json")
|
370
|
-
File.write(metadata_file, JSON.pretty_generate(metadata))
|
271
|
+
}.tap do |metadata|
|
272
|
+
# Add worktree info if applicable
|
273
|
+
metadata["worktree"] = @worktree_manager.session_metadata if @worktree_manager
|
274
|
+
end
|
371
275
|
end
|
372
276
|
|
373
277
|
def cleanup_processes
|
@@ -378,9 +282,7 @@ module ClaudeSwarm
|
|
378
282
|
end
|
379
283
|
|
380
284
|
def cleanup_worktrees
|
381
|
-
|
382
|
-
|
383
|
-
@worktree_manager.cleanup_worktrees
|
285
|
+
@worktree_manager&.cleanup_worktrees
|
384
286
|
rescue StandardError => e
|
385
287
|
puts "⚠️ Error during worktree cleanup: #{e.message}"
|
386
288
|
end
|
@@ -417,7 +319,7 @@ module ClaudeSwarm
|
|
417
319
|
|
418
320
|
File.write(metadata_file, JSON.pretty_generate(metadata))
|
419
321
|
rescue StandardError => e
|
420
|
-
|
322
|
+
non_interactive_output { print("⚠️ Error updating session metadata: #{e.message}") }
|
421
323
|
end
|
422
324
|
|
423
325
|
def calculate_total_cost
|
@@ -466,7 +368,7 @@ module ClaudeSwarm
|
|
466
368
|
File.symlink(@session_path, symlink_path)
|
467
369
|
rescue StandardError => e
|
468
370
|
# Don't fail the process if symlink creation fails
|
469
|
-
|
371
|
+
non_interactive_output { print("⚠️ Warning: Could not create run symlink: #{e.message}") }
|
470
372
|
end
|
471
373
|
|
472
374
|
def cleanup_run_symlink
|
@@ -481,15 +383,11 @@ module ClaudeSwarm
|
|
481
383
|
|
482
384
|
def start_log_streaming
|
483
385
|
Thread.new do
|
484
|
-
session_log_path = File.join(ENV.fetch("CLAUDE_SWARM_SESSION_PATH", nil), "session.log")
|
485
|
-
|
486
386
|
# Wait for log file to be created
|
487
|
-
sleep(0.1) until File.exist?(session_log_path)
|
387
|
+
sleep(0.1) until File.exist?(@session_log_path)
|
488
388
|
|
489
389
|
# Open file and seek to end
|
490
|
-
File.open(session_log_path, "r") do |file|
|
491
|
-
file.seek(0, IO::SEEK_END)
|
492
|
-
|
390
|
+
File.open(@session_log_path, "r") do |file|
|
493
391
|
loop do
|
494
392
|
changes = file.read
|
495
393
|
if changes
|
@@ -516,11 +414,13 @@ module ClaudeSwarm
|
|
516
414
|
end
|
517
415
|
|
518
416
|
def build_main_command(instance)
|
519
|
-
parts = [
|
520
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
417
|
+
parts = ["claude"]
|
418
|
+
|
419
|
+
# Only add --model if ANTHROPIC_MODEL env var is not set
|
420
|
+
unless ENV["ANTHROPIC_MODEL"]
|
421
|
+
parts << "--model"
|
422
|
+
parts << instance[:model]
|
423
|
+
end
|
524
424
|
|
525
425
|
# Add resume flag if restoring session
|
526
426
|
if @restore_session_path
|
@@ -593,6 +493,8 @@ module ClaudeSwarm
|
|
593
493
|
# Non-interactive mode with -p
|
594
494
|
parts << "-p"
|
595
495
|
parts << @non_interactive_prompt
|
496
|
+
parts << "--verbose"
|
497
|
+
parts << "--output-format=stream-json"
|
596
498
|
elsif @interactive_prompt
|
597
499
|
# Interactive mode with initial prompt (no -p flag)
|
598
500
|
parts << @interactive_prompt
|
@@ -610,9 +512,8 @@ module ClaudeSwarm
|
|
610
512
|
worktree_data = metadata["worktree"]
|
611
513
|
return unless worktree_data && worktree_data["enabled"]
|
612
514
|
|
613
|
-
|
515
|
+
non_interactive_output do
|
614
516
|
puts "🌳 Restoring Git worktrees..."
|
615
|
-
puts
|
616
517
|
end
|
617
518
|
|
618
519
|
# Restore worktrees using the saved configuration
|
@@ -624,10 +525,91 @@ module ClaudeSwarm
|
|
624
525
|
all_instances = @config.instances.values
|
625
526
|
@worktree_manager.setup_worktrees(all_instances)
|
626
527
|
|
627
|
-
|
528
|
+
non_interactive_output do
|
529
|
+
puts "✓ Worktrees restored with branch: #{@worktree_manager.worktree_name}"
|
530
|
+
end
|
531
|
+
end
|
628
532
|
|
629
|
-
|
630
|
-
|
533
|
+
def stream_to_session_log(*command)
|
534
|
+
# Setup logger for session logging
|
535
|
+
logger = Logger.new(@session_log_path, level: :info, progname: @config.main_instance)
|
536
|
+
|
537
|
+
# Use Open3.popen2e to capture stdout and stderr merged for formatting
|
538
|
+
Open3.popen2e(*command) do |stdin, stdout_and_stderr, wait_thr|
|
539
|
+
stdin.close
|
540
|
+
|
541
|
+
# Read and process the merged output
|
542
|
+
stdout_and_stderr.each_line do |line|
|
543
|
+
# Try to parse and prettify JSON lines
|
544
|
+
|
545
|
+
json_data = JSON.parse(line.chomp)
|
546
|
+
pretty_json = JSON.pretty_generate(json_data)
|
547
|
+
logger.info { pretty_json }
|
548
|
+
rescue JSON::ParserError
|
549
|
+
# Warn about non-JSON output since we expect stream-json format
|
550
|
+
warn("⚠️ Warning: Non-JSON output detected in stream-json mode: #{line.chomp}")
|
551
|
+
# Log the line as-is
|
552
|
+
logger.info { line.chomp }
|
553
|
+
end
|
554
|
+
|
555
|
+
wait_thr.value
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
def execute_commands(commands, phase:, fail_fast:)
|
560
|
+
all_succeeded = true
|
561
|
+
|
562
|
+
# Setup logger for session logging if we have a session path
|
563
|
+
logger = Logger.new(@session_log_path, level: :info)
|
564
|
+
|
565
|
+
commands.each_with_index do |command, index|
|
566
|
+
# Log the command execution to session log
|
567
|
+
logger.info { "Executing #{phase} command #{index + 1}/#{commands.size}: #{command}" }
|
568
|
+
|
569
|
+
# Execute the command and capture output
|
570
|
+
begin
|
571
|
+
if @debug
|
572
|
+
non_interactive_output do
|
573
|
+
debug_prefix = phase == "after" ? "after " : ""
|
574
|
+
print("Debug: Executing #{debug_prefix} command #{index + 1}/#{commands.size}: #{format_command_for_display(command)}")
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
output = %x(#{command} 2>&1)
|
579
|
+
success = $CHILD_STATUS.success?
|
580
|
+
output_separator = "-" * 80
|
581
|
+
|
582
|
+
logger.info { "Command output:" }
|
583
|
+
logger.info { output }
|
584
|
+
logger.info { "Exit status: #{$CHILD_STATUS.exitstatus}" }
|
585
|
+
logger.info { output_separator }
|
586
|
+
|
587
|
+
# Show output if in debug mode or if command failed
|
588
|
+
if @debug || !success
|
589
|
+
non_interactive_output do
|
590
|
+
output_prefix = phase == "after" ? "After command" : "Command"
|
591
|
+
puts "#{output_prefix} #{index + 1} output:"
|
592
|
+
puts output
|
593
|
+
print("Exit status: #{$CHILD_STATUS.exitstatus}")
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
unless success
|
598
|
+
error_prefix = phase.capitalize
|
599
|
+
non_interactive_output { print("❌ #{error_prefix} command #{index + 1} failed: #{command}") }
|
600
|
+
all_succeeded = false
|
601
|
+
return false if fail_fast
|
602
|
+
end
|
603
|
+
rescue StandardError => e
|
604
|
+
non_interactive_output { print("Error executing #{phase} command #{index + 1}: #{e.message}") }
|
605
|
+
logger.info { "Error: #{e.message}" }
|
606
|
+
logger.info { output_separator }
|
607
|
+
all_succeeded = false
|
608
|
+
return false if fail_fast
|
609
|
+
end
|
610
|
+
end
|
611
|
+
|
612
|
+
all_succeeded
|
631
613
|
end
|
632
614
|
end
|
633
615
|
end
|
data/lib/claude_swarm/version.rb
CHANGED
data/lib/claude_swarm.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: claude_swarm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paulo Arruda
|
@@ -51,6 +51,20 @@ dependencies:
|
|
51
51
|
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '0.1'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: faraday-retry
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '2.0'
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '2.0'
|
54
68
|
- !ruby/object:Gem::Dependency
|
55
69
|
name: fast-mcp-annotations
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
@@ -133,6 +147,7 @@ files:
|
|
133
147
|
- examples/with-before-commands.yml
|
134
148
|
- exe/claude-swarm
|
135
149
|
- lib/claude_swarm.rb
|
150
|
+
- lib/claude_swarm/base_executor.rb
|
136
151
|
- lib/claude_swarm/claude_code_executor.rb
|
137
152
|
- lib/claude_swarm/claude_mcp_server.rb
|
138
153
|
- lib/claude_swarm/cli.rb
|