claude_swarm 0.3.6 → 0.3.8
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 +26 -0
- data/CLAUDE.md +50 -3
- data/README.md +64 -0
- data/examples/simple-session-hook-swarm.yml +37 -0
- data/lib/claude_swarm/base_executor.rb +133 -0
- data/lib/claude_swarm/claude_code_executor.rb +21 -136
- data/lib/claude_swarm/claude_mcp_server.rb +2 -1
- data/lib/claude_swarm/cli.rb +1 -0
- data/lib/claude_swarm/configuration.rb +1 -0
- data/lib/claude_swarm/openai/chat_completion.rb +15 -15
- data/lib/claude_swarm/openai/executor.rb +46 -160
- data/lib/claude_swarm/openai/responses.rb +27 -27
- data/lib/claude_swarm/orchestrator.rb +166 -166
- data/lib/claude_swarm/settings_generator.rb +54 -0
- data/lib/claude_swarm/version.rb +1 -1
- metadata +4 -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
|
@@ -14,6 +17,7 @@ module ClaudeSwarm
|
|
14
17
|
restore_session_path: nil, worktree: nil, session_id: nil)
|
15
18
|
@config = configuration
|
16
19
|
@generator = mcp_generator
|
20
|
+
@settings_generator = SettingsGenerator.new(configuration)
|
17
21
|
@vibe = vibe
|
18
22
|
@non_interactive_prompt = prompt
|
19
23
|
@interactive_prompt = interactive_prompt
|
@@ -21,6 +25,7 @@ module ClaudeSwarm
|
|
21
25
|
@debug = debug
|
22
26
|
@restore_session_path = restore_session_path
|
23
27
|
@session_path = nil
|
28
|
+
@session_log_path = nil
|
24
29
|
@provided_session_id = session_id
|
25
30
|
# Store worktree option for later use
|
26
31
|
@worktree_option = worktree
|
@@ -40,24 +45,23 @@ module ClaudeSwarm
|
|
40
45
|
@start_time = Time.now
|
41
46
|
|
42
47
|
if @restore_session_path
|
43
|
-
|
48
|
+
non_interactive_output do
|
44
49
|
puts "🔄 Restoring Claude Swarm: #{@config.swarm_name}"
|
45
50
|
puts "😎 Vibe mode ON" if @vibe
|
46
|
-
puts
|
47
51
|
end
|
48
52
|
|
49
53
|
# Use existing session path
|
50
54
|
session_path = @restore_session_path
|
51
55
|
@session_path = session_path
|
56
|
+
@session_log_path = File.join(@session_path, "session.log")
|
52
57
|
ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
|
53
58
|
ENV["CLAUDE_SWARM_ROOT_DIR"] = ClaudeSwarm.root_dir
|
54
59
|
|
55
60
|
# Create run symlink for restored session
|
56
61
|
create_run_symlink
|
57
62
|
|
58
|
-
|
63
|
+
non_interactive_output do
|
59
64
|
puts "📝 Using existing session: #{session_path}/"
|
60
|
-
puts
|
61
65
|
end
|
62
66
|
|
63
67
|
# Initialize process tracker
|
@@ -68,25 +72,28 @@ module ClaudeSwarm
|
|
68
72
|
|
69
73
|
# Regenerate MCP configurations with session IDs for restoration
|
70
74
|
@generator.generate_all
|
71
|
-
|
75
|
+
non_interactive_output do
|
72
76
|
puts "✓ Regenerated MCP configurations with session IDs"
|
73
|
-
|
77
|
+
end
|
78
|
+
|
79
|
+
# Generate settings files
|
80
|
+
@settings_generator.generate_all
|
81
|
+
non_interactive_output do
|
82
|
+
puts "✓ Generated settings files with hooks"
|
74
83
|
end
|
75
84
|
else
|
76
|
-
|
85
|
+
non_interactive_output do
|
77
86
|
puts "🐝 Starting Claude Swarm: #{@config.swarm_name}"
|
78
87
|
puts "😎 Vibe mode ON" if @vibe
|
79
|
-
puts
|
80
88
|
end
|
81
89
|
|
82
90
|
# Generate and set session path for all instances
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
SessionPath.generate(working_dir: ClaudeSwarm.root_dir)
|
87
|
-
end
|
91
|
+
session_params = { working_dir: ClaudeSwarm.root_dir }
|
92
|
+
session_params[:session_id] = @provided_session_id if @provided_session_id
|
93
|
+
session_path = SessionPath.generate(**session_params)
|
88
94
|
SessionPath.ensure_directory(session_path)
|
89
95
|
@session_path = session_path
|
96
|
+
@session_log_path = File.join(@session_path, "session.log")
|
90
97
|
|
91
98
|
# Extract session ID from path (the timestamp part)
|
92
99
|
@session_id = File.basename(session_path)
|
@@ -97,9 +104,8 @@ module ClaudeSwarm
|
|
97
104
|
# Create run symlink for new session
|
98
105
|
create_run_symlink
|
99
106
|
|
100
|
-
|
107
|
+
non_interactive_output do
|
101
108
|
puts "📝 Session files will be saved to: #{session_path}/"
|
102
|
-
puts
|
103
109
|
end
|
104
110
|
|
105
111
|
# Initialize process tracker
|
@@ -109,7 +115,7 @@ module ClaudeSwarm
|
|
109
115
|
if @needs_worktree_manager
|
110
116
|
cli_option = @worktree_option.is_a?(String) && !@worktree_option.empty? ? @worktree_option : nil
|
111
117
|
@worktree_manager = WorktreeManager.new(cli_option, session_id: @session_id)
|
112
|
-
|
118
|
+
non_interactive_output { print("🌳 Setting up Git worktrees...") }
|
113
119
|
|
114
120
|
# Get all instances for worktree setup
|
115
121
|
# Note: instances.values already includes the main instance
|
@@ -117,17 +123,21 @@ module ClaudeSwarm
|
|
117
123
|
|
118
124
|
@worktree_manager.setup_worktrees(all_instances)
|
119
125
|
|
120
|
-
|
126
|
+
non_interactive_output do
|
121
127
|
puts "✓ Worktrees created with branch: #{@worktree_manager.worktree_name}"
|
122
|
-
puts
|
123
128
|
end
|
124
129
|
end
|
125
130
|
|
126
131
|
# Generate all MCP configuration files
|
127
132
|
@generator.generate_all
|
128
|
-
|
133
|
+
non_interactive_output do
|
129
134
|
puts "✓ Generated MCP configurations in session directory"
|
130
|
-
|
135
|
+
end
|
136
|
+
|
137
|
+
# Generate settings files
|
138
|
+
@settings_generator.generate_all
|
139
|
+
non_interactive_output do
|
140
|
+
puts "✓ Generated settings files with hooks"
|
131
141
|
end
|
132
142
|
|
133
143
|
# Save swarm config path for restoration
|
@@ -136,7 +146,7 @@ module ClaudeSwarm
|
|
136
146
|
|
137
147
|
# Launch the main instance (fetch after worktree setup to get modified paths)
|
138
148
|
main_instance = @config.main_instance_config
|
139
|
-
|
149
|
+
non_interactive_output do
|
140
150
|
puts "🚀 Launching main instance: #{@config.main_instance}"
|
141
151
|
puts " Model: #{main_instance[:model]}"
|
142
152
|
if main_instance[:directories].size == 1
|
@@ -149,13 +159,13 @@ module ClaudeSwarm
|
|
149
159
|
puts " Disallowed tools: #{main_instance[:disallowed_tools].join(", ")}" if main_instance[:disallowed_tools]&.any?
|
150
160
|
puts " Connections: #{main_instance[:connections].join(", ")}" if main_instance[:connections].any?
|
151
161
|
puts " 😎 Vibe mode ON for this instance" if main_instance[:vibe]
|
152
|
-
puts
|
153
162
|
end
|
154
163
|
|
155
164
|
command = build_main_command(main_instance)
|
156
|
-
if @debug
|
157
|
-
|
158
|
-
|
165
|
+
if @debug
|
166
|
+
non_interactive_output do
|
167
|
+
puts "🏃 Running: #{format_command_for_display(command)}"
|
168
|
+
end
|
159
169
|
end
|
160
170
|
|
161
171
|
# Start log streaming thread if in non-interactive mode with --stream-logs
|
@@ -171,23 +181,21 @@ module ClaudeSwarm
|
|
171
181
|
# Execute before commands if specified
|
172
182
|
before_commands = @config.before_commands
|
173
183
|
if before_commands.any? && !@restore_session_path
|
174
|
-
|
184
|
+
non_interactive_output do
|
175
185
|
puts "⚙️ Executing before commands..."
|
176
|
-
puts
|
177
186
|
end
|
178
187
|
|
179
188
|
success = execute_before_commands?(before_commands)
|
180
189
|
unless success
|
181
|
-
|
190
|
+
non_interactive_output { print("❌ Before commands failed. Aborting swarm launch.") }
|
182
191
|
cleanup_processes
|
183
192
|
cleanup_run_symlink
|
184
193
|
cleanup_worktrees
|
185
194
|
exit(1)
|
186
195
|
end
|
187
196
|
|
188
|
-
|
197
|
+
non_interactive_output do
|
189
198
|
puts "✓ Before commands completed successfully"
|
190
|
-
puts
|
191
199
|
end
|
192
200
|
end
|
193
201
|
|
@@ -195,7 +203,11 @@ module ClaudeSwarm
|
|
195
203
|
# This ensures the main instance runs in a clean environment without inheriting
|
196
204
|
# Claude Swarm's BUNDLE_* environment variables
|
197
205
|
Bundler.with_unbundled_env do
|
198
|
-
|
206
|
+
if @non_interactive_prompt
|
207
|
+
stream_to_session_log(*command)
|
208
|
+
else
|
209
|
+
system!(*command)
|
210
|
+
end
|
199
211
|
end
|
200
212
|
end
|
201
213
|
|
@@ -212,16 +224,15 @@ module ClaudeSwarm
|
|
212
224
|
after_commands = @config.after_commands
|
213
225
|
if after_commands.any? && !@restore_session_path
|
214
226
|
Dir.chdir(main_instance[:directory]) do
|
215
|
-
|
216
|
-
|
217
|
-
puts "⚙️ Executing after commands..."
|
218
|
-
puts
|
227
|
+
non_interactive_output do
|
228
|
+
print("⚙️ Executing after commands...")
|
219
229
|
end
|
220
230
|
|
221
231
|
success = execute_after_commands?(after_commands)
|
222
|
-
|
223
|
-
|
224
|
-
|
232
|
+
unless success
|
233
|
+
non_interactive_output do
|
234
|
+
puts "⚠️ Some after commands failed"
|
235
|
+
end
|
225
236
|
end
|
226
237
|
end
|
227
238
|
end
|
@@ -234,115 +245,19 @@ module ClaudeSwarm
|
|
234
245
|
|
235
246
|
private
|
236
247
|
|
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
|
248
|
+
def non_interactive_output
|
249
|
+
return if @non_interactive_prompt
|
272
250
|
|
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
|
251
|
+
yield
|
252
|
+
puts
|
253
|
+
end
|
288
254
|
|
289
|
-
|
255
|
+
def execute_before_commands?(commands)
|
256
|
+
execute_commands(commands, phase: "before", fail_fast: true)
|
290
257
|
end
|
291
258
|
|
292
259
|
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
|
260
|
+
execute_commands(commands, phase: "after", fail_fast: false)
|
346
261
|
end
|
347
262
|
|
348
263
|
def save_swarm_config_path(session_path)
|
@@ -355,19 +270,21 @@ module ClaudeSwarm
|
|
355
270
|
File.write(root_dir_file, ClaudeSwarm.root_dir)
|
356
271
|
|
357
272
|
# Save session metadata
|
358
|
-
|
273
|
+
metadata_file = File.join(session_path, "session_metadata.json")
|
274
|
+
File.write(metadata_file, JSON.pretty_generate(build_session_metadata))
|
275
|
+
end
|
276
|
+
|
277
|
+
def build_session_metadata
|
278
|
+
{
|
359
279
|
"root_directory" => ClaudeSwarm.root_dir,
|
360
280
|
"timestamp" => Time.now.utc.iso8601,
|
361
281
|
"start_time" => @start_time.utc.iso8601,
|
362
282
|
"swarm_name" => @config.swarm_name,
|
363
283
|
"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))
|
284
|
+
}.tap do |metadata|
|
285
|
+
# Add worktree info if applicable
|
286
|
+
metadata["worktree"] = @worktree_manager.session_metadata if @worktree_manager
|
287
|
+
end
|
371
288
|
end
|
372
289
|
|
373
290
|
def cleanup_processes
|
@@ -378,9 +295,7 @@ module ClaudeSwarm
|
|
378
295
|
end
|
379
296
|
|
380
297
|
def cleanup_worktrees
|
381
|
-
|
382
|
-
|
383
|
-
@worktree_manager.cleanup_worktrees
|
298
|
+
@worktree_manager&.cleanup_worktrees
|
384
299
|
rescue StandardError => e
|
385
300
|
puts "⚠️ Error during worktree cleanup: #{e.message}"
|
386
301
|
end
|
@@ -417,7 +332,7 @@ module ClaudeSwarm
|
|
417
332
|
|
418
333
|
File.write(metadata_file, JSON.pretty_generate(metadata))
|
419
334
|
rescue StandardError => e
|
420
|
-
|
335
|
+
non_interactive_output { print("⚠️ Error updating session metadata: #{e.message}") }
|
421
336
|
end
|
422
337
|
|
423
338
|
def calculate_total_cost
|
@@ -466,7 +381,7 @@ module ClaudeSwarm
|
|
466
381
|
File.symlink(@session_path, symlink_path)
|
467
382
|
rescue StandardError => e
|
468
383
|
# Don't fail the process if symlink creation fails
|
469
|
-
|
384
|
+
non_interactive_output { print("⚠️ Warning: Could not create run symlink: #{e.message}") }
|
470
385
|
end
|
471
386
|
|
472
387
|
def cleanup_run_symlink
|
@@ -481,15 +396,11 @@ module ClaudeSwarm
|
|
481
396
|
|
482
397
|
def start_log_streaming
|
483
398
|
Thread.new do
|
484
|
-
session_log_path = File.join(ENV.fetch("CLAUDE_SWARM_SESSION_PATH", nil), "session.log")
|
485
|
-
|
486
399
|
# Wait for log file to be created
|
487
|
-
sleep(0.1) until File.exist?(session_log_path)
|
400
|
+
sleep(0.1) until File.exist?(@session_log_path)
|
488
401
|
|
489
402
|
# Open file and seek to end
|
490
|
-
File.open(session_log_path, "r") do |file|
|
491
|
-
file.seek(0, IO::SEEK_END)
|
492
|
-
|
403
|
+
File.open(@session_log_path, "r") do |file|
|
493
404
|
loop do
|
494
405
|
changes = file.read
|
495
406
|
if changes
|
@@ -590,11 +501,20 @@ module ClaudeSwarm
|
|
590
501
|
parts << "--mcp-config"
|
591
502
|
parts << mcp_config_path
|
592
503
|
|
504
|
+
# Add settings file if it exists for the main instance
|
505
|
+
settings_file = @settings_generator.settings_path(@config.main_instance)
|
506
|
+
if File.exist?(settings_file)
|
507
|
+
parts << "--settings"
|
508
|
+
parts << settings_file
|
509
|
+
end
|
510
|
+
|
593
511
|
# Handle different modes
|
594
512
|
if @non_interactive_prompt
|
595
513
|
# Non-interactive mode with -p
|
596
514
|
parts << "-p"
|
597
515
|
parts << @non_interactive_prompt
|
516
|
+
parts << "--verbose"
|
517
|
+
parts << "--output-format=stream-json"
|
598
518
|
elsif @interactive_prompt
|
599
519
|
# Interactive mode with initial prompt (no -p flag)
|
600
520
|
parts << @interactive_prompt
|
@@ -612,9 +532,8 @@ module ClaudeSwarm
|
|
612
532
|
worktree_data = metadata["worktree"]
|
613
533
|
return unless worktree_data && worktree_data["enabled"]
|
614
534
|
|
615
|
-
|
535
|
+
non_interactive_output do
|
616
536
|
puts "🌳 Restoring Git worktrees..."
|
617
|
-
puts
|
618
537
|
end
|
619
538
|
|
620
539
|
# Restore worktrees using the saved configuration
|
@@ -626,10 +545,91 @@ module ClaudeSwarm
|
|
626
545
|
all_instances = @config.instances.values
|
627
546
|
@worktree_manager.setup_worktrees(all_instances)
|
628
547
|
|
629
|
-
|
548
|
+
non_interactive_output do
|
549
|
+
puts "✓ Worktrees restored with branch: #{@worktree_manager.worktree_name}"
|
550
|
+
end
|
551
|
+
end
|
630
552
|
|
631
|
-
|
632
|
-
|
553
|
+
def stream_to_session_log(*command)
|
554
|
+
# Setup logger for session logging
|
555
|
+
logger = Logger.new(@session_log_path, level: :info, progname: @config.main_instance)
|
556
|
+
|
557
|
+
# Use Open3.popen2e to capture stdout and stderr merged for formatting
|
558
|
+
Open3.popen2e(*command) do |stdin, stdout_and_stderr, wait_thr|
|
559
|
+
stdin.close
|
560
|
+
|
561
|
+
# Read and process the merged output
|
562
|
+
stdout_and_stderr.each_line do |line|
|
563
|
+
# Try to parse and prettify JSON lines
|
564
|
+
|
565
|
+
json_data = JSON.parse(line.chomp)
|
566
|
+
pretty_json = JSON.pretty_generate(json_data)
|
567
|
+
logger.info { pretty_json }
|
568
|
+
rescue JSON::ParserError
|
569
|
+
# Warn about non-JSON output since we expect stream-json format
|
570
|
+
warn("⚠️ Warning: Non-JSON output detected in stream-json mode: #{line.chomp}")
|
571
|
+
# Log the line as-is
|
572
|
+
logger.info { line.chomp }
|
573
|
+
end
|
574
|
+
|
575
|
+
wait_thr.value
|
576
|
+
end
|
577
|
+
end
|
578
|
+
|
579
|
+
def execute_commands(commands, phase:, fail_fast:)
|
580
|
+
all_succeeded = true
|
581
|
+
|
582
|
+
# Setup logger for session logging if we have a session path
|
583
|
+
logger = Logger.new(@session_log_path, level: :info)
|
584
|
+
|
585
|
+
commands.each_with_index do |command, index|
|
586
|
+
# Log the command execution to session log
|
587
|
+
logger.info { "Executing #{phase} command #{index + 1}/#{commands.size}: #{command}" }
|
588
|
+
|
589
|
+
# Execute the command and capture output
|
590
|
+
begin
|
591
|
+
if @debug
|
592
|
+
non_interactive_output do
|
593
|
+
debug_prefix = phase == "after" ? "after " : ""
|
594
|
+
print("Debug: Executing #{debug_prefix} command #{index + 1}/#{commands.size}: #{format_command_for_display(command)}")
|
595
|
+
end
|
596
|
+
end
|
597
|
+
|
598
|
+
output = %x(#{command} 2>&1)
|
599
|
+
success = $CHILD_STATUS.success?
|
600
|
+
output_separator = "-" * 80
|
601
|
+
|
602
|
+
logger.info { "Command output:" }
|
603
|
+
logger.info { output }
|
604
|
+
logger.info { "Exit status: #{$CHILD_STATUS.exitstatus}" }
|
605
|
+
logger.info { output_separator }
|
606
|
+
|
607
|
+
# Show output if in debug mode or if command failed
|
608
|
+
if @debug || !success
|
609
|
+
non_interactive_output do
|
610
|
+
output_prefix = phase == "after" ? "After command" : "Command"
|
611
|
+
puts "#{output_prefix} #{index + 1} output:"
|
612
|
+
puts output
|
613
|
+
print("Exit status: #{$CHILD_STATUS.exitstatus}")
|
614
|
+
end
|
615
|
+
end
|
616
|
+
|
617
|
+
unless success
|
618
|
+
error_prefix = phase.capitalize
|
619
|
+
non_interactive_output { print("❌ #{error_prefix} command #{index + 1} failed: #{command}") }
|
620
|
+
all_succeeded = false
|
621
|
+
return false if fail_fast
|
622
|
+
end
|
623
|
+
rescue StandardError => e
|
624
|
+
non_interactive_output { print("Error executing #{phase} command #{index + 1}: #{e.message}") }
|
625
|
+
logger.info { "Error: #{e.message}" }
|
626
|
+
logger.info { output_separator }
|
627
|
+
all_succeeded = false
|
628
|
+
return false if fail_fast
|
629
|
+
end
|
630
|
+
end
|
631
|
+
|
632
|
+
all_succeeded
|
633
633
|
end
|
634
634
|
end
|
635
635
|
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ClaudeSwarm
|
4
|
+
class SettingsGenerator
|
5
|
+
def initialize(configuration)
|
6
|
+
@config = configuration
|
7
|
+
end
|
8
|
+
|
9
|
+
def generate_all
|
10
|
+
ensure_session_directory
|
11
|
+
|
12
|
+
@config.instances.each do |name, instance|
|
13
|
+
generate_settings(name, instance)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def settings_path(instance_name)
|
18
|
+
File.join(session_path, "#{instance_name}_settings.json")
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def session_path
|
24
|
+
# In tests, use the session path from env if available, otherwise use a temp path
|
25
|
+
@session_path ||= if ENV["CLAUDE_SWARM_SESSION_PATH"]
|
26
|
+
SessionPath.from_env
|
27
|
+
else
|
28
|
+
# This should only happen in unit tests
|
29
|
+
Dir.pwd
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def ensure_session_directory
|
34
|
+
# Session directory is already created by orchestrator
|
35
|
+
# Just ensure it exists
|
36
|
+
SessionPath.ensure_directory(session_path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def generate_settings(name, instance)
|
40
|
+
settings = {}
|
41
|
+
|
42
|
+
# Add hooks if configured
|
43
|
+
if instance[:hooks] && !instance[:hooks].empty?
|
44
|
+
settings["hooks"] = instance[:hooks]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Only write settings file if there are settings to write
|
48
|
+
return if settings.empty?
|
49
|
+
|
50
|
+
# Write settings file
|
51
|
+
File.write(settings_path(name), JSON.pretty_generate(settings))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
data/lib/claude_swarm/version.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.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Paulo Arruda
|
@@ -143,10 +143,12 @@ files:
|
|
143
143
|
- examples/monitoring-demo.yml
|
144
144
|
- examples/multi-directory.yml
|
145
145
|
- examples/session-restoration-demo.yml
|
146
|
+
- examples/simple-session-hook-swarm.yml
|
146
147
|
- examples/test-generation.yml
|
147
148
|
- examples/with-before-commands.yml
|
148
149
|
- exe/claude-swarm
|
149
150
|
- lib/claude_swarm.rb
|
151
|
+
- lib/claude_swarm/base_executor.rb
|
150
152
|
- lib/claude_swarm/claude_code_executor.rb
|
151
153
|
- lib/claude_swarm/claude_mcp_server.rb
|
152
154
|
- lib/claude_swarm/cli.rb
|
@@ -161,6 +163,7 @@ files:
|
|
161
163
|
- lib/claude_swarm/process_tracker.rb
|
162
164
|
- lib/claude_swarm/session_cost_calculator.rb
|
163
165
|
- lib/claude_swarm/session_path.rb
|
166
|
+
- lib/claude_swarm/settings_generator.rb
|
164
167
|
- lib/claude_swarm/system_utils.rb
|
165
168
|
- lib/claude_swarm/templates/generation_prompt.md.erb
|
166
169
|
- lib/claude_swarm/tools/reset_session_tool.rb
|