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 +4 -4
- data/CHANGELOG.md +35 -0
- data/CLAUDE.md +2 -7
- data/README.md +5 -1
- data/lib/claude_swarm/claude_code_executor.rb +35 -30
- data/lib/claude_swarm/cli.rb +28 -6
- data/lib/claude_swarm/commands/ps.rb +4 -4
- data/lib/claude_swarm/commands/show.rb +3 -3
- data/lib/claude_swarm/mcp_generator.rb +46 -1
- data/lib/claude_swarm/openai/executor.rb +20 -13
- data/lib/claude_swarm/orchestrator.rb +58 -45
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm.rb +6 -0
- data/team.yml +213 -300
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 84d6a42818a51bc2b0d47519fb874d77875c23646559a0d8820087866cc1a6d4
|
4
|
+
data.tar.gz: 4f4b1a8cb570cab6c38a385e00d3155715f04a4b55f41fe87f0fb397caf0be6c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
-
[](https://badge.fury.io/rb/claude_swarm)
|
4
4
|
[](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
|
37
|
-
|
38
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
end
|
49
|
+
# Process stdout line by line
|
50
|
+
stdout.each_line do |line|
|
51
|
+
json_data = JSON.parse(line.strip)
|
44
52
|
|
45
|
-
|
46
|
-
|
47
|
-
json_data = JSON.parse(line.strip)
|
53
|
+
# Log each JSON event
|
54
|
+
log_streaming_event(json_data)
|
48
55
|
|
49
|
-
|
50
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
#
|
59
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
|
data/lib/claude_swarm/cli.rb
CHANGED
@@ -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:
|
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
|
522
|
-
|
523
|
-
if File.exist?(
|
524
|
-
original_dir = File.read(
|
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:
|
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
|
86
|
-
base_dir =
|
87
|
-
|
88
|
-
base_dir = File.read(
|
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
|
41
|
-
|
42
|
-
puts "
|
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
|
-
|
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
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|