claude_swarm 0.3.8 → 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: a256a8d09a86290b06002437b38bc9092b7d83c1e4d1e0f4866b37cd1251bd99
4
- data.tar.gz: d5eeec5e3a350dac2a04860c64c00043a8618fd29d40b9e1acf77e86b2f3fd49
3
+ metadata.gz: 55711fd9483c68d996c0a0e448c4d31135d7423269d1ed691bb3590db5487627
4
+ data.tar.gz: a84c0b4e1165057cbbe22bcd4d6c02c8fcdaedb5abb2f18e150b667ee0083385
5
5
  SHA512:
6
- metadata.gz: 3bb8119400be33d056b795aaea4f7f3d045d85a29d830cc42fe8c3dba0fd406100553b68740fe884e2e769e9685c439983d3a38ee52f55730f9f1ff7b40ddbff
7
- data.tar.gz: a2dd8483358b673698e7ca3a71d1952fa0b2c85d0daf9cd2ff06c5f62d78f7ffebacc95a0e622cfb3f62f229e81c8177c5d6b263303b88c26c592ba53e7c7607
6
+ metadata.gz: cc2808da58a6cb2b2ac545cfbe4803382602d4339017337d3af84b92274baab241952186f7de4e72adfbd413d0fcb448b955fb4cb7ebb5ebc014b90895988247
7
+ data.tar.gz: 2d58c3795f4ea80da550c6241ad7639ffa5d25fa868eb9af18bf58382961f8cad4fb930616c133197858193fb2f9627aead812bfd60a3565631ef38398351b16
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
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
+
1
11
  ## [0.3.8]
2
12
 
3
13
  ### Added
@@ -9,6 +19,14 @@
9
19
  - Connected instances receive hooks via SDK's `settings` attribute
10
20
  - Full environment variable interpolation support in hook configurations
11
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
12
30
 
13
31
  ### Fixed
14
32
  - **Settings integration**: Fixed passing settings to Claude instances
@@ -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
@@ -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
@@ -17,15 +17,12 @@ module ClaudeSwarm
17
17
  restore_session_path: nil, worktree: nil, session_id: nil)
18
18
  @config = configuration
19
19
  @generator = mcp_generator
20
- @settings_generator = SettingsGenerator.new(configuration)
21
20
  @vibe = vibe
22
21
  @non_interactive_prompt = prompt
23
22
  @interactive_prompt = interactive_prompt
24
23
  @stream_logs = stream_logs
25
24
  @debug = debug
26
25
  @restore_session_path = restore_session_path
27
- @session_path = nil
28
- @session_log_path = nil
29
26
  @provided_session_id = session_id
30
27
  # Store worktree option for later use
31
28
  @worktree_option = worktree
@@ -35,9 +32,41 @@ module ClaudeSwarm
35
32
  @modified_instances = nil
36
33
  # Track start time for runtime calculation
37
34
  @start_time = nil
35
+ # Track transcript tailing thread
36
+ @transcript_thread = nil
38
37
 
39
38
  # Set environment variable for prompt mode to suppress output
40
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
41
70
  end
42
71
 
43
72
  def start
@@ -50,25 +79,15 @@ module ClaudeSwarm
50
79
  puts "😎 Vibe mode ON" if @vibe
51
80
  end
52
81
 
53
- # Use existing session path
54
- session_path = @restore_session_path
55
- @session_path = session_path
56
- @session_log_path = File.join(@session_path, "session.log")
57
- ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
58
- ENV["CLAUDE_SWARM_ROOT_DIR"] = ClaudeSwarm.root_dir
59
-
60
82
  # Create run symlink for restored session
61
83
  create_run_symlink
62
84
 
63
85
  non_interactive_output do
64
- puts "📝 Using existing session: #{session_path}/"
86
+ puts "📝 Using existing session: #{@session_path}/"
65
87
  end
66
88
 
67
- # Initialize process tracker
68
- @process_tracker = ProcessTracker.new(session_path)
69
-
70
89
  # Check if the original session used worktrees
71
- restore_worktrees_if_needed(session_path)
90
+ restore_worktrees_if_needed(@session_path)
72
91
 
73
92
  # Regenerate MCP configurations with session IDs for restoration
74
93
  @generator.generate_all
@@ -87,34 +106,15 @@ module ClaudeSwarm
87
106
  puts "😎 Vibe mode ON" if @vibe
88
107
  end
89
108
 
90
- # Generate and set session path for all instances
91
- session_params = { working_dir: ClaudeSwarm.root_dir }
92
- session_params[:session_id] = @provided_session_id if @provided_session_id
93
- session_path = SessionPath.generate(**session_params)
94
- SessionPath.ensure_directory(session_path)
95
- @session_path = session_path
96
- @session_log_path = File.join(@session_path, "session.log")
97
-
98
- # Extract session ID from path (the timestamp part)
99
- @session_id = File.basename(session_path)
100
-
101
- ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
102
- ENV["CLAUDE_SWARM_ROOT_DIR"] = ClaudeSwarm.root_dir
103
-
104
109
  # Create run symlink for new session
105
110
  create_run_symlink
106
111
 
107
112
  non_interactive_output do
108
- puts "📝 Session files will be saved to: #{session_path}/"
113
+ puts "📝 Session files will be saved to: #{@session_path}/"
109
114
  end
110
115
 
111
- # Initialize process tracker
112
- @process_tracker = ProcessTracker.new(session_path)
113
-
114
- # Create WorktreeManager if needed with session ID
115
- if @needs_worktree_manager
116
- cli_option = @worktree_option.is_a?(String) && !@worktree_option.empty? ? @worktree_option : nil
117
- @worktree_manager = WorktreeManager.new(cli_option, session_id: @session_id)
116
+ # Setup worktrees if needed
117
+ if @worktree_manager
118
118
  non_interactive_output { print("🌳 Setting up Git worktrees...") }
119
119
 
120
120
  # Get all instances for worktree setup
@@ -141,7 +141,7 @@ module ClaudeSwarm
141
141
  end
142
142
 
143
143
  # Save swarm config path for restoration
144
- save_swarm_config_path(session_path)
144
+ save_swarm_config_path(@session_path)
145
145
  end
146
146
 
147
147
  # Launch the main instance (fetch after worktree setup to get modified paths)
@@ -172,6 +172,9 @@ module ClaudeSwarm
172
172
  log_thread = nil
173
173
  log_thread = start_log_streaming if @non_interactive_prompt && @stream_logs
174
174
 
175
+ # Start transcript tailing thread for main instance
176
+ @transcript_thread = start_transcript_tailing
177
+
175
178
  # Write the current process PID (orchestrator) to a file for easy access
176
179
  main_pid_file = File.join(@session_path, "main_pid")
177
180
  File.write(main_pid_file, Process.pid.to_s)
@@ -217,6 +220,9 @@ module ClaudeSwarm
217
220
  log_thread.join
218
221
  end
219
222
 
223
+ # Clean up transcript tailing thread
224
+ cleanup_transcript_thread
225
+
220
226
  # Display runtime and cost summary
221
227
  display_summary
222
228
 
@@ -289,6 +295,7 @@ module ClaudeSwarm
289
295
 
290
296
  def cleanup_processes
291
297
  @process_tracker.cleanup_all
298
+ cleanup_transcript_thread
292
299
  puts "✓ Cleanup complete"
293
300
  rescue StandardError => e
294
301
  puts "⚠️ Error during cleanup: #{e.message}"
@@ -576,6 +583,86 @@ module ClaudeSwarm
576
583
  end
577
584
  end
578
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
+
579
666
  def execute_commands(commands, phase:, fail_fast:)
580
667
  all_succeeded = true
581
668
 
@@ -21,13 +21,7 @@ module ClaudeSwarm
21
21
  private
22
22
 
23
23
  def session_path
24
- # In tests, use the session path from env if available, otherwise use a temp path
25
- @session_path ||= if ENV["CLAUDE_SWARM_SESSION_PATH"]
26
- SessionPath.from_env
27
- else
28
- # This should only happen in unit tests
29
- Dir.pwd
30
- end
24
+ @session_path ||= SessionPath.from_env
31
25
  end
32
26
 
33
27
  def ensure_session_directory
@@ -44,11 +38,40 @@ module ClaudeSwarm
44
38
  settings["hooks"] = instance[:hooks]
45
39
  end
46
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
+
47
53
  # Only write settings file if there are settings to write
48
54
  return if settings.empty?
49
55
 
50
56
  # Write settings file
51
57
  File.write(settings_path(name), JSON.pretty_generate(settings))
52
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
53
76
  end
54
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.8"
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.8
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
@@ -155,6 +169,7 @@ files:
155
169
  - lib/claude_swarm/commands/ps.rb
156
170
  - lib/claude_swarm/commands/show.rb
157
171
  - lib/claude_swarm/configuration.rb
172
+ - lib/claude_swarm/hooks/session_start_hook.rb
158
173
  - lib/claude_swarm/mcp_generator.rb
159
174
  - lib/claude_swarm/openai/chat_completion.rb
160
175
  - lib/claude_swarm/openai/executor.rb