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.
@@ -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
- unless @non_interactive_prompt
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
- unless @non_interactive_prompt
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
- unless @non_interactive_prompt
75
+ non_interactive_output do
72
76
  puts "✓ Regenerated MCP configurations with session IDs"
73
- puts
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
- unless @non_interactive_prompt
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
- session_path = if @provided_session_id
84
- SessionPath.generate(working_dir: ClaudeSwarm.root_dir, session_id: @provided_session_id)
85
- else
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
- unless @non_interactive_prompt
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
- puts "🌳 Setting up Git worktrees..." unless @non_interactive_prompt
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
- unless @non_interactive_prompt
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
- unless @non_interactive_prompt
133
+ non_interactive_output do
129
134
  puts "✓ Generated MCP configurations in session directory"
130
- puts
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
- unless @non_interactive_prompt
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 && !@non_interactive_prompt
157
- puts "🏃 Running: #{format_command_for_display(command)}"
158
- puts
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
- unless @non_interactive_prompt
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
- puts "❌ Before commands failed. Aborting swarm launch." unless @non_interactive_prompt
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
- unless @non_interactive_prompt
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
- system!(*command)
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
- unless @non_interactive_prompt
216
- puts
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
- if !success && !@non_interactive_prompt
223
- puts "⚠️ Some after commands failed"
224
- puts
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 execute_before_commands?(commands)
238
- log_file = File.join(@session_path, "session.log") if @session_path
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
- unless success
274
- puts "❌ Before command #{index + 1} failed: #{command}" unless @non_interactive_prompt
275
- return false
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
- true
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
- log_file = File.join(@session_path, "session.log") if @session_path
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
- metadata = {
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
- # Add worktree info if applicable
367
- metadata["worktree"] = @worktree_manager.session_metadata if @worktree_manager
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
- return unless @worktree_manager
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
- puts "⚠️ Error updating session metadata: #{e.message}" unless @non_interactive_prompt
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
- puts "⚠️ Warning: Could not create run symlink: #{e.message}" unless @non_interactive_prompt
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
- unless @non_interactive_prompt
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
- return if @non_interactive_prompt
548
+ non_interactive_output do
549
+ puts "✓ Worktrees restored with branch: #{@worktree_manager.worktree_name}"
550
+ end
551
+ end
630
552
 
631
- puts "✓ Worktrees restored with branch: #{@worktree_manager.worktree_name}"
632
- puts
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "0.3.6"
4
+ VERSION = "0.3.8"
5
5
  end
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.6
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