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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4f802dadf6aa3673a354582aed75d8887d8ff83c4d30d700eba89f7f3663eec9
4
- data.tar.gz: a2aea35f422333fd2421d078bf336745c61172c900ac96da1d20d50100fef607
3
+ metadata.gz: 84d6a42818a51bc2b0d47519fb874d77875c23646559a0d8820087866cc1a6d4
4
+ data.tar.gz: 4f4b1a8cb570cab6c38a385e00d3155715f04a4b55f41fe87f0fb397caf0be6c
5
5
  SHA512:
6
- metadata.gz: 7fb47e9059f561852a81411920dc0a0d568c5c47a2361823ee5425e3820ef21699c26072e1bf330fdfff4514b8fc3c182df59975266f3f62b5e89aeba31dd63a
7
- data.tar.gz: dec84ec9eb8b0f95edb1d079fcc7ab155ed754713343f8730f68d248f28db1e9ad0571b734f885cbcc30fbda58a0cde077d5e7d18c960c5a9ad1e5c03df396a6
6
+ metadata.gz: 55fce00688afa714aa79897b008e885f600fb198b37e2bb2ba90563e57345ff922f448a86f2bde93409fdb4d5486fb763cfc1a27ba826b79106c55a92116a1e4
7
+ data.tar.gz: 8f9c58fbee771dec48c0c41fd13f078633120b686ecaa95517f95269e8796f8aaaf6d9ff54556edaa8389bda10a6a53bb45d1e0318ffc79027a7b6a7e848ef11
data/CHANGELOG.md CHANGED
@@ -1,3 +1,38 @@
1
+ ## [0.3.1]
2
+
3
+ ### Added
4
+ - **Interactive mode with initial prompt**: Added `-i/--interactive` flag to provide an initial prompt for interactive mode
5
+ - Use `claude-swarm -i "Your initial prompt"` to start in interactive mode with a prompt
6
+ - Cannot be used together with `-p/--prompt` (which is for non-interactive mode)
7
+ - Allows users to provide context or initial instructions while maintaining interactive session
8
+
9
+ ### Fixed
10
+ - **Development documentation**: Fixed `bundle exec` prefix in CLAUDE.md for development commands
11
+ - **Bundler environment conflicts**: Fixed issue where Claude instances would inherit bundler environment variables, causing conflicts when working in Ruby projects
12
+ - MCP servers now receive necessary Ruby/Bundler environment variables to run properly
13
+ - Claude instances (main and connected) run in clean environments via `Bundler.with_unbundled_env`
14
+ - Prevents `bundle install` and other bundler commands from using Claude Swarm's Gemfile instead of the project's Gemfile
15
+ - `claude mcp serve` now runs with a filtered environment that excludes Ruby/Bundler variables while preserving system variables
16
+
17
+ ## [0.3.0]
18
+
19
+ ### Added
20
+ - **Root directory parameter**: Added `--root-dir` option to the `start` command to enable running claude-swarm from any directory
21
+ - Use `claude-swarm start /path/to/config.yml --root-dir /path/to/project` to run from anywhere
22
+ - All relative paths in configuration files are resolved from the root directory
23
+ - Defaults to current directory when not specified, maintaining backward compatibility
24
+ - Environment variable `CLAUDE_SWARM_ROOT_DIR` is set and inherited by all child processes
25
+
26
+ ### Changed
27
+ - **BREAKING CHANGE: Renamed session directory references**: Session metadata and file storage have been updated to use "root_directory" terminology
28
+ - Environment variable renamed from `CLAUDE_SWARM_START_DIR` to `CLAUDE_SWARM_ROOT_DIR`
29
+ - Session file renamed from `start_directory` to `root_directory`
30
+ - Session metadata field renamed from `"start_directory"` to `"root_directory"`
31
+ - Display text in `show` command changed from "Start Directory:" to "Root Directory:"
32
+ - **Refactored root directory access**: Introduced `ClaudeSwarm.root_dir` method for cleaner code
33
+ - Centralizes root directory resolution logic
34
+ - Replaces repetitive `ENV.fetch` calls throughout the codebase
35
+
1
36
  ## [0.2.1]
2
37
 
3
38
  ### Added
data/CLAUDE.md CHANGED
@@ -8,14 +8,9 @@ Claude Swarm is a Ruby gem that orchestrates multiple Claude Code instances as a
8
8
 
9
9
  ## Development Commands
10
10
 
11
- ### Setup
12
- ```bash
13
- bin/setup # Install dependencies
14
- ```
15
-
16
11
  ### Testing
17
12
  ```bash
18
- rake test # Run the Minitest test suite
13
+ bundle exec rake test # Run the Minitest test suite
19
14
  ```
20
15
 
21
16
  **Important**: Tests should not generate any output to stdout or stderr. When writing tests:
@@ -37,7 +32,7 @@ end
37
32
 
38
33
  ### Linting
39
34
  ```bash
40
- rake rubocop -A # Run RuboCop linter to auto fix problems
35
+ bundle exec rubocop -A # Run RuboCop linter to auto fix problems
41
36
  ```
42
37
 
43
38
  ### Development Console
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Claude Swarm
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/claude_swarm.svg?cache_bust=0.2.0)](https://badge.fury.io/rb/claude_swarm)
3
+ [![Gem Version](https://badge.fury.io/rb/claude_swarm.svg?cache_bust=0.3.0)](https://badge.fury.io/rb/claude_swarm)
4
4
  [![CI](https://github.com/parruda/claude-swarm/actions/workflows/ci.yml/badge.svg)](https://github.com/parruda/claude-swarm/actions/workflows/ci.yml)
5
5
 
6
6
  Claude Swarm orchestrates multiple Claude Code instances as a collaborative AI development team. It enables running AI agents with specialized roles, tools, and directory contexts, communicating via MCP (Model Context Protocol) in a tree-like hierarchy. Define your swarm topology in simple YAML and let Claude instances delegate tasks through connected instances. Perfect for complex projects requiring specialized AI agents for frontend, backend, testing, DevOps, or research tasks.
@@ -767,6 +767,10 @@ claude-swarm --vibe
767
767
  claude-swarm -p "Implement the new user authentication feature"
768
768
  claude-swarm --prompt "Fix the bug in the payment module"
769
769
 
770
+ # Run in interactive mode with an initial prompt
771
+ claude-swarm -i "Review the codebase and suggest improvements"
772
+ claude-swarm --interactive "Help me debug this test failure"
773
+
770
774
  # Use a custom session ID instead of auto-generated UUID
771
775
  claude-swarm --session-id my-custom-session-123
772
776
 
@@ -33,43 +33,48 @@ module ClaudeSwarm
33
33
  stderr_output = []
34
34
  result_response = nil
35
35
 
36
- # Execute command with streaming
37
- Open3.popen3(*cmd_array, chdir: @working_directory) do |stdin, stdout, stderr, wait_thread|
38
- stdin.close
36
+ # Execute command with unbundled environment to avoid bundler conflicts
37
+ # This ensures claude runs in a clean environment without inheriting
38
+ # Claude Swarm's BUNDLE_* environment variables
39
+ Bundler.with_unbundled_env do
40
+ # Execute command with streaming
41
+ Open3.popen3(*cmd_array, chdir: @working_directory) do |stdin, stdout, stderr, wait_thread|
42
+ stdin.close
43
+
44
+ # Read stderr in a separate thread
45
+ stderr_thread = Thread.new do
46
+ stderr.each_line { |line| stderr_output << line }
47
+ end
39
48
 
40
- # Read stderr in a separate thread
41
- stderr_thread = Thread.new do
42
- stderr.each_line { |line| stderr_output << line }
43
- end
49
+ # Process stdout line by line
50
+ stdout.each_line do |line|
51
+ json_data = JSON.parse(line.strip)
44
52
 
45
- # Process stdout line by line
46
- stdout.each_line do |line|
47
- json_data = JSON.parse(line.strip)
53
+ # Log each JSON event
54
+ log_streaming_event(json_data)
48
55
 
49
- # Log each JSON event
50
- log_streaming_event(json_data)
56
+ # Capture session_id from system init
57
+ if json_data["type"] == "system" && json_data["subtype"] == "init"
58
+ @session_id = json_data["session_id"]
59
+ write_instance_state
60
+ end
51
61
 
52
- # Capture session_id from system init
53
- if json_data["type"] == "system" && json_data["subtype"] == "init"
54
- @session_id = json_data["session_id"]
55
- write_instance_state
62
+ # Capture the final result
63
+ result_response = json_data if json_data["type"] == "result"
64
+ rescue JSON::ParserError => e
65
+ @logger.warn("Failed to parse JSON line: #{line.strip} - #{e.message}")
56
66
  end
57
67
 
58
- # Capture the final result
59
- result_response = json_data if json_data["type"] == "result"
60
- rescue JSON::ParserError => e
61
- @logger.warn("Failed to parse JSON line: #{line.strip} - #{e.message}")
62
- end
68
+ # Wait for stderr thread to finish
69
+ stderr_thread.join
63
70
 
64
- # Wait for stderr thread to finish
65
- stderr_thread.join
66
-
67
- # Check exit status
68
- exit_status = wait_thread.value
69
- unless exit_status.success?
70
- error_msg = stderr_output.join
71
- @logger.error("Execution error for #{@instance_name}: #{error_msg}")
72
- raise ExecutionError, "Claude Code execution failed: #{error_msg}"
71
+ # Check exit status
72
+ exit_status = wait_thread.value
73
+ unless exit_status.success?
74
+ error_msg = stderr_output.join
75
+ @logger.error("Execution error for #{@instance_name}: #{error_msg}")
76
+ raise ExecutionError, "Claude Code execution failed: #{error_msg}"
77
+ end
73
78
  end
74
79
  end
75
80
 
@@ -18,6 +18,10 @@ module ClaudeSwarm
18
18
  aliases: "-p",
19
19
  type: :string,
20
20
  desc: "Prompt to pass to the main Claude instance (non-interactive mode)"
21
+ method_option :interactive,
22
+ aliases: "-i",
23
+ type: :string,
24
+ desc: "Initial prompt for interactive mode"
21
25
  method_option :stream_logs,
22
26
  type: :boolean,
23
27
  default: false,
@@ -34,6 +38,9 @@ module ClaudeSwarm
34
38
  method_option :session_id,
35
39
  type: :string,
36
40
  desc: "Use a specific session ID instead of generating one"
41
+ method_option :root_dir,
42
+ type: :string,
43
+ desc: "Root directory for resolving relative paths (defaults to current directory)"
37
44
  def start(config_file = nil)
38
45
  config_path = config_file || "claude-swarm.yml"
39
46
  unless File.exist?(config_path)
@@ -41,6 +48,10 @@ module ClaudeSwarm
41
48
  exit(1)
42
49
  end
43
50
 
51
+ # Set root directory early so it's available to all components
52
+ root_dir = options[:root_dir] || Dir.pwd
53
+ ENV["CLAUDE_SWARM_ROOT_DIR"] = File.expand_path(root_dir)
54
+
44
55
  say("Starting Claude Swarm from #{config_path}...") unless options[:prompt]
45
56
 
46
57
  # Validate stream_logs option
@@ -49,14 +60,21 @@ module ClaudeSwarm
49
60
  exit(1)
50
61
  end
51
62
 
63
+ # Validate conflicting options
64
+ if options[:prompt] && options[:interactive]
65
+ error("Cannot use both -p/--prompt and -i/--interactive")
66
+ exit(1)
67
+ end
68
+
52
69
  begin
53
- config = Configuration.new(config_path, base_dir: Dir.pwd, options: options)
70
+ config = Configuration.new(config_path, base_dir: ClaudeSwarm.root_dir, options: options)
54
71
  generator = McpGenerator.new(config, vibe: options[:vibe])
55
72
  orchestrator = Orchestrator.new(
56
73
  config,
57
74
  generator,
58
75
  vibe: options[:vibe],
59
76
  prompt: options[:prompt],
77
+ interactive_prompt: options[:interactive],
60
78
  stream_logs: options[:stream_logs],
61
79
  debug: options[:debug],
62
80
  worktree: options[:worktree],
@@ -518,20 +536,24 @@ module ClaudeSwarm
518
536
  exit(1)
519
537
  end
520
538
 
521
- # Change to the original start directory if it exists
522
- start_dir_file = File.join(session_path, "start_directory")
523
- if File.exist?(start_dir_file)
524
- original_dir = File.read(start_dir_file).strip
539
+ # Change to the original root directory if it exists
540
+ root_dir_file = File.join(session_path, "root_directory")
541
+ if File.exist?(root_dir_file)
542
+ original_dir = File.read(root_dir_file).strip
525
543
  if Dir.exist?(original_dir)
526
544
  Dir.chdir(original_dir)
545
+ ENV["CLAUDE_SWARM_ROOT_DIR"] = original_dir
527
546
  say("Changed to original directory: #{original_dir}", :green) unless options[:prompt]
528
547
  else
529
548
  error("Original directory no longer exists: #{original_dir}")
530
549
  exit(1)
531
550
  end
551
+ else
552
+ # If no root_directory file, use current directory
553
+ ENV["CLAUDE_SWARM_ROOT_DIR"] = Dir.pwd
532
554
  end
533
555
 
534
- config = Configuration.new(config_file, base_dir: Dir.pwd)
556
+ config = Configuration.new(config_file, base_dir: ClaudeSwarm.root_dir)
535
557
 
536
558
  # Load session metadata if it exists to check for worktree info
537
559
  session_metadata_file = File.join(session_path, "session_metadata.json")
@@ -82,10 +82,10 @@ module ClaudeSwarm
82
82
  swarm_name = config.dig("swarm", "name") || "Unknown"
83
83
  main_instance = config.dig("swarm", "main")
84
84
 
85
- # Get base directory from session metadata or start_directory file
86
- base_dir = Dir.pwd
87
- start_dir_file = File.join(session_dir, "start_directory")
88
- base_dir = File.read(start_dir_file).strip if File.exist?(start_dir_file)
85
+ # Get base directory from session metadata or root_directory file
86
+ base_dir = ClaudeSwarm.root_dir
87
+ root_dir_file = File.join(session_dir, "root_directory")
88
+ base_dir = File.read(root_dir_file).strip if File.exist?(root_dir_file)
89
89
 
90
90
  # Get all directories - handle both string and array formats
91
91
  dir_config = config.dig("swarm", "instances", main_instance, "directory")
@@ -37,9 +37,9 @@ module ClaudeSwarm
37
37
 
38
38
  puts "Total Cost: #{cost_display}"
39
39
 
40
- # Try to read start directory
41
- start_dir_file = File.join(session_path, "start_directory")
42
- puts "Start Directory: #{File.read(start_dir_file).strip}" if File.exist?(start_dir_file)
40
+ # Try to read root directory
41
+ root_dir_file = File.join(session_path, "root_directory")
42
+ puts "Root Directory: #{File.read(root_dir_file).strip}" if File.exist?(root_dir_file)
43
43
 
44
44
  puts
45
45
  puts "Instance Hierarchy:"
@@ -95,10 +95,21 @@ module ClaudeSwarm
95
95
  end
96
96
 
97
97
  def build_claude_tools_mcp_config
98
+ # Build environment for claude mcp serve by excluding Ruby/Bundler-specific variables
99
+ # This preserves all system variables while removing Ruby contamination
100
+ clean_env = ENV.to_h.reject do |key, _|
101
+ key.start_with?("BUNDLE_") ||
102
+ key.start_with?("RUBY") ||
103
+ key.start_with?("GEM_") ||
104
+ key == "RUBYOPT" ||
105
+ key == "RUBYLIB"
106
+ end
107
+
98
108
  {
99
109
  "type" => "stdio",
100
110
  "command" => "claude",
101
111
  "args" => ["mcp", "serve"],
112
+ "env" => clean_env,
102
113
  }
103
114
  end
104
115
 
@@ -161,11 +172,45 @@ module ClaudeSwarm
161
172
  args.push("--claude-session-id", claude_session_id) if claude_session_id
162
173
  end
163
174
 
164
- {
175
+ # Capture environment variables needed for Ruby and Bundler to work properly
176
+ # This includes both BUNDLE_* variables and Ruby-specific variables
177
+ required_env = {}
178
+
179
+ # Bundle-specific variables
180
+ ENV.each do |k, v|
181
+ required_env[k] = v if k.start_with?("BUNDLE_")
182
+ end
183
+
184
+ # Claude Swarm-specific variables
185
+ ENV.each do |k, v|
186
+ required_env[k] = v if k.start_with?("CLAUDE_SWARM_")
187
+ end
188
+
189
+ # Ruby-specific variables that MCP servers need
190
+ [
191
+ "RUBY_ROOT",
192
+ "RUBY_ENGINE",
193
+ "RUBY_VERSION",
194
+ "GEM_ROOT",
195
+ "GEM_HOME",
196
+ "GEM_PATH",
197
+ "RUBYOPT",
198
+ "RUBYLIB",
199
+ "PATH",
200
+ ].each do |key|
201
+ required_env[key] = ENV[key] if ENV[key]
202
+ end
203
+
204
+ config = {
165
205
  "type" => "stdio",
166
206
  "command" => exe_path,
167
207
  "args" => args,
168
208
  }
209
+
210
+ # Add required environment variables if any exist
211
+ config["env"] = required_env unless required_env.empty?
212
+
213
+ config
169
214
  end
170
215
 
171
216
  def load_instance_states
@@ -168,10 +168,12 @@ module ClaudeSwarm
168
168
  command_array = [server_config["command"]]
169
169
  command_array.concat(server_config["args"] || [])
170
170
 
171
- mcp_configs << MCPClient.stdio_config(
171
+ stdio_config = MCPClient.stdio_config(
172
172
  command: command_array,
173
173
  name: name,
174
174
  )
175
+ stdio_config[:read_timeout] = 1800
176
+ mcp_configs << stdio_config
175
177
  when "sse"
176
178
  @logger.warn("SSE MCP servers not yet supported for OpenAI instances: #{name}")
177
179
  # TODO: Add SSE support when available in ruby-mcp-client
@@ -179,18 +181,23 @@ module ClaudeSwarm
179
181
  end
180
182
 
181
183
  if mcp_configs.any?
182
- @mcp_client = MCPClient.create_client(
183
- mcp_server_configs: mcp_configs,
184
- logger: @logger,
185
- )
186
-
187
- # List available tools from all MCP servers
188
- begin
189
- @available_tools = @mcp_client.list_tools
190
- @logger.info("Loaded #{@available_tools.size} tools from #{mcp_configs.size} MCP server(s)")
191
- rescue StandardError => e
192
- @logger.error("Failed to load MCP tools: #{e.message}")
193
- @available_tools = []
184
+ # Create MCP client with unbundled environment to avoid bundler conflicts
185
+ # This ensures MCP servers run in a clean environment without inheriting
186
+ # Claude Swarm's BUNDLE_* environment variables
187
+ Bundler.with_unbundled_env do
188
+ @mcp_client = MCPClient.create_client(
189
+ mcp_server_configs: mcp_configs,
190
+ logger: @logger,
191
+ )
192
+
193
+ # List available tools from all MCP servers
194
+ begin
195
+ @available_tools = @mcp_client.list_tools
196
+ @logger.info("Loaded #{@available_tools.size} tools from #{mcp_configs.size} MCP server(s)")
197
+ rescue StandardError => e
198
+ @logger.error("Failed to load MCP tools: #{e.message}")
199
+ @available_tools = []
200
+ end
194
201
  end
195
202
  end
196
203
  end