claude_swarm 0.3.7 → 0.3.9

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: bc0d22524516d5e0f2a76715c2a80b36d1a974a3ff7ba451c103c86cb4a36fd1
4
- data.tar.gz: 4cebdba8032fc3dfe223033ba9b2890b5849dd5516a539ffdf57231bcec66c38
3
+ metadata.gz: 55711fd9483c68d996c0a0e448c4d31135d7423269d1ed691bb3590db5487627
4
+ data.tar.gz: a84c0b4e1165057cbbe22bcd4d6c02c8fcdaedb5abb2f18e150b667ee0083385
5
5
  SHA512:
6
- metadata.gz: e5ec9ed928d5e73d12eb1257571e3c2b45fc9db64dbd64e8f3186ea2df5f187927d4cc0a6e8eb9901cb8dbaf4679f442f340c85f6f6f728ca01f1d8b82db8bf8
7
- data.tar.gz: 8fce151a6a47d68736fc62b6453515f46e12d293d2eee2f3e951f99510351941faf9f0b9c5f0497de37847fac282cc997ccebdbb11f477fc71b21b8514dd753d
6
+ metadata.gz: cc2808da58a6cb2b2ac545cfbe4803382602d4339017337d3af84b92274baab241952186f7de4e72adfbd413d0fcb448b955fb4cb7ebb5ebc014b90895988247
7
+ data.tar.gz: 2d58c3795f4ea80da550c6241ad7639ffa5d25fa868eb9af18bf58382961f8cad4fb930616c133197858193fb2f9627aead812bfd60a3565631ef38398351b16
data/CHANGELOG.md CHANGED
@@ -1,3 +1,38 @@
1
+ ## [0.3.9]
2
+
3
+ ### Added
4
+ - **Main instance transcript integration**: Main Claude instance activity is now captured in session.log.json during interactive mode
5
+ - Automatically configures SessionStart hook for main instance to capture transcript path
6
+ - Background thread continuously tails transcript file and integrates entries into session.log.json
7
+ - Filters out summary entries to avoid duplicate conversation titles
8
+ - Uses file locking for thread-safe writes to maintain consistency
9
+ - Provides complete session history including main instance interactions
10
+
11
+ ## [0.3.8]
12
+
13
+ ### Added
14
+ - **Hooks support**: Claude Swarm now supports configuring Claude Code hooks for each instance
15
+ - Configure hooks directly in the YAML configuration file using Claude Code's format
16
+ - Each instance can have its own hooks configuration (PreToolUse, PostToolUse, UserPromptSubmit, etc.)
17
+ - Automatically generates `settings.json` files in the session directory when hooks are configured
18
+ - Main instance receives hooks via `--settings` CLI flag
19
+ - Connected instances receive hooks via SDK's `settings` attribute
20
+ - Full environment variable interpolation support in hook configurations
21
+ - See README.md "Hooks Configuration" section for usage examples
22
+ - **Persistent HTTP connections for OpenAI**: Added `faraday-net_http_persistent` dependency and configured OpenAI client to use persistent connections
23
+ - Improves performance when making multiple API requests by reusing HTTP connections
24
+ - Automatically configured for all OpenAI instances
25
+
26
+ ### Changed
27
+ - **Improved OpenAI executor code organization**: Refactored internal methods for better maintainability
28
+ - Extracted configuration building and response handling into focused private methods
29
+ - Improved code readability with functional patterns
30
+
31
+ ### Fixed
32
+ - **Settings integration**: Fixed passing settings to Claude instances
33
+ - Corrected SDK attribute name from `settings_path` to `settings`
34
+ - Added missing `--settings` flag for main instance CLI command
35
+
1
36
  ## [0.3.7]
2
37
 
3
38
  ### 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.
@@ -184,6 +184,10 @@ module ClaudeSwarm
184
184
  setup_additional_directories_mcp(sdk_options)
185
185
  end
186
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
+
187
191
  sdk_options
188
192
  end
189
193
 
@@ -693,7 +693,12 @@ module ClaudeSwarm
693
693
  def build_generation_prompt(readme_content, output_file)
694
694
  template_path = File.expand_path("templates/generation_prompt.md.erb", __dir__)
695
695
  template = File.read(template_path)
696
- ERB.new(template, trim_mode: "-").result(binding)
696
+ <<~PROMPT
697
+ #{ERB.new(template, trim_mode: "-").result(binding)}
698
+
699
+ Start the conversation by greeting the user and asking: 'What kind of project would you like to create a Claude Swarm for?'
700
+ Say: 'I am ready to start'
701
+ PROMPT
697
702
  end
698
703
  end
699
704
  end
@@ -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"
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # This hook is called when Claude Code starts a session
5
+ # It saves the transcript path for the main instance so the orchestrator can tail it
6
+
7
+ require "json"
8
+ require "fileutils"
9
+
10
+ # Read input from stdin
11
+ begin
12
+ stdin_data = $stdin.read
13
+ input = JSON.parse(stdin_data)
14
+ rescue => e
15
+ # Return error response
16
+ puts JSON.generate({
17
+ "success" => false,
18
+ "error" => "Failed to read/parse input: #{e.message}",
19
+ })
20
+ exit(1)
21
+ end
22
+
23
+ # Get session path from command-line argument or environment
24
+ session_path = ARGV[0] || ENV["CLAUDE_SWARM_SESSION_PATH"]
25
+
26
+ if session_path && input["transcript_path"]
27
+ # Write the transcript path to a known location
28
+ path_file = File.join(session_path, "main_instance_transcript.path")
29
+ File.write(path_file, input["transcript_path"])
30
+
31
+ # Return success
32
+ puts JSON.generate({
33
+ "success" => true,
34
+ })
35
+ else
36
+ # Return error if missing required data
37
+ puts JSON.generate({
38
+ "success" => false,
39
+ "error" => "Missing session path or transcript path",
40
+ })
41
+ exit(1)
42
+ end
@@ -1,8 +1,32 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openai"
4
+ require "faraday/net_http_persistent"
5
+ require "faraday/retry"
6
+
3
7
  module ClaudeSwarm
4
8
  module OpenAI
5
9
  class Executor < BaseExecutor
10
+ # Static configuration for Faraday retry middleware
11
+ FARADAY_RETRY_CONFIG = {
12
+ max: 3, # Maximum number of retries
13
+ interval: 0.5, # Initial delay between retries (in seconds)
14
+ interval_randomness: 0.5, # Randomness factor for retry intervals
15
+ backoff_factor: 2, # Exponential backoff factor
16
+ exceptions: [
17
+ Faraday::TimeoutError,
18
+ Faraday::ConnectionFailed,
19
+ Faraday::ServerError, # Retry on 5xx errors
20
+ ].freeze,
21
+ retry_statuses: [429, 500, 502, 503, 504].freeze, # HTTP status codes to retry
22
+ }.freeze
23
+
24
+ # Static configuration for OpenAI client
25
+ OPENAI_CLIENT_CONFIG = {
26
+ log_errors: true,
27
+ request_timeout: 1800, # 30 minutes
28
+ }.freeze
29
+
6
30
  def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false,
7
31
  instance_name: nil, instance_id: nil, calling_instance: nil, calling_instance_id: nil,
8
32
  claude_session_id: nil, additional_directories: [], debug: false,
@@ -56,19 +80,8 @@ module ClaudeSwarm
56
80
  # Calculate duration
57
81
  duration_ms = ((Time.now - start_time) * 1000).round
58
82
 
59
- # Format response similar to ClaudeCodeExecutor
60
- response = {
61
- "type" => "result",
62
- "result" => result,
63
- "duration_ms" => duration_ms,
64
- "total_cost" => calculate_cost(result),
65
- "session_id" => @session_id,
66
- }
67
-
68
- log_response(response)
69
-
70
- @last_response = response
71
- response
83
+ # Build and return response
84
+ build_response(result, duration_ms)
72
85
  rescue StandardError => e
73
86
  logger.error { "Unexpected error for #{@instance_name}: #{e.class} - #{e.message}" }
74
87
  logger.error { "Backtrace: #{e.backtrace.join("\n")}" }
@@ -92,35 +105,14 @@ module ClaudeSwarm
92
105
  private
93
106
 
94
107
  def setup_openai_client(token_env)
95
- config = {
96
- access_token: ENV.fetch(token_env),
97
- log_errors: true,
98
- request_timeout: 1800, # 30 minutes
99
- }
100
- config[:uri_base] = @base_url if @base_url
108
+ openai_client_config = build_openai_client_config(token_env)
109
+
110
+ @openai_client = ::OpenAI::Client.new(openai_client_config) do |faraday|
111
+ # Use persistent HTTP connections for better performance
112
+ faraday.adapter(:net_http_persistent)
101
113
 
102
- @openai_client = ::OpenAI::Client.new(config) do |faraday|
103
114
  # Add retry middleware with custom configuration
104
- faraday.request(
105
- :retry,
106
- max: 3, # Maximum number of retries
107
- interval: 0.5, # Initial delay between retries (in seconds)
108
- interval_randomness: 0.5, # Randomness factor for retry intervals
109
- backoff_factor: 2, # Exponential backoff factor
110
- exceptions: [
111
- Faraday::TimeoutError,
112
- Faraday::ConnectionFailed,
113
- Faraday::ServerError, # Retry on 5xx errors
114
- ],
115
- retry_statuses: [429, 500, 502, 503, 504], # HTTP status codes to retry
116
- retry_block: lambda do |env:, options:, retry_count:, exception:, will_retry:|
117
- if will_retry
118
- @logger.warn("Request failed (attempt #{retry_count}/#{options.max}): #{exception&.message || "HTTP #{env.status}"}. Retrying in #{options.interval * (options.backoff_factor**(retry_count - 1))} seconds...")
119
- else
120
- @logger.warn("Request failed after #{retry_count} attempts: #{exception&.message || "HTTP #{env.status}"}. Giving up.")
121
- end
122
- end,
123
- )
115
+ faraday.request(:retry, **build_faraday_retry_config)
124
116
  end
125
117
  rescue KeyError
126
118
  raise ExecutionError, "OpenAI API key not found in environment variable: #{token_env}"
@@ -132,49 +124,21 @@ module ClaudeSwarm
132
124
  # Read MCP config to find MCP servers
133
125
  mcp_data = JSON.parse(File.read(@mcp_config))
134
126
 
135
- # Create MCP client with all MCP servers from the config
136
- if mcp_data["mcpServers"] && !mcp_data["mcpServers"].empty?
137
- mcp_configs = []
138
-
139
- mcp_data["mcpServers"].each do |name, server_config|
140
- case server_config["type"]
141
- when "stdio"
142
- # Combine command and args into a single array
143
- command_array = [server_config["command"]]
144
- command_array.concat(server_config["args"] || [])
145
-
146
- stdio_config = MCPClient.stdio_config(
147
- command: command_array,
148
- name: name,
149
- )
150
- stdio_config[:read_timeout] = 1800
151
- mcp_configs << stdio_config
152
- when "sse"
153
- logger.warn { "SSE MCP servers not yet supported for OpenAI instances: #{name}" }
154
- # TODO: Add SSE support when available in ruby-mcp-client
155
- end
156
- end
127
+ # Build MCP configurations from servers
128
+ mcp_configs = build_mcp_configs(mcp_data["mcpServers"])
129
+ return if mcp_configs.empty?
130
+
131
+ # Create MCP client with unbundled environment to avoid bundler conflicts
132
+ # This ensures MCP servers run in a clean environment without inheriting
133
+ # Claude Swarm's BUNDLE_* environment variables
134
+ Bundler.with_unbundled_env do
135
+ @mcp_client = MCPClient.create_client(
136
+ mcp_server_configs: mcp_configs,
137
+ logger: @logger,
138
+ )
157
139
 
158
- if mcp_configs.any?
159
- # Create MCP client with unbundled environment to avoid bundler conflicts
160
- # This ensures MCP servers run in a clean environment without inheriting
161
- # Claude Swarm's BUNDLE_* environment variables
162
- Bundler.with_unbundled_env do
163
- @mcp_client = MCPClient.create_client(
164
- mcp_server_configs: mcp_configs,
165
- logger: @logger,
166
- )
167
-
168
- # List available tools from all MCP servers
169
- begin
170
- @available_tools = @mcp_client.list_tools
171
- logger.info { "Loaded #{@available_tools.size} tools from #{mcp_configs.size} MCP server(s)" }
172
- rescue StandardError => e
173
- logger.error { "Failed to load MCP tools: #{e.message}" }
174
- @available_tools = []
175
- end
176
- end
177
- end
140
+ # List available tools from all MCP servers
141
+ load_mcp_tools(mcp_configs)
178
142
  end
179
143
  rescue StandardError => e
180
144
  logger.error { "Failed to setup MCP client: #{e.message}" }
@@ -211,6 +175,80 @@ module ClaudeSwarm
211
175
  # Log streaming content similar to ClaudeCodeExecutor
212
176
  logger.debug { "#{instance_info} streaming: #{content}" }
213
177
  end
178
+
179
+ def build_faraday_retry_config
180
+ FARADAY_RETRY_CONFIG.merge(
181
+ retry_block: method(:handle_retry_logging),
182
+ )
183
+ end
184
+
185
+ def handle_retry_logging(env:, options:, retry_count:, exception:, will_retry:)
186
+ retry_delay = options.interval * (options.backoff_factor**(retry_count - 1))
187
+ error_info = exception&.message || "HTTP #{env.status}"
188
+
189
+ message = if will_retry
190
+ "Request failed (attempt #{retry_count}/#{options.max}): #{error_info}. Retrying in #{retry_delay} seconds..."
191
+ else
192
+ "Request failed after #{retry_count} attempts: #{error_info}. Giving up."
193
+ end
194
+
195
+ @logger.warn(message)
196
+ end
197
+
198
+ def build_openai_client_config(token_env)
199
+ OPENAI_CLIENT_CONFIG.merge(access_token: ENV.fetch(token_env)).tap do |config|
200
+ config[:uri_base] = @base_url if @base_url
201
+ end
202
+ end
203
+
204
+ def build_stdio_config(name, server_config)
205
+ # Combine command and args into a single array
206
+ command_array = [server_config["command"]]
207
+ command_array.concat(server_config["args"] || [])
208
+
209
+ MCPClient.stdio_config(
210
+ command: command_array,
211
+ name: name,
212
+ ).tap do |config|
213
+ config[:read_timeout] = 1800
214
+ end
215
+ end
216
+
217
+ def build_mcp_configs(mcp_servers)
218
+ return [] if mcp_servers.nil? || mcp_servers.empty?
219
+
220
+ mcp_servers.filter_map do |name, server_config|
221
+ case server_config["type"]
222
+ when "stdio"
223
+ build_stdio_config(name, server_config)
224
+ when "sse"
225
+ logger.warn { "SSE MCP servers not yet supported for OpenAI instances: #{name}" }
226
+ # TODO: Add SSE support when available in ruby-mcp-client
227
+ nil
228
+ end
229
+ end
230
+ end
231
+
232
+ def load_mcp_tools(mcp_configs)
233
+ @available_tools = @mcp_client.list_tools
234
+ logger.info { "Loaded #{@available_tools.size} tools from #{mcp_configs.size} MCP server(s)" }
235
+ rescue StandardError => e
236
+ logger.error { "Failed to load MCP tools: #{e.message}" }
237
+ @available_tools = []
238
+ end
239
+
240
+ def build_response(result, duration_ms)
241
+ {
242
+ "type" => "result",
243
+ "result" => result,
244
+ "duration_ms" => duration_ms,
245
+ "total_cost" => calculate_cost(result),
246
+ "session_id" => @session_id,
247
+ }.tap do |response|
248
+ log_response(response)
249
+ @last_response = response
250
+ end
251
+ end
214
252
  end
215
253
  end
216
254
  end
@@ -23,8 +23,6 @@ module ClaudeSwarm
23
23
  @stream_logs = stream_logs
24
24
  @debug = debug
25
25
  @restore_session_path = restore_session_path
26
- @session_path = nil
27
- @session_log_path = nil
28
26
  @provided_session_id = session_id
29
27
  # Store worktree option for later use
30
28
  @worktree_option = worktree
@@ -34,9 +32,41 @@ module ClaudeSwarm
34
32
  @modified_instances = nil
35
33
  # Track start time for runtime calculation
36
34
  @start_time = nil
35
+ # Track transcript tailing thread
36
+ @transcript_thread = nil
37
37
 
38
38
  # Set environment variable for prompt mode to suppress output
39
39
  ENV["CLAUDE_SWARM_PROMPT"] = "1" if @non_interactive_prompt
40
+
41
+ # Initialize session path
42
+ if @restore_session_path
43
+ # Use existing session path for restoration
44
+ @session_path = @restore_session_path
45
+ @session_log_path = File.join(@session_path, "session.log")
46
+ else
47
+ # Generate new session path
48
+ session_params = { working_dir: ClaudeSwarm.root_dir }
49
+ session_params[:session_id] = @provided_session_id if @provided_session_id
50
+ @session_path = SessionPath.generate(**session_params)
51
+ SessionPath.ensure_directory(@session_path)
52
+ @session_log_path = File.join(@session_path, "session.log")
53
+
54
+ # Extract session ID from path (the timestamp part)
55
+ @session_id = File.basename(@session_path)
56
+
57
+ end
58
+ ENV["CLAUDE_SWARM_SESSION_PATH"] = @session_path
59
+ ENV["CLAUDE_SWARM_ROOT_DIR"] = ClaudeSwarm.root_dir
60
+
61
+ # Initialize components that depend on session path
62
+ @process_tracker = ProcessTracker.new(@session_path)
63
+ @settings_generator = SettingsGenerator.new(@config)
64
+
65
+ # Initialize WorktreeManager if needed
66
+ if @needs_worktree_manager && !@restore_session_path
67
+ cli_option = @worktree_option.is_a?(String) && !@worktree_option.empty? ? @worktree_option : nil
68
+ @worktree_manager = WorktreeManager.new(cli_option, session_id: @session_id)
69
+ end
40
70
  end
41
71
 
42
72
  def start
@@ -49,65 +79,42 @@ module ClaudeSwarm
49
79
  puts "😎 Vibe mode ON" if @vibe
50
80
  end
51
81
 
52
- # Use existing session path
53
- session_path = @restore_session_path
54
- @session_path = session_path
55
- @session_log_path = File.join(@session_path, "session.log")
56
- ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
57
- ENV["CLAUDE_SWARM_ROOT_DIR"] = ClaudeSwarm.root_dir
58
-
59
82
  # Create run symlink for restored session
60
83
  create_run_symlink
61
84
 
62
85
  non_interactive_output do
63
- puts "📝 Using existing session: #{session_path}/"
86
+ puts "📝 Using existing session: #{@session_path}/"
64
87
  end
65
88
 
66
- # Initialize process tracker
67
- @process_tracker = ProcessTracker.new(session_path)
68
-
69
89
  # Check if the original session used worktrees
70
- restore_worktrees_if_needed(session_path)
90
+ restore_worktrees_if_needed(@session_path)
71
91
 
72
92
  # Regenerate MCP configurations with session IDs for restoration
73
93
  @generator.generate_all
74
94
  non_interactive_output do
75
95
  puts "✓ Regenerated MCP configurations with session IDs"
76
96
  end
97
+
98
+ # Generate settings files
99
+ @settings_generator.generate_all
100
+ non_interactive_output do
101
+ puts "✓ Generated settings files with hooks"
102
+ end
77
103
  else
78
104
  non_interactive_output do
79
105
  puts "🐝 Starting Claude Swarm: #{@config.swarm_name}"
80
106
  puts "😎 Vibe mode ON" if @vibe
81
107
  end
82
108
 
83
- # Generate and set session path for all instances
84
- session_params = { working_dir: ClaudeSwarm.root_dir }
85
- session_params[:session_id] = @provided_session_id if @provided_session_id
86
- session_path = SessionPath.generate(**session_params)
87
- SessionPath.ensure_directory(session_path)
88
- @session_path = session_path
89
- @session_log_path = File.join(@session_path, "session.log")
90
-
91
- # Extract session ID from path (the timestamp part)
92
- @session_id = File.basename(session_path)
93
-
94
- ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
95
- ENV["CLAUDE_SWARM_ROOT_DIR"] = ClaudeSwarm.root_dir
96
-
97
109
  # Create run symlink for new session
98
110
  create_run_symlink
99
111
 
100
112
  non_interactive_output do
101
- puts "📝 Session files will be saved to: #{session_path}/"
113
+ puts "📝 Session files will be saved to: #{@session_path}/"
102
114
  end
103
115
 
104
- # Initialize process tracker
105
- @process_tracker = ProcessTracker.new(session_path)
106
-
107
- # Create WorktreeManager if needed with session ID
108
- if @needs_worktree_manager
109
- cli_option = @worktree_option.is_a?(String) && !@worktree_option.empty? ? @worktree_option : nil
110
- @worktree_manager = WorktreeManager.new(cli_option, session_id: @session_id)
116
+ # Setup worktrees if needed
117
+ if @worktree_manager
111
118
  non_interactive_output { print("🌳 Setting up Git worktrees...") }
112
119
 
113
120
  # Get all instances for worktree setup
@@ -127,8 +134,14 @@ module ClaudeSwarm
127
134
  puts "✓ Generated MCP configurations in session directory"
128
135
  end
129
136
 
137
+ # Generate settings files
138
+ @settings_generator.generate_all
139
+ non_interactive_output do
140
+ puts "✓ Generated settings files with hooks"
141
+ end
142
+
130
143
  # Save swarm config path for restoration
131
- save_swarm_config_path(session_path)
144
+ save_swarm_config_path(@session_path)
132
145
  end
133
146
 
134
147
  # Launch the main instance (fetch after worktree setup to get modified paths)
@@ -159,6 +172,9 @@ module ClaudeSwarm
159
172
  log_thread = nil
160
173
  log_thread = start_log_streaming if @non_interactive_prompt && @stream_logs
161
174
 
175
+ # Start transcript tailing thread for main instance
176
+ @transcript_thread = start_transcript_tailing
177
+
162
178
  # Write the current process PID (orchestrator) to a file for easy access
163
179
  main_pid_file = File.join(@session_path, "main_pid")
164
180
  File.write(main_pid_file, Process.pid.to_s)
@@ -204,6 +220,9 @@ module ClaudeSwarm
204
220
  log_thread.join
205
221
  end
206
222
 
223
+ # Clean up transcript tailing thread
224
+ cleanup_transcript_thread
225
+
207
226
  # Display runtime and cost summary
208
227
  display_summary
209
228
 
@@ -276,6 +295,7 @@ module ClaudeSwarm
276
295
 
277
296
  def cleanup_processes
278
297
  @process_tracker.cleanup_all
298
+ cleanup_transcript_thread
279
299
  puts "✓ Cleanup complete"
280
300
  rescue StandardError => e
281
301
  puts "⚠️ Error during cleanup: #{e.message}"
@@ -488,6 +508,13 @@ module ClaudeSwarm
488
508
  parts << "--mcp-config"
489
509
  parts << mcp_config_path
490
510
 
511
+ # Add settings file if it exists for the main instance
512
+ settings_file = @settings_generator.settings_path(@config.main_instance)
513
+ if File.exist?(settings_file)
514
+ parts << "--settings"
515
+ parts << settings_file
516
+ end
517
+
491
518
  # Handle different modes
492
519
  if @non_interactive_prompt
493
520
  # Non-interactive mode with -p
@@ -556,6 +583,86 @@ module ClaudeSwarm
556
583
  end
557
584
  end
558
585
 
586
+ def start_transcript_tailing
587
+ Thread.new do
588
+ path_file = File.join(@session_path, "main_instance_transcript.path")
589
+
590
+ # Wait for path file to exist (created by SessionStart hook)
591
+ sleep(0.5) until File.exist?(path_file)
592
+
593
+ # Read the transcript path
594
+ transcript_path = File.read(path_file).strip
595
+
596
+ # Wait for transcript file to exist
597
+ sleep(0.5) until File.exist?(transcript_path)
598
+
599
+ # Tail the transcript file continuously (like tail -f)
600
+ File.open(transcript_path, "r") do |file|
601
+ # Start from the beginning to capture all entries
602
+ file.seek(0, IO::SEEK_SET) # Start at beginning of file
603
+
604
+ loop do
605
+ line = file.gets
606
+ if line
607
+ begin
608
+ # Parse JSONL entry
609
+ transcript_entry = JSON.parse(line)
610
+
611
+ # Skip summary entries - these are just conversation titles
612
+ next if transcript_entry["type"] == "summary"
613
+
614
+ # Convert to session.log.json format
615
+ session_entry = convert_transcript_to_session_format(transcript_entry)
616
+
617
+ # Write with file locking (same pattern as BaseExecutor)
618
+ session_json_path = File.join(@session_path, "session.log.json")
619
+ File.open(session_json_path, File::WRONLY | File::APPEND | File::CREAT) do |log_file|
620
+ log_file.flock(File::LOCK_EX)
621
+ log_file.puts(session_entry.to_json)
622
+ log_file.flock(File::LOCK_UN)
623
+ end
624
+ rescue JSON::ParserError
625
+ # Silently skip unparseable lines
626
+ rescue StandardError
627
+ # Silently handle other errors to keep thread running
628
+ end
629
+ else
630
+ # No new data, sleep briefly
631
+ sleep(0.1)
632
+ end
633
+ end
634
+ end
635
+ rescue StandardError
636
+ # Silently handle thread errors
637
+ end
638
+ end
639
+
640
+ def convert_transcript_to_session_format(transcript_entry)
641
+ {
642
+ instance: @config.main_instance,
643
+ instance_id: "main",
644
+ timestamp: transcript_entry["timestamp"] || Time.now.iso8601,
645
+ event: {
646
+ type: "transcript",
647
+ source: "main_instance",
648
+ data: transcript_entry,
649
+ },
650
+ }
651
+ end
652
+
653
+ def cleanup_transcript_thread
654
+ return unless @transcript_thread
655
+
656
+ @transcript_thread.terminate if @transcript_thread.alive?
657
+ @transcript_thread.join(1) # Wait up to 1 second for thread to finish
658
+ rescue StandardError => e
659
+ logger.error { "Error cleaning up transcript thread: #{e.message}" }
660
+ end
661
+
662
+ def logger
663
+ @logger ||= Logger.new(File.join(@session_path, "session.log"), level: :info, progname: "orchestrator")
664
+ end
665
+
559
666
  def execute_commands(commands, phase:, fail_fast:)
560
667
  all_succeeded = true
561
668
 
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ class SettingsGenerator
5
+ def initialize(configuration)
6
+ @config = configuration
7
+ end
8
+
9
+ def generate_all
10
+ ensure_session_directory
11
+
12
+ @config.instances.each do |name, instance|
13
+ generate_settings(name, instance)
14
+ end
15
+ end
16
+
17
+ def settings_path(instance_name)
18
+ File.join(session_path, "#{instance_name}_settings.json")
19
+ end
20
+
21
+ private
22
+
23
+ def session_path
24
+ @session_path ||= SessionPath.from_env
25
+ end
26
+
27
+ def ensure_session_directory
28
+ # Session directory is already created by orchestrator
29
+ # Just ensure it exists
30
+ SessionPath.ensure_directory(session_path)
31
+ end
32
+
33
+ def generate_settings(name, instance)
34
+ settings = {}
35
+
36
+ # Add hooks if configured
37
+ if instance[:hooks] && !instance[:hooks].empty?
38
+ settings["hooks"] = instance[:hooks]
39
+ end
40
+
41
+ # Add SessionStart hook for main instance to capture transcript path
42
+ if name == @config.main_instance
43
+ session_start_hook = build_session_start_hook
44
+
45
+ # Initialize hooks if not present
46
+ settings["hooks"] ||= {}
47
+ settings["hooks"]["SessionStart"] ||= []
48
+
49
+ # Add our hook to the SessionStart hooks
50
+ settings["hooks"]["SessionStart"] << session_start_hook
51
+ end
52
+
53
+ # Only write settings file if there are settings to write
54
+ return if settings.empty?
55
+
56
+ # Write settings file
57
+ File.write(settings_path(name), JSON.pretty_generate(settings))
58
+ end
59
+
60
+ def build_session_start_hook
61
+ hook_script_path = File.expand_path("hooks/session_start_hook.rb", __dir__)
62
+ # Pass session path as an argument since ENV may not be inherited
63
+ session_path_arg = session_path
64
+
65
+ {
66
+ "matcher" => "startup",
67
+ "hooks" => [
68
+ {
69
+ "type" => "command",
70
+ "command" => "ruby #{hook_script_path} '#{session_path_arg}'",
71
+ "timeout" => 5,
72
+ },
73
+ ],
74
+ }
75
+ end
76
+ end
77
+ end
@@ -1,4 +1,4 @@
1
- You are a Claude Swarm configuration generator assistant. Your role is to help the user create a well-structured claude-swarm.yml file through an interactive conversation.
1
+ You are a Claude Swarm configuration generator assistant. Your role is to help the user create a well-structured swarm YAML file through an interactive conversation.
2
2
 
3
3
  ## Claude Swarm Overview
4
4
  Claude Swarm is a Ruby gem that 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).
@@ -10,6 +10,7 @@ Key capabilities:
10
10
  - Run instances in different directories or Git worktrees
11
11
  - Support for custom system prompts per instance
12
12
  - Choose appropriate models (opus for complex tasks, sonnet for simpler ones)
13
+ - Support for OpenAI instances
13
14
 
14
15
  ## Your Task
15
16
  1. Start by asking about the user's project structure and development needs
@@ -43,7 +44,7 @@ swarm:
43
44
  instance_name:
44
45
  description: "Clear description of role and responsibilities"
45
46
  directory: ./path/to/directory
46
- model: sonnet # or opus for complex tasks
47
+ model: opus
47
48
  allowed_tools: [Read, Edit, Write, Bash]
48
49
  connections: [other_instance_names] # Optional
49
50
  prompt: |
@@ -57,7 +58,7 @@ swarm:
57
58
  - Write clear descriptions explaining each instance's responsibilities
58
59
  - Choose opus model for complex architectural or algorithmic tasks and routine development.
59
60
  - Choose sonnet model for simpler tasks
60
- - Set up logical connections (e.g., lead → team members, architect → implementers), but avoid circular dependencies.
61
+ - Set up logical connections (e.g., lead → team members, architect → implementers), but do not create circular dependencies.
61
62
  - Always add this to the end of every prompt: `For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.`
62
63
  - Select tools based on each instance's actual needs:
63
64
  - Read: For code review and analysis roles
@@ -72,7 +73,6 @@ swarm:
72
73
 
73
74
  ## Interactive Questions to Ask
74
75
  - What type of project are you working on?
75
- - What's your project's directory structure?
76
76
  - What are the main technologies/frameworks you're using?
77
77
  - What development tasks do you need help with?
78
78
  - Do you need specialized roles (testing, DevOps, documentation)?
@@ -225,6 +225,3 @@ The more precisely you explain what you want, the better Claude's response will
225
225
  * **Provide instructions as sequential steps:** Use numbered lists or bullet points to better ensure that Claude carries out the task the exact way you want it to.
226
226
 
227
227
  </prompt_best_practices>
228
-
229
- Start the conversation by greeting the user and asking: "What kind of project would you like to create a Claude Swarm for?"
230
- Say: I am ready to start
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "0.3.7"
4
+ VERSION = "0.3.9"
5
5
  end
data/lib/claude_swarm.rb CHANGED
@@ -23,9 +23,7 @@ require "yaml"
23
23
  # External dependencies
24
24
  require "claude_sdk"
25
25
  require "fast_mcp_annotations"
26
- require "faraday/retry"
27
26
  require "mcp_client"
28
- require "openai"
29
27
  require "thor"
30
28
 
31
29
  # Zeitwerk setup
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude_swarm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.7
4
+ version: 0.3.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda
@@ -51,6 +51,20 @@ dependencies:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
53
  version: '0.1'
54
+ - !ruby/object:Gem::Dependency
55
+ name: faraday-net_http_persistent
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '2.0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
54
68
  - !ruby/object:Gem::Dependency
55
69
  name: faraday-retry
56
70
  requirement: !ruby/object:Gem::Requirement
@@ -143,6 +157,7 @@ files:
143
157
  - examples/monitoring-demo.yml
144
158
  - examples/multi-directory.yml
145
159
  - examples/session-restoration-demo.yml
160
+ - examples/simple-session-hook-swarm.yml
146
161
  - examples/test-generation.yml
147
162
  - examples/with-before-commands.yml
148
163
  - exe/claude-swarm
@@ -154,6 +169,7 @@ files:
154
169
  - lib/claude_swarm/commands/ps.rb
155
170
  - lib/claude_swarm/commands/show.rb
156
171
  - lib/claude_swarm/configuration.rb
172
+ - lib/claude_swarm/hooks/session_start_hook.rb
157
173
  - lib/claude_swarm/mcp_generator.rb
158
174
  - lib/claude_swarm/openai/chat_completion.rb
159
175
  - lib/claude_swarm/openai/executor.rb
@@ -162,6 +178,7 @@ files:
162
178
  - lib/claude_swarm/process_tracker.rb
163
179
  - lib/claude_swarm/session_cost_calculator.rb
164
180
  - lib/claude_swarm/session_path.rb
181
+ - lib/claude_swarm/settings_generator.rb
165
182
  - lib/claude_swarm/system_utils.rb
166
183
  - lib/claude_swarm/templates/generation_prompt.md.erb
167
184
  - lib/claude_swarm/tools/reset_session_tool.rb