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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b13f053c52c3e4ff662746f5027fcc6005be35ab6621c827b5fac205bb69b917
4
- data.tar.gz: 70d1fbedfa25a81060259f37b4de2d47c740d8719171ce956a5462ce022359d2
3
+ metadata.gz: a256a8d09a86290b06002437b38bc9092b7d83c1e4d1e0f4866b37cd1251bd99
4
+ data.tar.gz: d5eeec5e3a350dac2a04860c64c00043a8618fd29d40b9e1acf77e86b2f3fd49
5
5
  SHA512:
6
- metadata.gz: 60b9dbc2a41ae296a4a4d844c5dbf1195e26a38a4ce90aeb72a23f86467e94bc803e5b7488f8189829ad5a898a0887d1a639dc888adf24af87c4d391346709ac
7
- data.tar.gz: 4935a7b7001b78d0a8ae1f4f732f61aed8d4fc35859478615f035edd2411ea5f9b5df3be1e26f910ad8ce8943c750d3b51f22adf3c105bbd97c5a36b17b072e9
6
+ metadata.gz: 3bb8119400be33d056b795aaea4f7f3d045d85a29d830cc42fe8c3dba0fd406100553b68740fe884e2e769e9685c439983d3a38ee52f55730f9f1ff7b40ddbff
7
+ data.tar.gz: a2dd8483358b673698e7ca3a71d1952fa0b2c85d0daf9cd2ff06c5f62d78f7ffebacc95a0e622cfb3f62f229e81c8177c5d6b263303b88c26c592ba53e7c7607
data/CHANGELOG.md CHANGED
@@ -1,3 +1,29 @@
1
+ ## [0.3.8]
2
+
3
+ ### Added
4
+ - **Hooks support**: Claude Swarm now supports configuring Claude Code hooks for each instance
5
+ - Configure hooks directly in the YAML configuration file using Claude Code's format
6
+ - Each instance can have its own hooks configuration (PreToolUse, PostToolUse, UserPromptSubmit, etc.)
7
+ - Automatically generates `settings.json` files in the session directory when hooks are configured
8
+ - Main instance receives hooks via `--settings` CLI flag
9
+ - Connected instances receive hooks via SDK's `settings` attribute
10
+ - Full environment variable interpolation support in hook configurations
11
+ - See README.md "Hooks Configuration" section for usage examples
12
+
13
+ ### Fixed
14
+ - **Settings integration**: Fixed passing settings to Claude instances
15
+ - Corrected SDK attribute name from `settings_path` to `settings`
16
+ - Added missing `--settings` flag for main instance CLI command
17
+
18
+ ## [0.3.7]
19
+
20
+ ### Added
21
+ - **Main instance logging**: Captures main Claude instance output in `session.log` with prettified JSON format
22
+
23
+ ### Changed
24
+ - **Updated claude-code-sdk-ruby dependency**: Bumped from 0.1.4 to 0.1.6
25
+ - Includes latest SDK improvements and bug fixes
26
+
1
27
  ## [0.3.6]
2
28
 
3
29
  ### Added
data/CLAUDE.md CHANGED
@@ -130,9 +130,10 @@ The gem is fully implemented with the following components:
130
130
  1. User creates a `claude-swarm.yml` file defining the swarm topology
131
131
  2. Running `claude-swarm` parses the configuration and validates it
132
132
  3. MCP configuration files are generated for each instance in a session directory
133
- 4. The main instance is launched with `exec`, replacing the current process
134
- 5. Connected instances are available as MCP servers to the main instance
135
- 6. When an instance has connections, those connections are automatically added to its allowed tools as `mcp__<connection_name>`
133
+ 4. Settings files (with hooks) are generated for each instance if hooks are configured
134
+ 5. The main instance is launched with `exec`, replacing the current process
135
+ 6. Connected instances are available as MCP servers to the main instance
136
+ 7. When an instance has connections, those connections are automatically added to its allowed tools as `mcp__<connection_name>`
136
137
 
137
138
  ### Configuration Example
138
139
 
@@ -159,6 +160,52 @@ swarm:
159
160
  worktree: false # Optional: disable worktree for this instance
160
161
  ```
161
162
 
163
+ ### Hooks Support
164
+
165
+ Claude Swarm supports configuring [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) for each instance. This allows you to run custom scripts before/after tools, on prompt submission, and more.
166
+
167
+ #### Configuration Example with Hooks
168
+
169
+ ```yaml
170
+ version: 1
171
+ swarm:
172
+ name: "Dev Team"
173
+ main: lead
174
+ instances:
175
+ lead:
176
+ description: "Lead developer"
177
+ directory: .
178
+ model: opus
179
+ # Hooks configuration follows Claude Code's format exactly
180
+ hooks:
181
+ PreToolUse:
182
+ - matcher: "Write|Edit"
183
+ hooks:
184
+ - type: "command"
185
+ command: "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-code.py"
186
+ timeout: 10
187
+ PostToolUse:
188
+ - matcher: "Bash"
189
+ hooks:
190
+ - type: "command"
191
+ command: "echo 'Command executed by lead' >> /tmp/lead.log"
192
+ UserPromptSubmit:
193
+ - hooks:
194
+ - type: "command"
195
+ command: "$CLAUDE_PROJECT_DIR/.claude/hooks/add-context.py"
196
+ frontend:
197
+ description: "Frontend developer"
198
+ directory: ./frontend
199
+ hooks:
200
+ PreToolUse:
201
+ - matcher: "Write"
202
+ hooks:
203
+ - type: "command"
204
+ command: "npm run lint"
205
+ ```
206
+
207
+ The hooks configuration is passed directly to Claude Code via a generated settings.json file in the session directory. Each instance gets its own settings file with its specific hooks.
208
+
162
209
  ## Testing
163
210
 
164
211
  The gem includes comprehensive tests covering:
data/README.md CHANGED
@@ -303,6 +303,7 @@ Each instance can have:
303
303
  - **vibe**: Enable vibe mode (--dangerously-skip-permissions) for this instance (default: false)
304
304
  - **worktree**: Configure Git worktree usage for this instance (true/false/string)
305
305
  - **provider**: AI provider to use - "claude" (default) or "openai"
306
+ - **hooks**: Configure Claude Code hooks for this instance (see Hooks Configuration section below)
306
307
 
307
308
  #### OpenAI Provider Configuration
308
309
 
@@ -394,6 +395,69 @@ mcps:
394
395
  X-Custom-Header: "value"
395
396
  ```
396
397
 
398
+ ### Hooks Configuration
399
+
400
+ Claude Swarm supports configuring [Claude Code hooks](https://docs.anthropic.com/en/docs/claude-code/hooks) for each instance. Hooks allow you to run custom scripts before/after tools, on prompt submission, and more. Each instance can have its own hooks configuration.
401
+
402
+ #### Supported Hook Events
403
+
404
+ - **PreToolUse**: Run before a tool is executed
405
+ - **PostToolUse**: Run after a tool completes
406
+ - **UserPromptSubmit**: Run when a user prompt is submitted
407
+ - **Stop**: Run when the Claude instance stops
408
+ - **SessionStart**: Run when a session starts
409
+ - **And more...** (see Claude Code hooks documentation)
410
+
411
+ #### Configuration Example
412
+
413
+ ```yaml
414
+ instances:
415
+ lead:
416
+ description: "Lead developer"
417
+ directory: .
418
+ # Hooks configuration follows Claude Code's format exactly
419
+ hooks:
420
+ PreToolUse:
421
+ - matcher: "Write|Edit"
422
+ hooks:
423
+ - type: "command"
424
+ command: "$CLAUDE_PROJECT_DIR/.claude/hooks/validate-code.py"
425
+ timeout: 10
426
+ PostToolUse:
427
+ - matcher: "Bash"
428
+ hooks:
429
+ - type: "command"
430
+ command: "echo 'Bash executed by ${INSTANCE_NAME}' >> ${LOG_DIR}/commands.log"
431
+ UserPromptSubmit:
432
+ - hooks:
433
+ - type: "command"
434
+ command: "${HOOKS_DIR:=$CLAUDE_PROJECT_DIR/.claude/hooks}/add-context.py"
435
+ frontend:
436
+ description: "Frontend developer"
437
+ directory: ./frontend
438
+ hooks:
439
+ PreToolUse:
440
+ - matcher: "Write"
441
+ hooks:
442
+ - type: "command"
443
+ command: "npm run lint"
444
+ timeout: 5
445
+ ```
446
+
447
+ #### How It Works
448
+
449
+ 1. Define hooks in your instance configuration using the exact format expected by Claude Code
450
+ 2. Claude Swarm generates a `settings.json` file for each instance with hooks
451
+ 3. The settings file is passed to Claude Code SDK via the `--settings` parameter
452
+ 4. Each instance runs with its own hooks configuration
453
+
454
+ #### Environment Variables in Hooks
455
+
456
+ Hooks have access to standard Claude Code environment variables plus:
457
+ - `$CLAUDE_PROJECT_DIR` - The project directory
458
+ - `$CLAUDE_SWARM_SESSION_DIR` - The swarm session directory
459
+ - `$CLAUDE_SWARM_INSTANCE_NAME` - The name of the current instance
460
+
397
461
  ### Tools
398
462
 
399
463
  Specify which tools each instance can use:
@@ -0,0 +1,37 @@
1
+ version: 1
2
+ swarm:
3
+ name: "Simple Session Hook Swarm"
4
+ main: developer
5
+ instances:
6
+ developer:
7
+ description: "Main developer instance"
8
+ directory: .
9
+ model: sonnet
10
+ allowed_tools:
11
+ - Read
12
+ - Edit
13
+ - Write
14
+ - Bash
15
+ connections: [session_tracker]
16
+ prompt: |
17
+ You are the main developer. You can delegate session tracking tasks to the session_tracker instance.
18
+
19
+ For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
20
+
21
+ session_tracker:
22
+ description: "Session tracking specialist that monitors and logs session information"
23
+ directory: .
24
+ model: sonnet
25
+ allowed_tools:
26
+ - Write
27
+ - Bash
28
+ hooks:
29
+ SessionStart:
30
+ - hooks:
31
+ - type: "command"
32
+ command: "cat > session_id.txt"
33
+ timeout: 5
34
+ prompt: |
35
+ You specialize in session tracking and monitoring. You automatically create session tracking files when you start.
36
+
37
+ For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ class BaseExecutor
5
+ attr_reader :session_id, :last_response, :working_directory, :logger, :session_path, :session_json_path, :instance_info
6
+
7
+ def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false,
8
+ instance_name: nil, instance_id: nil, calling_instance: nil, calling_instance_id: nil,
9
+ claude_session_id: nil, additional_directories: [], debug: false)
10
+ @working_directory = working_directory
11
+ @additional_directories = additional_directories
12
+ @model = model
13
+ @mcp_config = mcp_config
14
+ @vibe = vibe
15
+ @session_id = claude_session_id
16
+ @last_response = nil
17
+ @instance_name = instance_name
18
+ @instance_id = instance_id
19
+ @calling_instance = calling_instance
20
+ @calling_instance_id = calling_instance_id
21
+ @debug = debug
22
+
23
+ # Setup static info strings for logging
24
+ @instance_info = build_info(@instance_name, @instance_id)
25
+ @caller_info = build_info(@calling_instance, @calling_instance_id)
26
+ @caller_to_instance = "#{@caller_info} -> #{instance_info}:"
27
+ @instance_to_caller = "#{instance_info} -> #{@caller_info}:"
28
+
29
+ # Setup logging
30
+ setup_logging
31
+
32
+ # Setup static event templates
33
+ setup_event_templates
34
+ end
35
+
36
+ def execute(_prompt, _options = {})
37
+ raise NotImplementedError, "Subclasses must implement the execute method"
38
+ end
39
+
40
+ def reset_session
41
+ @session_id = nil
42
+ @last_response = nil
43
+ end
44
+
45
+ def has_session?
46
+ !@session_id.nil?
47
+ end
48
+
49
+ protected
50
+
51
+ def build_info(name, id)
52
+ return name unless id
53
+
54
+ "#{name} (#{id})"
55
+ end
56
+
57
+ def setup_logging
58
+ # Use session path from environment (required)
59
+ @session_path = SessionPath.from_env
60
+ SessionPath.ensure_directory(@session_path)
61
+
62
+ # Initialize session JSON path
63
+ @session_json_path = File.join(@session_path, "session.log.json")
64
+
65
+ # Create logger with session.log filename
66
+ log_filename = "session.log"
67
+ log_path = File.join(@session_path, log_filename)
68
+ log_level = @debug ? :debug : :info
69
+ @logger = Logger.new(log_path, level: log_level, progname: @instance_name)
70
+
71
+ logger.info { "Started #{self.class.name} for instance: #{instance_info}" }
72
+ end
73
+
74
+ def setup_event_templates
75
+ @log_request_event_template = {
76
+ type: "request",
77
+ from_instance: @calling_instance,
78
+ from_instance_id: @calling_instance_id,
79
+ to_instance: @instance_name,
80
+ to_instance_id: @instance_id,
81
+ }.freeze
82
+
83
+ @session_json_entry_template = {
84
+ instance: @instance_name,
85
+ instance_id: @instance_id,
86
+ calling_instance: @calling_instance,
87
+ calling_instance_id: @calling_instance_id,
88
+ }.freeze
89
+ end
90
+
91
+ def log_request(prompt)
92
+ logger.info { "#{@caller_to_instance} \n---\n#{prompt}\n---" }
93
+
94
+ # Merge dynamic data with static template
95
+ event = @log_request_event_template.merge(
96
+ prompt: prompt,
97
+ timestamp: Time.now.iso8601,
98
+ )
99
+
100
+ append_to_session_json(event)
101
+ end
102
+
103
+ def log_response(response)
104
+ logger.info do
105
+ "($#{response["total_cost"]} - #{response["duration_ms"]}ms) #{@instance_to_caller} \n---\n#{response["result"]}\n---"
106
+ end
107
+ end
108
+
109
+ def append_to_session_json(event)
110
+ # Use file locking to ensure thread-safe writes
111
+ File.open(@session_json_path, File::WRONLY | File::APPEND | File::CREAT) do |file|
112
+ file.flock(File::LOCK_EX)
113
+
114
+ # Merge dynamic data with static template
115
+ entry = @session_json_entry_template.merge(
116
+ timestamp: Time.now.iso8601,
117
+ event: event,
118
+ )
119
+
120
+ # Write as single line JSON (JSONL format)
121
+ file.puts(entry.to_json)
122
+
123
+ file.flock(File::LOCK_UN)
124
+ end
125
+ rescue StandardError => e
126
+ logger.error { "Failed to append to session JSON: #{e.message}" }
127
+ raise
128
+ end
129
+
130
+ class ExecutionError < StandardError; end
131
+ class ParseError < StandardError; end
132
+ end
133
+ end
@@ -1,28 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- class ClaudeCodeExecutor
5
- attr_reader :session_id, :last_response, :working_directory, :logger, :session_path
6
-
7
- def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false,
8
- instance_name: nil, instance_id: nil, calling_instance: nil, calling_instance_id: nil,
9
- claude_session_id: nil, additional_directories: [])
10
- @working_directory = working_directory
11
- @additional_directories = additional_directories
12
- @model = model
13
- @mcp_config = mcp_config
14
- @vibe = vibe
15
- @session_id = claude_session_id
16
- @last_response = nil
17
- @instance_name = instance_name
18
- @instance_id = instance_id
19
- @calling_instance = calling_instance
20
- @calling_instance_id = calling_instance_id
21
-
22
- # Setup logging
23
- setup_logging
24
- end
25
-
4
+ class ClaudeCodeExecutor < BaseExecutor
26
5
  def execute(prompt, options = {})
27
6
  # Log the request
28
7
  log_request(prompt)
@@ -75,8 +54,8 @@ module ClaudeSwarm
75
54
  end
76
55
  end
77
56
  rescue StandardError => e
78
- @logger.error("Execution error for #{@instance_name}: #{e.class} - #{e.message}")
79
- @logger.error("Backtrace: #{e.backtrace.join("\n")}")
57
+ logger.error { "Execution error for #{@instance_name}: #{e.class} - #{e.message}" }
58
+ logger.error { "Backtrace: #{e.backtrace.join("\n")}" }
80
59
  raise ExecutionError, "Claude Code execution failed: #{e.message}"
81
60
  end
82
61
 
@@ -90,20 +69,11 @@ module ClaudeSwarm
90
69
 
91
70
  result_response
92
71
  rescue StandardError => e
93
- @logger.error("Unexpected error for #{@instance_name}: #{e.class} - #{e.message}")
94
- @logger.error("Backtrace: #{e.backtrace.join("\n")}")
72
+ logger.error { "Unexpected error for #{@instance_name}: #{e.class} - #{e.message}" }
73
+ logger.error { "Backtrace: #{e.backtrace.join("\n")}" }
95
74
  raise
96
75
  end
97
76
 
98
- def reset_session
99
- @session_id = nil
100
- @last_response = nil
101
- end
102
-
103
- def has_session?
104
- !@session_id.nil?
105
- end
106
-
107
77
  private
108
78
 
109
79
  def write_instance_state
@@ -122,63 +92,9 @@ module ClaudeSwarm
122
92
  }
123
93
 
124
94
  File.write(state_file, JSON.pretty_generate(state_data))
125
- @logger.info("Wrote instance state for #{@instance_name} (#{@instance_id}) with session ID: #{@session_id}")
95
+ logger.info { "Wrote instance state for #{@instance_name} (#{@instance_id}) with session ID: #{@session_id}" }
126
96
  rescue StandardError => e
127
- @logger.error("Failed to write instance state for #{@instance_name} (#{@instance_id}): #{e.message}")
128
- end
129
-
130
- def setup_logging
131
- # Use session path from environment (required)
132
- @session_path = SessionPath.from_env
133
- SessionPath.ensure_directory(@session_path)
134
-
135
- # Create logger with session.log filename
136
- log_filename = "session.log"
137
- log_path = File.join(@session_path, log_filename)
138
- @logger = Logger.new(log_path)
139
- @logger.level = Logger::INFO
140
-
141
- # Custom formatter for better readability
142
- @logger.formatter = proc do |severity, datetime, _progname, msg|
143
- "[#{datetime.strftime("%Y-%m-%d %H:%M:%S.%L")}] [#{severity}] #{msg}\n"
144
- end
145
-
146
- return unless @instance_name
147
-
148
- instance_info = @instance_name
149
- instance_info += " (#{@instance_id})" if @instance_id
150
- @logger.info("Started Claude Code executor for instance: #{instance_info}")
151
- end
152
-
153
- def log_request(prompt)
154
- caller_info = @calling_instance
155
- caller_info += " (#{@calling_instance_id})" if @calling_instance_id
156
- instance_info = @instance_name
157
- instance_info += " (#{@instance_id})" if @instance_id
158
- @logger.info("#{caller_info} -> #{instance_info}: \n---\n#{prompt}\n---")
159
-
160
- # Build event hash for JSON logging
161
- event = {
162
- type: "request",
163
- from_instance: @calling_instance,
164
- from_instance_id: @calling_instance_id,
165
- to_instance: @instance_name,
166
- to_instance_id: @instance_id,
167
- prompt: prompt,
168
- timestamp: Time.now.iso8601,
169
- }
170
-
171
- append_to_session_json(event)
172
- end
173
-
174
- def log_response(response)
175
- caller_info = @calling_instance
176
- caller_info += " (#{@calling_instance_id})" if @calling_instance_id
177
- instance_info = @instance_name
178
- instance_info += " (#{@instance_id})" if @instance_id
179
- @logger.info(
180
- "($#{response["total_cost"]} - #{response["duration_ms"]}ms) #{instance_info} -> #{caller_info}: \n---\n#{response["result"]}\n---",
181
- )
97
+ logger.error { "Failed to write instance state for #{@instance_name} (#{@instance_id}): #{e.message}" }
182
98
  end
183
99
 
184
100
  def log_streaming_event(event)
@@ -198,13 +114,13 @@ module ClaudeSwarm
198
114
  end
199
115
 
200
116
  def log_system_message(event)
201
- @logger.debug("SYSTEM: #{JSON.pretty_generate(event)}")
117
+ logger.debug { "SYSTEM: #{JSON.pretty_generate(event)}" }
202
118
  end
203
119
 
204
120
  def log_assistant_message(msg)
205
121
  # Assistant messages don't have stop_reason in SDK - they only have content
206
122
  content = msg["content"]
207
- @logger.debug("ASSISTANT: #{JSON.pretty_generate(content)}") if content
123
+ logger.debug { "ASSISTANT: #{JSON.pretty_generate(content)}" } if content
208
124
 
209
125
  # Log tool calls
210
126
  tool_calls = content&.select { |c| c["type"] == "tool_use" } || []
@@ -212,52 +128,20 @@ module ClaudeSwarm
212
128
  arguments = tool_call["input"].to_json
213
129
  arguments = "#{arguments[0..300]} ...}" if arguments.length > 300
214
130
 
215
- instance_info = @instance_name
216
- instance_info += " (#{@instance_id})" if @instance_id
217
- @logger.info(
218
- "Tool call from #{instance_info} -> Tool: #{tool_call["name"]}, ID: #{tool_call["id"]}, Arguments: #{arguments}",
219
- )
131
+ logger.info do
132
+ "Tool call from #{instance_info} -> Tool: #{tool_call["name"]}, ID: #{tool_call["id"]}, Arguments: #{arguments}"
133
+ end
220
134
  end
221
135
 
222
136
  # Log thinking text
223
137
  text = content&.select { |c| c["type"] == "text" } || []
224
138
  text.each do |t|
225
- instance_info = @instance_name
226
- instance_info += " (#{@instance_id})" if @instance_id
227
- @logger.info("#{instance_info} is thinking:\n---\n#{t["text"]}\n---")
139
+ logger.info { "#{instance_info} is thinking:\n---\n#{t["text"]}\n---" }
228
140
  end
229
141
  end
230
142
 
231
143
  def log_user_message(content)
232
- @logger.debug("USER: #{JSON.pretty_generate(content)}")
233
- end
234
-
235
- def append_to_session_json(event)
236
- json_filename = "session.log.json"
237
- json_path = File.join(@session_path, json_filename)
238
-
239
- # Use file locking to ensure thread-safe writes
240
- File.open(json_path, File::WRONLY | File::APPEND | File::CREAT) do |file|
241
- file.flock(File::LOCK_EX)
242
-
243
- # Create entry with metadata
244
- entry = {
245
- instance: @instance_name,
246
- instance_id: @instance_id,
247
- calling_instance: @calling_instance,
248
- calling_instance_id: @calling_instance_id,
249
- timestamp: Time.now.iso8601,
250
- event: event,
251
- }
252
-
253
- # Write as single line JSON (JSONL format)
254
- file.puts(entry.to_json)
255
-
256
- file.flock(File::LOCK_UN)
257
- end
258
- rescue StandardError => e
259
- @logger.error("Failed to append to session JSON: #{e.message}")
260
- raise
144
+ logger.debug { "USER: #{JSON.pretty_generate(content)}" }
261
145
  end
262
146
 
263
147
  def build_sdk_options(prompt, options)
@@ -300,6 +184,10 @@ module ClaudeSwarm
300
184
  setup_additional_directories_mcp(sdk_options)
301
185
  end
302
186
 
187
+ # Add settings file path if it exists
188
+ settings_file = File.join(@session_path, "#{@instance_name}_settings.json")
189
+ sdk_options.settings = settings_file if File.exist?(settings_file)
190
+
303
191
  sdk_options
304
192
  end
305
193
 
@@ -329,14 +217,14 @@ module ClaudeSwarm
329
217
  headers: server_config["headers"] || {},
330
218
  )
331
219
  else
332
- @logger.warn("Unsupported MCP server type: #{server_type} for server: #{name}")
220
+ logger.warn { "Unsupported MCP server type: #{server_type} for server: #{name}" }
333
221
  nil
334
222
  end
335
223
  end
336
224
 
337
225
  mcp_servers.compact
338
226
  rescue StandardError => e
339
- @logger.error("Failed to parse MCP config: #{e.message}")
227
+ logger.error { "Failed to parse MCP config: #{e.message}" }
340
228
  {}
341
229
  end
342
230
 
@@ -347,7 +235,7 @@ module ClaudeSwarm
347
235
  @additional_directories.each do |dir|
348
236
  # This is a placeholder - the SDK doesn't directly support file system servers
349
237
  # You would need to implement a proper MCP server that provides file access
350
- @logger.warn("Additional directories not fully supported: #{dir}")
238
+ logger.warn { "Additional directories not fully supported: #{dir}" }
351
239
  end
352
240
  end
353
241
 
@@ -452,8 +340,5 @@ module ClaudeSwarm
452
340
  end
453
341
  end
454
342
  end
455
-
456
- class ExecutionError < StandardError; end
457
- class ParseError < StandardError; end
458
343
  end
459
344
  end
@@ -7,7 +7,7 @@ module ClaudeSwarm
7
7
  attr_accessor :executor, :instance_config, :logger, :session_path, :calling_instance, :calling_instance_id
8
8
  end
9
9
 
10
- def initialize(instance_config, calling_instance:, calling_instance_id: nil)
10
+ def initialize(instance_config, calling_instance:, calling_instance_id: nil, debug: false)
11
11
  @instance_config = instance_config
12
12
  @calling_instance = calling_instance
13
13
  @calling_instance_id = calling_instance_id
@@ -24,6 +24,7 @@ module ClaudeSwarm
24
24
  calling_instance_id: calling_instance_id,
25
25
  claude_session_id: instance_config[:claude_session_id],
26
26
  additional_directories: instance_config[:directories][1..] || [],
27
+ debug: debug,
27
28
  }
28
29
 
29
30
  @executor = if instance_config[:provider] == "openai"
@@ -230,6 +230,7 @@ module ClaudeSwarm
230
230
  instance_config,
231
231
  calling_instance: options[:calling_instance],
232
232
  calling_instance_id: options[:calling_instance_id],
233
+ debug: options[:debug],
233
234
  )
234
235
  server.start
235
236
  rescue StandardError => e
@@ -200,6 +200,7 @@ module ClaudeSwarm
200
200
  vibe: config["vibe"],
201
201
  worktree: parse_worktree_value(config["worktree"]),
202
202
  provider: provider, # nil means Claude (default)
203
+ hooks: config["hooks"], # Pass hooks configuration as-is
203
204
  }
204
205
 
205
206
  # Add OpenAI-specific fields only when provider is "openai"