claude_swarm 0.1.20 → 0.2.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -66
  3. data/.rubocop_todo.yml +11 -0
  4. data/CHANGELOG.md +106 -0
  5. data/CLAUDE.md +61 -0
  6. data/README.md +174 -16
  7. data/Rakefile +1 -1
  8. data/examples/mixed-provider-swarm.yml +23 -0
  9. data/lib/claude_swarm/claude_code_executor.rb +7 -12
  10. data/lib/claude_swarm/claude_mcp_server.rb +26 -12
  11. data/lib/claude_swarm/cli.rb +293 -165
  12. data/lib/claude_swarm/commands/ps.rb +22 -24
  13. data/lib/claude_swarm/commands/show.rb +45 -63
  14. data/lib/claude_swarm/configuration.rb +161 -8
  15. data/lib/claude_swarm/mcp_generator.rb +39 -14
  16. data/lib/claude_swarm/openai/chat_completion.rb +264 -0
  17. data/lib/claude_swarm/openai/executor.rb +301 -0
  18. data/lib/claude_swarm/openai/responses.rb +338 -0
  19. data/lib/claude_swarm/orchestrator.rb +205 -39
  20. data/lib/claude_swarm/process_tracker.rb +7 -7
  21. data/lib/claude_swarm/session_cost_calculator.rb +93 -0
  22. data/lib/claude_swarm/session_path.rb +3 -5
  23. data/lib/claude_swarm/system_utils.rb +1 -3
  24. data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
  25. data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
  26. data/lib/claude_swarm/tools/task_tool.rb +43 -0
  27. data/lib/claude_swarm/version.rb +1 -1
  28. data/lib/claude_swarm/worktree_manager.rb +39 -22
  29. data/lib/claude_swarm.rb +23 -10
  30. data/single.yml +481 -6
  31. metadata +54 -14
  32. data/claude-swarm.yml +0 -64
  33. data/lib/claude_swarm/reset_session_tool.rb +0 -22
  34. data/lib/claude_swarm/session_info_tool.rb +0 -22
  35. data/lib/claude_swarm/task_tool.rb +0 -39
  36. /data/{example → examples}/claude-swarm.yml +0 -0
  37. /data/{example → examples}/microservices-team.yml +0 -0
  38. /data/{example → examples}/session-restoration-demo.yml +0 -0
  39. /data/{example → examples}/test-generation.yml +0 -0
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
- require "json"
5
- require "time"
6
-
7
3
  module ClaudeSwarm
8
4
  module Commands
9
5
  class Ps
@@ -80,7 +76,7 @@ module ClaudeSwarm
80
76
 
81
77
  # Load config for swarm name and main directory
82
78
  config_file = File.join(session_dir, "config.yml")
83
- return nil unless File.exist?(config_file)
79
+ return unless File.exist?(config_file)
84
80
 
85
81
  config = YAML.load_file(config_file)
86
82
  swarm_name = config.dig("swarm", "name") || "Unknown"
@@ -94,10 +90,10 @@ module ClaudeSwarm
94
90
  # Get all directories - handle both string and array formats
95
91
  dir_config = config.dig("swarm", "instances", main_instance, "directory")
96
92
  directories = if dir_config.is_a?(Array)
97
- dir_config
98
- else
99
- [dir_config || "."]
100
- end
93
+ dir_config
94
+ else
95
+ [dir_config || "."]
96
+ end
101
97
 
102
98
  # Expand paths relative to the base directory
103
99
  expanded_directories = directories.map do |dir|
@@ -110,10 +106,11 @@ module ClaudeSwarm
110
106
  directories_str = expanded_directories.join(", ")
111
107
 
112
108
  # Calculate total cost from JSON log
113
- total_cost = calculate_total_cost(session_dir)
109
+ log_file = File.join(session_dir, "session.log.json")
110
+ total_cost = SessionCostCalculator.calculate_simple_total(log_file)
114
111
 
115
- # Get uptime from directory creation time
116
- start_time = File.stat(session_dir).ctime
112
+ # Get uptime from session metadata or fallback to directory creation time
113
+ start_time = get_start_time(session_dir)
117
114
  uptime = format_duration(Time.now - start_time)
118
115
 
119
116
  {
@@ -122,24 +119,25 @@ module ClaudeSwarm
122
119
  cost: total_cost,
123
120
  uptime: uptime,
124
121
  directory: directories_str,
125
- start_time: start_time
122
+ start_time: start_time,
126
123
  }
127
124
  rescue StandardError
128
125
  nil
129
126
  end
130
127
 
131
- def calculate_total_cost(session_dir)
132
- log_file = File.join(session_dir, "session.log.json")
133
- return 0.0 unless File.exist?(log_file)
134
-
135
- total = 0.0
136
- File.foreach(log_file) do |line|
137
- data = JSON.parse(line)
138
- total += data["event"]["total_cost_usd"] if data.dig("event", "type") == "result" && data.dig("event", "total_cost_usd")
139
- rescue JSON::ParserError
140
- next
128
+ def get_start_time(session_dir)
129
+ # Try to get from session metadata first
130
+ metadata_file = File.join(session_dir, "session_metadata.json")
131
+ if File.exist?(metadata_file)
132
+ metadata = JSON.parse(File.read(metadata_file))
133
+ return Time.parse(metadata["start_time"]) if metadata["start_time"]
141
134
  end
142
- total
135
+
136
+ # Fallback to directory creation time
137
+ File.stat(session_dir).ctime
138
+ rescue StandardError
139
+ # If anything fails, use directory creation time
140
+ File.stat(session_dir).ctime
143
141
  end
144
142
 
145
143
  def format_duration(seconds)
@@ -1,8 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
- require "json"
5
-
6
3
  module ClaudeSwarm
7
4
  module Commands
8
5
  class Show
@@ -10,7 +7,7 @@ module ClaudeSwarm
10
7
  session_path = find_session_path(session_id)
11
8
  unless session_path
12
9
  puts "Session not found: #{session_id}"
13
- exit 1
10
+ exit(1)
14
11
  end
15
12
 
16
13
  # Load config to get main instance name
@@ -18,19 +15,26 @@ module ClaudeSwarm
18
15
  main_instance_name = config.dig("swarm", "main")
19
16
 
20
17
  # Parse all events to build instance data
21
- instances = parse_instance_hierarchy(session_path, main_instance_name)
18
+ log_file = File.join(session_path, "session.log.json")
19
+ instances = SessionCostCalculator.parse_instance_hierarchy(log_file)
22
20
 
23
21
  # Calculate total cost (excluding main if not available)
24
22
  total_cost = instances.values.sum { |i| i[:cost] }
25
23
  cost_display = if instances[main_instance_name] && instances[main_instance_name][:has_cost_data]
26
- format("$%.4f", total_cost)
27
- else
28
- "#{format("$%.4f", total_cost)} (excluding main instance)"
29
- end
24
+ format("$%.4f", total_cost)
25
+ else
26
+ "#{format("$%.4f", total_cost)} (excluding main instance)"
27
+ end
30
28
 
31
29
  # Display session info
32
30
  puts "Session: #{session_id}"
31
+ puts "Session Path: #{session_path}"
33
32
  puts "Swarm: #{config.dig("swarm", "name")}"
33
+
34
+ # Display runtime if available
35
+ runtime_info = get_runtime_info(session_path)
36
+ puts "Runtime: #{runtime_info}" if runtime_info
37
+
34
38
  puts "Total Cost: #{cost_display}"
35
39
 
36
40
  # Try to read start directory
@@ -71,58 +75,36 @@ module ClaudeSwarm
71
75
  end
72
76
  end
73
77
 
74
- def parse_instance_hierarchy(session_path, _main_instance_name)
75
- log_file = File.join(session_path, "session.log.json")
76
- instances = {}
77
-
78
- return instances unless File.exist?(log_file)
79
-
80
- File.foreach(log_file) do |line|
81
- data = JSON.parse(line)
82
- instance_name = data["instance"]
83
- instance_id = data["instance_id"]
84
- calling_instance = data["calling_instance"]
85
-
86
- # Initialize instance data
87
- instances[instance_name] ||= {
88
- name: instance_name,
89
- id: instance_id,
90
- cost: 0.0,
91
- calls: 0,
92
- called_by: Set.new,
93
- calls_to: Set.new,
94
- has_cost_data: false
95
- }
96
-
97
- # Track relationships
98
- if calling_instance && calling_instance != instance_name
99
- instances[instance_name][:called_by] << calling_instance
100
-
101
- instances[calling_instance] ||= {
102
- name: calling_instance,
103
- id: data["calling_instance_id"],
104
- cost: 0.0,
105
- calls: 0,
106
- called_by: Set.new,
107
- calls_to: Set.new,
108
- has_cost_data: false
109
- }
110
- instances[calling_instance][:calls_to] << instance_name
111
- end
112
-
113
- # Track costs and calls
114
- if data.dig("event", "type") == "result"
115
- instances[instance_name][:calls] += 1
116
- if (cost = data.dig("event", "total_cost_usd"))
117
- instances[instance_name][:cost] += cost
118
- instances[instance_name][:has_cost_data] = true
119
- end
120
- end
121
- rescue JSON::ParserError
122
- next
78
+ def get_runtime_info(session_path)
79
+ metadata_file = File.join(session_path, "session_metadata.json")
80
+ return unless File.exist?(metadata_file)
81
+
82
+ metadata = JSON.parse(File.read(metadata_file))
83
+
84
+ if metadata["duration_seconds"]
85
+ # Session has completed
86
+ format_duration(metadata["duration_seconds"])
87
+ elsif metadata["start_time"]
88
+ # Session is still running or was interrupted
89
+ start_time = Time.parse(metadata["start_time"])
90
+ duration = (Time.now - start_time).to_i
91
+ "#{format_duration(duration)} (active)"
123
92
  end
93
+ rescue StandardError
94
+ nil
95
+ end
124
96
 
125
- instances
97
+ def format_duration(seconds)
98
+ hours = seconds / 3600
99
+ minutes = (seconds % 3600) / 60
100
+ secs = seconds % 60
101
+
102
+ parts = []
103
+ parts << "#{hours}h" if hours.positive?
104
+ parts << "#{minutes}m" if minutes.positive?
105
+ parts << "#{secs}s"
106
+
107
+ parts.join(" ")
126
108
  end
127
109
 
128
110
  def display_instance_tree(instance, all_instances, level, main_instance_name)
@@ -137,10 +119,10 @@ module ClaudeSwarm
137
119
 
138
120
  # Display cost - show n/a for main instance without cost data
139
121
  cost_display = if instance[:name] == main_instance_name && !instance[:has_cost_data]
140
- "n/a (interactive)"
141
- else
142
- format("$%.4f", instance[:cost])
143
- end
122
+ "n/a (interactive)"
123
+ else
124
+ format("$%.4f", instance[:cost])
125
+ end
144
126
 
145
127
  puts "#{indent} Cost: #{cost_display} | Calls: #{instance[:calls]}"
146
128
 
@@ -1,16 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "yaml"
4
- require "pathname"
5
-
6
3
  module ClaudeSwarm
7
4
  class Configuration
5
+ # Frozen constants for validation
6
+ VALID_PROVIDERS = ["claude", "openai"].freeze
7
+ OPENAI_SPECIFIC_FIELDS = ["temperature", "api_version", "openai_token_env", "base_url", "reasoning_effort"].freeze
8
+ VALID_API_VERSIONS = ["chat_completion", "responses"].freeze
9
+ VALID_REASONING_EFFORTS = ["low", "medium", "high"].freeze
10
+
11
+ # Regex patterns
12
+ ENV_VAR_PATTERN = /\$\{([^}]+)\}/
13
+ ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
14
+ O_SERIES_MODEL_PATTERN = /^o\d+(\s+(Preview|preview))?(-pro|-mini|-deep-research|-mini-deep-research)?$/
15
+
8
16
  attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances
9
17
 
10
- def initialize(config_path, base_dir: nil)
18
+ def initialize(config_path, base_dir: nil, options: {})
11
19
  @config_path = Pathname.new(config_path).expand_path
12
20
  @config_dir = @config_path.dirname
13
21
  @base_dir = base_dir || @config_dir
22
+ @options = options
14
23
  load_and_validate
15
24
  end
16
25
 
@@ -30,10 +39,15 @@ module ClaudeSwarm
30
39
  @swarm["before"] || []
31
40
  end
32
41
 
42
+ def after_commands
43
+ @swarm["after"] || []
44
+ end
45
+
33
46
  private
34
47
 
35
48
  def load_and_validate
36
49
  @config = YAML.load_file(@config_path)
50
+ interpolate_env_vars!(@config)
37
51
  validate_version
38
52
  validate_swarm
39
53
  parse_swarm
@@ -44,6 +58,35 @@ module ClaudeSwarm
44
58
  raise Error, "Invalid YAML syntax: #{e.message}"
45
59
  end
46
60
 
61
+ def interpolate_env_vars!(obj)
62
+ case obj
63
+ when String
64
+ interpolate_env_string(obj)
65
+ when Hash
66
+ obj.transform_values! { |v| interpolate_env_vars!(v) }
67
+ when Array
68
+ obj.map! { |v| interpolate_env_vars!(v) }
69
+ else
70
+ obj
71
+ end
72
+ end
73
+
74
+ def interpolate_env_string(str)
75
+ str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
76
+ env_var = Regexp.last_match(1)
77
+ has_default = Regexp.last_match(2)
78
+ default_value = Regexp.last_match(3)
79
+
80
+ if ENV.key?(env_var)
81
+ ENV[env_var]
82
+ elsif has_default
83
+ default_value || ""
84
+ else
85
+ raise Error, "Environment variable '#{env_var}' is not set"
86
+ end
87
+ end
88
+ end
89
+
47
90
  def validate_version
48
91
  version = @config["version"]
49
92
  raise Error, "Missing 'version' field in configuration" unless version
@@ -72,8 +115,11 @@ module ClaudeSwarm
72
115
  @swarm["instances"].each do |name, config|
73
116
  @instances[name] = parse_instance(name, config)
74
117
  end
118
+ validate_main_instance_provider
75
119
  validate_connections
76
120
  detect_circular_dependencies
121
+ validate_openai_env_vars
122
+ validate_openai_responses_api_compatibility
77
123
  end
78
124
 
79
125
  def parse_instance(name, config)
@@ -82,6 +128,52 @@ module ClaudeSwarm
82
128
  # Validate required fields
83
129
  raise Error, "Instance '#{name}' missing required 'description' field" unless config["description"]
84
130
 
131
+ # Parse provider (optional, defaults to claude)
132
+ provider = config["provider"]
133
+ model = config["model"]
134
+
135
+ # Validate provider value if specified
136
+ if provider && !VALID_PROVIDERS.include?(provider)
137
+ raise Error, "Instance '#{name}' has invalid provider '#{provider}'. Must be 'claude' or 'openai'"
138
+ end
139
+
140
+ # Validate reasoning_effort for OpenAI provider
141
+ if config["reasoning_effort"]
142
+ # Ensure it's only used with OpenAI provider
143
+ if provider != "openai"
144
+ raise Error, "Instance '#{name}' has reasoning_effort but provider is not 'openai'"
145
+ end
146
+
147
+ # Validate the value
148
+ unless VALID_REASONING_EFFORTS.include?(config["reasoning_effort"])
149
+ raise Error, "Instance '#{name}' has invalid reasoning_effort '#{config["reasoning_effort"]}'. Must be 'low', 'medium', or 'high'"
150
+ end
151
+
152
+ # Validate it's only used with o-series models
153
+ # Support patterns like: o1, o1-mini, o1-pro, o1 Preview, o3-deep-research, o4-mini-deep-research, etc.
154
+ unless model&.match?(O_SERIES_MODEL_PATTERN)
155
+ raise Error, "Instance '#{name}' has reasoning_effort but model '#{model}' is not an o-series model (o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, etc.)"
156
+ end
157
+ end
158
+
159
+ # Validate temperature is not used with o-series models when provider is openai
160
+ if provider == "openai" && config["temperature"] && model&.match?(O_SERIES_MODEL_PATTERN)
161
+ raise Error, "Instance '#{name}' has temperature parameter but model '#{model}' is an o-series model. O-series models use deterministic reasoning and don't accept temperature settings"
162
+ end
163
+
164
+ # Validate OpenAI-specific fields only when provider is not "openai"
165
+ if provider != "openai"
166
+ invalid_fields = OPENAI_SPECIFIC_FIELDS & config.keys
167
+ unless invalid_fields.empty?
168
+ raise Error, "Instance '#{name}' has OpenAI-specific fields #{invalid_fields.join(", ")} but provider is not 'openai'"
169
+ end
170
+ end
171
+
172
+ # Validate api_version if specified
173
+ if config["api_version"] && !VALID_API_VERSIONS.include?(config["api_version"])
174
+ raise Error, "Instance '#{name}' has invalid api_version '#{config["api_version"]}'. Must be 'chat_completion' or 'responses'"
175
+ end
176
+
85
177
  # Validate tool fields are arrays if present
86
178
  validate_tool_field(name, config, "tools")
87
179
  validate_tool_field(name, config, "allowed_tools")
@@ -93,7 +185,7 @@ module ClaudeSwarm
93
185
  # Parse directory field - support both string and array
94
186
  directories = parse_directories(config["directory"])
95
187
 
96
- {
188
+ instance_config = {
97
189
  name: name,
98
190
  directory: directories.first, # Keep single directory for backward compatibility
99
191
  directories: directories, # New field with all directories
@@ -105,9 +197,26 @@ module ClaudeSwarm
105
197
  mcps: parse_mcps(config["mcps"] || []),
106
198
  prompt: config["prompt"],
107
199
  description: config["description"],
108
- vibe: config["vibe"] || false,
109
- worktree: parse_worktree_value(config["worktree"])
200
+ vibe: config["vibe"],
201
+ worktree: parse_worktree_value(config["worktree"]),
202
+ provider: provider, # nil means Claude (default)
110
203
  }
204
+
205
+ # Add OpenAI-specific fields only when provider is "openai"
206
+ if provider == "openai"
207
+ instance_config[:temperature] = config["temperature"] if config["temperature"]
208
+ instance_config[:api_version] = config["api_version"] || "chat_completion"
209
+ instance_config[:openai_token_env] = config["openai_token_env"] || "OPENAI_API_KEY"
210
+ instance_config[:base_url] = config["base_url"]
211
+ instance_config[:reasoning_effort] = config["reasoning_effort"] if config["reasoning_effort"]
212
+ # Default vibe to true for OpenAI instances if not specified
213
+ instance_config[:vibe] = true if config["vibe"].nil?
214
+ elsif config["vibe"].nil?
215
+ # Default vibe to false for Claude instances if not specified
216
+ instance_config[:vibe] = false
217
+ end
218
+
219
+ instance_config
111
220
  end
112
221
 
113
222
  def parse_mcps(mcps)
@@ -195,11 +304,55 @@ module ClaudeSwarm
195
304
  end
196
305
 
197
306
  def parse_worktree_value(value)
198
- return nil if value.nil? # Omitted means follow CLI behavior
307
+ return if value.nil? # Omitted means follow CLI behavior
199
308
  return value if value.is_a?(TrueClass) || value.is_a?(FalseClass)
200
309
  return value.to_s if value.is_a?(String) && !value.empty?
201
310
 
202
311
  raise Error, "Invalid worktree value: #{value.inspect}. Must be true, false, or a non-empty string"
203
312
  end
313
+
314
+ def validate_openai_env_vars
315
+ @instances.each_value do |instance|
316
+ next unless instance[:provider] == "openai"
317
+
318
+ env_var = instance[:openai_token_env]
319
+ unless ENV.key?(env_var) && !ENV[env_var].to_s.strip.empty?
320
+ raise Error, "Environment variable '#{env_var}' is not set. OpenAI provider instances require an API key."
321
+ end
322
+ end
323
+ end
324
+
325
+ def validate_main_instance_provider
326
+ # Only validate in interactive mode (when no prompt is provided)
327
+ return if @options[:prompt]
328
+
329
+ main_config = @instances[@main_instance]
330
+ if main_config[:provider]
331
+ raise Error, "Main instance '#{@main_instance}' cannot have a provider setting in interactive mode"
332
+ end
333
+ end
334
+
335
+ def validate_openai_responses_api_compatibility
336
+ # Check if any instance uses OpenAI provider with responses API
337
+ responses_api_instances = @instances.select do |_name, instance|
338
+ instance[:provider] == "openai" && instance[:api_version] == "responses"
339
+ end
340
+
341
+ return if responses_api_instances.empty?
342
+
343
+ # Check ruby-openai version
344
+ begin
345
+ require "openai/version"
346
+ openai_version = Gem::Version.new(::OpenAI::VERSION)
347
+ required_version = Gem::Version.new("8.0.0")
348
+
349
+ if openai_version < required_version
350
+ instance_names = responses_api_instances.keys.join(", ")
351
+ raise Error, "Instances #{instance_names} use OpenAI provider with api_version 'responses', which requires ruby-openai >= 8.0. Current version is #{openai_version}. Please update your Gemfile or run: gem install ruby-openai -v '>= 8.0'"
352
+ end
353
+ rescue LoadError
354
+ # ruby-openai is not installed, which is fine - it will be caught later when trying to use it
355
+ end
356
+ end
204
357
  end
205
358
  end
@@ -1,10 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
- require "fileutils"
5
- require "shellwords"
6
- require "securerandom"
7
-
8
3
  module ClaudeSwarm
9
4
  class McpGenerator
10
5
  def initialize(configuration, vibe: false, restore_session_path: nil)
@@ -62,15 +57,20 @@ module ClaudeSwarm
62
57
  instance[:connections].each do |connection_name|
63
58
  connected_instance = @config.instances[connection_name]
64
59
  mcp_servers[connection_name] = build_instance_mcp_config(
65
- connection_name, connected_instance,
66
- calling_instance: name, calling_instance_id: @instance_ids[name]
60
+ connection_name,
61
+ connected_instance,
62
+ calling_instance: name,
63
+ calling_instance_id: @instance_ids[name],
67
64
  )
68
65
  end
69
66
 
67
+ # Add Claude tools MCP server for OpenAI instances
68
+ mcp_servers["claude_tools"] = build_claude_tools_mcp_config if instance[:provider] == "openai"
69
+
70
70
  config = {
71
71
  "instance_id" => @instance_ids[name],
72
72
  "instance_name" => name,
73
- "mcpServers" => mcp_servers
73
+ "mcpServers" => mcp_servers,
74
74
  }
75
75
 
76
76
  File.write(mcp_config_path(name), JSON.pretty_generate(config))
@@ -82,18 +82,26 @@ module ClaudeSwarm
82
82
  {
83
83
  "type" => "stdio",
84
84
  "command" => mcp["command"],
85
- "args" => mcp["args"] || []
85
+ "args" => mcp["args"] || [],
86
86
  }.tap do |config|
87
87
  config["env"] = mcp["env"] if mcp["env"]
88
88
  end
89
89
  when "sse"
90
90
  {
91
91
  "type" => "sse",
92
- "url" => mcp["url"]
92
+ "url" => mcp["url"],
93
93
  }
94
94
  end
95
95
  end
96
96
 
97
+ def build_claude_tools_mcp_config
98
+ {
99
+ "type" => "stdio",
100
+ "command" => "claude",
101
+ "args" => ["mcp", "serve"],
102
+ }
103
+ end
104
+
97
105
  def build_instance_mcp_config(name, instance, calling_instance:, calling_instance_id:)
98
106
  # Get the path to the claude-swarm executable
99
107
  exe_path = "claude-swarm"
@@ -101,9 +109,12 @@ module ClaudeSwarm
101
109
  # Build command-line arguments for Thor
102
110
  args = [
103
111
  "mcp-serve",
104
- "--name", name,
105
- "--directory", instance[:directory],
106
- "--model", instance[:model]
112
+ "--name",
113
+ name,
114
+ "--directory",
115
+ instance[:directory],
116
+ "--model",
117
+ instance[:model],
107
118
  ]
108
119
 
109
120
  # Add directories array if we have multiple directories
@@ -130,6 +141,20 @@ module ClaudeSwarm
130
141
 
131
142
  args.push("--vibe") if @vibe || instance[:vibe]
132
143
 
144
+ # Add provider-specific parameters
145
+ if instance[:provider]
146
+ args.push("--provider", instance[:provider])
147
+
148
+ # Add OpenAI-specific parameters
149
+ if instance[:provider] == "openai"
150
+ args.push("--reasoning-effort", instance[:reasoning_effort]) if instance[:reasoning_effort]
151
+ args.push("--temperature", instance[:temperature].to_s) if instance[:temperature]
152
+ args.push("--api-version", instance[:api_version]) if instance[:api_version]
153
+ args.push("--openai-token-env", instance[:openai_token_env]) if instance[:openai_token_env]
154
+ args.push("--base-url", instance[:base_url]) if instance[:base_url]
155
+ end
156
+ end
157
+
133
158
  # Add claude session ID if restoring
134
159
  if @restore_states[name.to_s]
135
160
  claude_session_id = @restore_states[name.to_s]["claude_session_id"]
@@ -139,7 +164,7 @@ module ClaudeSwarm
139
164
  {
140
165
  "type" => "stdio",
141
166
  "command" => exe_path,
142
- "args" => args
167
+ "args" => args,
143
168
  }
144
169
  end
145
170