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 +4 -4
- data/CHANGELOG.md +26 -0
- data/CLAUDE.md +50 -3
- data/README.md +64 -0
- data/examples/simple-session-hook-swarm.yml +37 -0
- data/lib/claude_swarm/base_executor.rb +133 -0
- data/lib/claude_swarm/claude_code_executor.rb +21 -136
- data/lib/claude_swarm/claude_mcp_server.rb +2 -1
- data/lib/claude_swarm/cli.rb +1 -0
- data/lib/claude_swarm/configuration.rb +1 -0
- data/lib/claude_swarm/openai/chat_completion.rb +15 -15
- data/lib/claude_swarm/openai/executor.rb +46 -160
- data/lib/claude_swarm/openai/responses.rb +27 -27
- data/lib/claude_swarm/orchestrator.rb +166 -166
- data/lib/claude_swarm/settings_generator.rb +54 -0
- data/lib/claude_swarm/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a256a8d09a86290b06002437b38bc9092b7d83c1e4d1e0f4866b37cd1251bd99
|
4
|
+
data.tar.gz: d5eeec5e3a350dac2a04860c64c00043a8618fd29d40b9e1acf77e86b2f3fd49
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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.
|
134
|
-
5.
|
135
|
-
6.
|
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
|
-
|
79
|
-
|
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
|
-
|
94
|
-
|
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
|
-
|
95
|
+
logger.info { "Wrote instance state for #{@instance_name} (#{@instance_id}) with session ID: #{@session_id}" }
|
126
96
|
rescue StandardError => e
|
127
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
216
|
-
|
217
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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"
|
data/lib/claude_swarm/cli.rb
CHANGED
@@ -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"
|