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.
@@ -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
- @prompt = prompt
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 @prompt
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 @prompt
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["CLAUDE_SWARM_START_DIR"] = Dir.pwd
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 @prompt
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 @prompt
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 @prompt
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: Dir.pwd, session_id: @provided_session_id)
82
+ SessionPath.generate(working_dir: ClaudeSwarm.root_dir, session_id: @provided_session_id)
82
83
  else
83
- SessionPath.generate(working_dir: Dir.pwd)
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["CLAUDE_SWARM_START_DIR"] = Dir.pwd
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 @prompt
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 @prompt
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 @prompt
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 @prompt
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 @prompt
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 && !@prompt
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 @prompt && @stream_logs
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 @prompt
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 @prompt
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 @prompt
189
+ unless @non_interactive_prompt
189
190
  puts "✓ Before commands completed successfully"
190
191
  puts
191
192
  end
192
193
  end
193
194
 
194
- system!(*command)
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 @prompt
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 && !@prompt
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 && !@prompt
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) && !@prompt
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 @prompt
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 @prompt
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 && !@prompt
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) && !@prompt
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 @prompt
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 @prompt
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 original working directory
349
- start_dir_file = File.join(session_path, "start_directory")
350
- File.write(start_dir_file, Dir.pwd)
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
- "start_directory" => Dir.pwd,
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 && !@prompt
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 @prompt
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 @prompt
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
- if @prompt
618
+ # Handle different modes
619
+ if @non_interactive_prompt
620
+ # Non-interactive mode with -p
612
621
  parts << "-p"
613
- parts << @prompt
614
- else
615
- parts << "#{instance[:prompt]}\n\nNow just say 'I am ready to start'"
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 @prompt
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 @prompt
654
+ return if @non_interactive_prompt
642
655
 
643
656
  puts "✓ Worktrees restored with branch: #{@worktree_manager.worktree_name}"
644
657
  puts
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.1"
5
5
  end
data/lib/claude_swarm.rb CHANGED
@@ -37,4 +37,10 @@ loader.setup
37
37
 
38
38
  module ClaudeSwarm
39
39
  class Error < StandardError; end
40
+
41
+ class << self
42
+ def root_dir
43
+ ENV.fetch("CLAUDE_SWARM_ROOT_DIR", Dir.pwd)
44
+ end
45
+ end
40
46
  end