claude_swarm 0.3.2 → 1.0.0

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.
@@ -42,16 +42,19 @@ module ClaudeSwarm
42
42
  type: :string,
43
43
  desc: "Root directory for resolving relative paths (defaults to current directory)"
44
44
  def start(config_file = nil)
45
+ # Set root directory early so it's available to all components
46
+ root_dir = options[:root_dir] || Dir.pwd
47
+ ENV["CLAUDE_SWARM_ROOT_DIR"] = File.expand_path(root_dir)
48
+
49
+ # Resolve config path relative to root directory
45
50
  config_path = config_file || "claude-swarm.yml"
51
+ config_path = File.expand_path(config_path, root_dir)
52
+
46
53
  unless File.exist?(config_path)
47
54
  error("Configuration file not found: #{config_path}")
48
55
  exit(1)
49
56
  end
50
57
 
51
- # Set root directory early so it's available to all components
52
- root_dir = options[:root_dir] || Dir.pwd
53
- ENV["CLAUDE_SWARM_ROOT_DIR"] = File.expand_path(root_dir)
54
-
55
58
  say("Starting Claude Swarm from #{config_path}...") unless options[:prompt]
56
59
 
57
60
  # Validate stream_logs option
@@ -230,6 +233,7 @@ module ClaudeSwarm
230
233
  instance_config,
231
234
  calling_instance: options[:calling_instance],
232
235
  calling_instance_id: options[:calling_instance_id],
236
+ debug: options[:debug],
233
237
  )
234
238
  server.start
235
239
  rescue StandardError => e
@@ -330,7 +334,7 @@ module ClaudeSwarm
330
334
  system!("command -v claude > /dev/null 2>&1")
331
335
  rescue Error
332
336
  error("Claude CLI is not installed or not in PATH")
333
- say("To install Claude CLI, visit: https://docs.anthropic.com/en/docs/claude-code")
337
+ error("To install Claude CLI, visit: https://docs.anthropic.com/en/docs/claude-code")
334
338
  exit(1)
335
339
  end
336
340
 
@@ -402,12 +406,12 @@ module ClaudeSwarm
402
406
  desc: "Number of lines to show initially"
403
407
  def watch(session_id)
404
408
  # Find session path
405
- run_symlink = File.join(File.expand_path("~/.claude-swarm/run"), session_id)
409
+ run_symlink = ClaudeSwarm.joined_run_dir(session_id)
406
410
  session_path = if File.symlink?(run_symlink)
407
411
  File.readlink(run_symlink)
408
412
  else
409
413
  # Search in sessions directory
410
- Dir.glob(File.expand_path("~/.claude-swarm/sessions/*/*")).find do |path|
414
+ Dir.glob(ClaudeSwarm.joined_sessions_dir("*", "*")).find do |path|
411
415
  File.basename(path) == session_id
412
416
  end
413
417
  end
@@ -433,7 +437,7 @@ module ClaudeSwarm
433
437
  default: 10,
434
438
  desc: "Maximum number of sessions to display"
435
439
  def list_sessions
436
- sessions_dir = File.expand_path("~/.claude-swarm/sessions")
440
+ sessions_dir = ClaudeSwarm.joined_sessions_dir
437
441
  unless Dir.exist?(sessions_dir)
438
442
  say("No sessions found", :yellow)
439
443
  return
@@ -507,7 +511,7 @@ module ClaudeSwarm
507
511
  private
508
512
 
509
513
  def error(message)
510
- say(message, :red)
514
+ $stderr.puts(Thor::Shell::Color.new.set_color(message, :red))
511
515
  end
512
516
 
513
517
  def restore_session(session_id)
@@ -558,12 +562,10 @@ module ClaudeSwarm
558
562
  # Load session metadata if it exists to check for worktree info
559
563
  session_metadata_file = File.join(session_path, "session_metadata.json")
560
564
  worktree_name = nil
561
- if File.exist?(session_metadata_file)
562
- metadata = JSON.parse(File.read(session_metadata_file))
563
- if metadata["worktree"] && metadata["worktree"]["enabled"]
564
- worktree_name = metadata["worktree"]["name"]
565
- say("Restoring with worktree: #{worktree_name}", :green) unless options[:prompt]
566
- end
565
+ metadata = JsonHandler.parse_file(session_metadata_file)
566
+ if metadata && metadata["worktree"] && metadata["worktree"]["enabled"]
567
+ worktree_name = metadata["worktree"]["name"]
568
+ say("Restoring with worktree: #{worktree_name}", :green) unless options[:prompt]
567
569
  end
568
570
 
569
571
  # Create orchestrator with restoration mode
@@ -588,7 +590,7 @@ module ClaudeSwarm
588
590
  end
589
591
 
590
592
  def find_session_path(session_id)
591
- sessions_dir = File.expand_path("~/.claude-swarm/sessions")
593
+ sessions_dir = ClaudeSwarm.joined_sessions_dir
592
594
 
593
595
  # Search for the session ID in all projects
594
596
  Dir.glob("#{sessions_dir}/*/#{session_id}").each do |path|
@@ -600,7 +602,7 @@ module ClaudeSwarm
600
602
  end
601
603
 
602
604
  def clean_stale_symlinks(days)
603
- run_dir = File.expand_path("~/.claude-swarm/run")
605
+ run_dir = ClaudeSwarm.joined_run_dir
604
606
  return 0 unless Dir.exist?(run_dir)
605
607
 
606
608
  cleaned = 0
@@ -629,10 +631,10 @@ module ClaudeSwarm
629
631
  end
630
632
 
631
633
  def clean_orphaned_worktrees(days)
632
- worktrees_dir = File.expand_path("~/.claude-swarm/worktrees")
634
+ worktrees_dir = ClaudeSwarm.joined_worktrees_dir
633
635
  return 0 unless Dir.exist?(worktrees_dir)
634
636
 
635
- sessions_dir = File.expand_path("~/.claude-swarm/sessions")
637
+ sessions_dir = ClaudeSwarm.joined_sessions_dir
636
638
  cleaned = 0
637
639
 
638
640
  Dir.glob("#{worktrees_dir}/*").each do |session_worktree_dir|
@@ -692,7 +694,12 @@ module ClaudeSwarm
692
694
  def build_generation_prompt(readme_content, output_file)
693
695
  template_path = File.expand_path("templates/generation_prompt.md.erb", __dir__)
694
696
  template = File.read(template_path)
695
- ERB.new(template, trim_mode: "-").result(binding)
697
+ <<~PROMPT
698
+ #{ERB.new(template, trim_mode: "-").result(binding)}
699
+
700
+ Start the conversation by greeting the user and asking: 'What kind of project would you like to create a Claude Swarm for?'
701
+ Say: 'I am ready to start'
702
+ PROMPT
696
703
  end
697
704
  end
698
705
  end
@@ -3,10 +3,9 @@
3
3
  module ClaudeSwarm
4
4
  module Commands
5
5
  class Ps
6
- RUN_DIR = File.expand_path("~/.claude-swarm/run")
7
-
8
6
  def execute
9
- unless Dir.exist?(RUN_DIR)
7
+ run_dir = ClaudeSwarm.joined_run_dir
8
+ unless Dir.exist?(run_dir)
10
9
  puts "No active sessions"
11
10
  return
12
11
  end
@@ -14,7 +13,7 @@ module ClaudeSwarm
14
13
  sessions = []
15
14
 
16
15
  # Read all symlinks in run directory
17
- Dir.glob("#{RUN_DIR}/*").each do |symlink|
16
+ Dir.glob("#{run_dir}/*").each do |symlink|
18
17
  next unless File.symlink?(symlink)
19
18
 
20
19
  begin
@@ -34,6 +33,9 @@ module ClaudeSwarm
34
33
  return
35
34
  end
36
35
 
36
+ # Check if any session is missing main instance costs
37
+ any_missing_main = sessions.any? { |s| !s[:main_has_cost] }
38
+
37
39
  # Column widths
38
40
  col_session = 15
39
41
  col_swarm = 25
@@ -50,13 +52,23 @@ module ClaudeSwarm
50
52
  } #{
51
53
  "UPTIME".ljust(col_uptime)
52
54
  } DIRECTORY"
53
- puts "\n⚠️ \e[3mTotal cost does not include the cost of the main instance\e[0m\n\n"
55
+
56
+ # Only show warning if any session is missing main instance costs
57
+ if any_missing_main
58
+ puts "\n⚠️ \e[3mTotal cost does not include the cost of the main instance for some sessions\e[0m\n\n"
59
+ else
60
+ puts
61
+ end
62
+
54
63
  puts header
55
64
  puts "-" * header.length
56
65
 
57
66
  # Display sessions sorted by start time (newest first)
58
67
  sessions.sort_by { |s| s[:start_time] }.reverse.each do |session|
59
68
  cost_str = format("$%.4f", session[:cost])
69
+ # Add asterisk if this session is missing main instance cost
70
+ cost_str += "*" unless session[:main_has_cost]
71
+
60
72
  puts "#{
61
73
  session[:id].ljust(col_session)
62
74
  } #{
@@ -107,7 +119,12 @@ module ClaudeSwarm
107
119
 
108
120
  # Calculate total cost from JSON log
109
121
  log_file = File.join(session_dir, "session.log.json")
110
- total_cost = SessionCostCalculator.calculate_simple_total(log_file)
122
+ cost_result = SessionCostCalculator.calculate_total_cost(log_file)
123
+ total_cost = cost_result[:total_cost]
124
+
125
+ # Check if main instance has cost data
126
+ instances_with_cost = cost_result[:instances_with_cost]
127
+ main_has_cost = main_instance && instances_with_cost.include?(main_instance)
111
128
 
112
129
  # Get uptime from session metadata or fallback to directory creation time
113
130
  start_time = get_start_time(session_dir)
@@ -117,6 +134,7 @@ module ClaudeSwarm
117
134
  id: session_id,
118
135
  name: swarm_name,
119
136
  cost: total_cost,
137
+ main_has_cost: main_has_cost,
120
138
  uptime: uptime,
121
139
  directory: directories_str,
122
140
  start_time: start_time,
@@ -128,9 +146,10 @@ module ClaudeSwarm
128
146
  def get_start_time(session_dir)
129
147
  # Try to get from session metadata first
130
148
  metadata_file = File.join(session_dir, "session_metadata.json")
131
- if File.exist?(metadata_file)
132
- metadata = JSON.parse(File.read(metadata_file))
133
- return Time.parse(metadata["start_time"]) if metadata["start_time"]
149
+ metadata = JsonHandler.parse_file(metadata_file)
150
+
151
+ if metadata && metadata["start_time"]
152
+ return Time.parse(metadata["start_time"])
134
153
  end
135
154
 
136
155
  # Fallback to directory creation time
@@ -160,7 +179,7 @@ module ClaudeSwarm
160
179
  session_metadata_file = File.join(session_dir, "session_metadata.json")
161
180
  return directories unless File.exist?(session_metadata_file)
162
181
 
163
- metadata = JSON.parse(File.read(session_metadata_file))
182
+ metadata = JsonHandler.parse_file!(session_metadata_file)
164
183
  worktree_info = metadata["worktree"]
165
184
  return directories unless worktree_info && worktree_info["enabled"]
166
185
 
@@ -63,23 +63,22 @@ module ClaudeSwarm
63
63
 
64
64
  def find_session_path(session_id)
65
65
  # First check the run directory
66
- run_symlink = File.join(File.expand_path("~/.claude-swarm/run"), session_id)
66
+ run_symlink = ClaudeSwarm.joined_run_dir(session_id)
67
67
  if File.symlink?(run_symlink)
68
68
  target = File.readlink(run_symlink)
69
69
  return target if Dir.exist?(target)
70
70
  end
71
71
 
72
72
  # Fall back to searching all sessions
73
- Dir.glob(File.expand_path("~/.claude-swarm/sessions/*/*")).find do |path|
73
+ Dir.glob(ClaudeSwarm.joined_sessions_dir("*", "*")).find do |path|
74
74
  File.basename(path) == session_id
75
75
  end
76
76
  end
77
77
 
78
78
  def get_runtime_info(session_path)
79
79
  metadata_file = File.join(session_path, "session_metadata.json")
80
- return unless File.exist?(metadata_file)
81
-
82
- metadata = JSON.parse(File.read(metadata_file))
80
+ metadata = JsonHandler.parse_file(metadata_file)
81
+ return unless metadata
83
82
 
84
83
  if metadata["duration_seconds"]
85
84
  # Session has completed
@@ -43,15 +43,30 @@ module ClaudeSwarm
43
43
  @swarm["after"] || []
44
44
  end
45
45
 
46
+ def validate_directories
47
+ @instances.each do |name, instance|
48
+ # Validate all directories in the directories array
49
+ instance[:directories].each do |directory|
50
+ raise Error, "Directory '#{directory}' for instance '#{name}' does not exist" unless File.directory?(directory)
51
+ end
52
+ end
53
+ end
54
+
46
55
  private
47
56
 
57
+ def has_before_commands?
58
+ @swarm && @swarm["before"] && !@swarm["before"].empty?
59
+ end
60
+
48
61
  def load_and_validate
49
- @config = YAML.load_file(@config_path)
62
+ @config = YAML.load_file(@config_path, aliases: true)
50
63
  interpolate_env_vars!(@config)
51
64
  validate_version
52
65
  validate_swarm
53
66
  parse_swarm
54
- validate_directories
67
+ # Skip directory validation if before commands are present
68
+ # They might create the directories
69
+ validate_directories unless has_before_commands?
55
70
  rescue Errno::ENOENT
56
71
  raise Error, "Configuration file not found: #{@config_path}"
57
72
  rescue Psych::SyntaxError => e
@@ -200,6 +215,7 @@ module ClaudeSwarm
200
215
  vibe: config["vibe"],
201
216
  worktree: parse_worktree_value(config["worktree"]),
202
217
  provider: provider, # nil means Claude (default)
218
+ hooks: config["hooks"], # Pass hooks configuration as-is
203
219
  }
204
220
 
205
221
  # Add OpenAI-specific fields only when provider is "openai"
@@ -232,7 +248,7 @@ module ClaudeSwarm
232
248
  case mcp["type"]
233
249
  when "stdio"
234
250
  raise Error, "MCP '#{mcp["name"]}' missing 'command'" unless mcp["command"]
235
- when "sse"
251
+ when "sse", "http"
236
252
  raise Error, "MCP '#{mcp["name"]}' missing 'url'" unless mcp["url"]
237
253
  else
238
254
  raise Error, "Unknown MCP type '#{mcp["type"]}' for '#{mcp["name"]}'"
@@ -272,15 +288,6 @@ module ClaudeSwarm
272
288
  visited.add(instance_name)
273
289
  end
274
290
 
275
- def validate_directories
276
- @instances.each do |name, instance|
277
- # Validate all directories in the directories array
278
- instance[:directories].each do |directory|
279
- raise Error, "Directory '#{directory}' for instance '#{name}' does not exist" unless File.directory?(directory)
280
- end
281
- end
282
- end
283
-
284
291
  def validate_tool_field(instance_name, config, field_name)
285
292
  return unless config.key?(field_name)
286
293
 
@@ -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
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ # Centralized JSON handling for the Claude Swarm codebase
5
+ class JsonHandler
6
+ class << self
7
+ # Parse JSON string into Ruby object
8
+ # @param json_string [String] The JSON string to parse
9
+ # @param raise_on_error [Boolean] Whether to raise exception on error (default: false)
10
+ # @return [Object] The parsed Ruby object, or original string if parsing fails and raise_on_error is false
11
+ # @raise [JSON::ParserError] If the JSON is invalid and raise_on_error is true
12
+ def parse(json_string, raise_on_error: false)
13
+ JSON.parse(json_string)
14
+ rescue JSON::ParserError => e
15
+ raise e if raise_on_error
16
+
17
+ json_string
18
+ end
19
+
20
+ # Parse JSON string with exception raising
21
+ # @param json_string [String] The JSON string to parse
22
+ # @return [Object] The parsed Ruby object
23
+ # @raise [JSON::ParserError] If the JSON is invalid
24
+ def parse!(json_string)
25
+ parse(json_string, raise_on_error: true)
26
+ end
27
+
28
+ # Parse JSON from a file with exception raising
29
+ # @param file_path [String] Path to the JSON file
30
+ # @return [Object] The parsed Ruby object
31
+ # @raise [Errno::ENOENT] If the file does not exist
32
+ # @raise [JSON::ParserError] If the file contains invalid JSON
33
+ def parse_file!(file_path)
34
+ content = File.read(file_path)
35
+ parse!(content)
36
+ end
37
+
38
+ # Parse JSON from a file, returning nil on error
39
+ # @param file_path [String] Path to the JSON file
40
+ # @return [Object, nil] The parsed Ruby object or nil if file doesn't exist or contains invalid JSON
41
+ def parse_file(file_path)
42
+ parse_file!(file_path)
43
+ rescue Errno::ENOENT, JSON::ParserError
44
+ nil
45
+ end
46
+
47
+ # Generate pretty-formatted JSON string
48
+ # @param object [Object] The Ruby object to convert to JSON
49
+ # @param raise_on_error [Boolean] Whether to raise exception on error (default: false)
50
+ # @return [String, nil] The pretty-formatted JSON string, or nil if generation fails and raise_on_error is false
51
+ # @raise [JSON::GeneratorError] If the object cannot be converted to JSON and raise_on_error is true
52
+ def pretty_generate(object, raise_on_error: false)
53
+ JSON.pretty_generate(object)
54
+ rescue JSON::GeneratorError, JSON::NestingError => e
55
+ raise e if raise_on_error
56
+
57
+ nil
58
+ end
59
+
60
+ # Generate pretty-formatted JSON string with exception raising
61
+ # @param object [Object] The Ruby object to convert to JSON
62
+ # @return [String] The pretty-formatted JSON string
63
+ # @raise [JSON::GeneratorError] If the object cannot be converted to JSON
64
+ def pretty_generate!(object)
65
+ pretty_generate(object, raise_on_error: true)
66
+ end
67
+
68
+ # Write Ruby object to a JSON file with pretty formatting
69
+ # @param file_path [String] Path to the JSON file
70
+ # @param object [Object] The Ruby object to write
71
+ # @return [Boolean] True if successful, false if generation or write fails
72
+ def write_file(file_path, object)
73
+ json_string = pretty_generate!(object)
74
+ File.write(file_path, json_string)
75
+ true
76
+ rescue JSON::GeneratorError, JSON::NestingError, SystemCallError
77
+ false
78
+ end
79
+
80
+ # Write Ruby object to a JSON file with exception raising
81
+ # @param file_path [String] Path to the JSON file
82
+ # @param object [Object] The Ruby object to write
83
+ # @raise [JSON::GeneratorError] If the object cannot be converted to JSON
84
+ # @raise [SystemCallError] If the file cannot be written
85
+ def write_file!(file_path, object)
86
+ json_string = pretty_generate!(object)
87
+ File.write(file_path, json_string)
88
+ end
89
+ end
90
+ end
91
+ end
@@ -73,7 +73,7 @@ module ClaudeSwarm
73
73
  "mcpServers" => mcp_servers,
74
74
  }
75
75
 
76
- File.write(mcp_config_path(name), JSON.pretty_generate(config))
76
+ JsonHandler.write_file!(mcp_config_path(name), config)
77
77
  end
78
78
 
79
79
  def build_mcp_server_config(mcp)
@@ -86,11 +86,13 @@ module ClaudeSwarm
86
86
  }.tap do |config|
87
87
  config["env"] = mcp["env"] if mcp["env"]
88
88
  end
89
- when "sse"
89
+ when "sse", "http"
90
90
  {
91
- "type" => "sse",
91
+ "type" => mcp["type"],
92
92
  "url" => mcp["url"],
93
- }
93
+ }.tap do |config|
94
+ config["headers"] = mcp["headers"] if mcp["headers"]
95
+ end
94
96
  end
95
97
  end
96
98
 
@@ -218,7 +220,7 @@ module ClaudeSwarm
218
220
  return unless Dir.exist?(state_dir)
219
221
 
220
222
  Dir.glob(File.join(state_dir, "*.json")).each do |state_file|
221
- data = JSON.parse(File.read(state_file))
223
+ data = JsonHandler.parse_file!(state_file)
222
224
  instance_name = data["instance_name"]
223
225
  instance_id = data["instance_id"]
224
226
 
@@ -5,11 +5,11 @@ module ClaudeSwarm
5
5
  class ChatCompletion
6
6
  MAX_TURNS_WITH_TOOLS = 100_000 # virtually infinite
7
7
 
8
- def initialize(openai_client:, mcp_client:, available_tools:, logger:, instance_name:, model:, temperature: nil, reasoning_effort: nil)
8
+ def initialize(openai_client:, mcp_client:, available_tools:, executor:, instance_name:, model:, temperature: nil, reasoning_effort: nil)
9
9
  @openai_client = openai_client
10
10
  @mcp_client = mcp_client
11
11
  @available_tools = available_tools
12
- @executor = logger # This is actually the executor, not a logger
12
+ @executor = executor
13
13
  @instance_name = instance_name
14
14
  @model = model
15
15
  @temperature = temperature
@@ -57,7 +57,7 @@ module ClaudeSwarm
57
57
  def process_chat_completion(messages, depth = 0)
58
58
  # Prevent infinite recursion
59
59
  if depth > MAX_TURNS_WITH_TOOLS
60
- @executor.error("Maximum recursion depth reached in tool execution")
60
+ @executor.logger.error { "Maximum recursion depth reached in tool execution" }
61
61
  return "Error: Maximum tool call depth exceeded"
62
62
  end
63
63
 
@@ -83,7 +83,7 @@ module ClaudeSwarm
83
83
  parameters[:tools] = @mcp_client.to_openai_tools if @available_tools&.any? && @mcp_client
84
84
 
85
85
  # Log the request parameters
86
- @executor.info("Chat API Request (depth=#{depth}): #{JSON.pretty_generate(parameters)}")
86
+ @executor.logger.info { "Chat API Request (depth=#{depth}): #{JsonHandler.pretty_generate!(parameters)}" }
87
87
 
88
88
  # Append to session JSON
89
89
  append_to_session_json({
@@ -97,16 +97,16 @@ module ClaudeSwarm
97
97
  begin
98
98
  response = @openai_client.chat(parameters: parameters)
99
99
  rescue StandardError => e
100
- @executor.error("Chat API error: #{e.class} - #{e.message}")
101
- @executor.error("Request parameters: #{JSON.pretty_generate(parameters)}")
100
+ @executor.logger.error { "Chat API error: #{e.class} - #{e.message}" }
101
+ @executor.logger.error { "Request parameters: #{JsonHandler.pretty_generate!(parameters)}" }
102
102
 
103
103
  # Try to extract and log the response body for better debugging
104
104
  if e.respond_to?(:response)
105
105
  begin
106
106
  error_body = e.response[:body]
107
- @executor.error("Error response body: #{error_body}")
107
+ @executor.logger.error { "Error response body: #{error_body}" }
108
108
  rescue StandardError => parse_error
109
- @executor.error("Could not parse error response: #{parse_error.message}")
109
+ @executor.logger.error { "Could not parse error response: #{parse_error.message}" }
110
110
  end
111
111
  end
112
112
 
@@ -127,7 +127,7 @@ module ClaudeSwarm
127
127
  end
128
128
 
129
129
  # Log the response
130
- @executor.info("Chat API Response (depth=#{depth}): #{JSON.pretty_generate(response)}")
130
+ @executor.logger.info { "Chat API Response (depth=#{depth}): #{JsonHandler.pretty_generate!(response)}" }
131
131
 
132
132
  # Append to session JSON
133
133
  append_to_session_json({
@@ -141,7 +141,7 @@ module ClaudeSwarm
141
141
  message = response.dig("choices", 0, "message")
142
142
 
143
143
  if message.nil?
144
- @executor.error("No message in response: #{response.inspect}")
144
+ @executor.logger.error { "No message in response: #{response.inspect}" }
145
145
  return "Error: No response from OpenAI"
146
146
  end
147
147
 
@@ -169,7 +169,7 @@ module ClaudeSwarm
169
169
 
170
170
  def execute_and_append_tool_results(tool_calls, messages)
171
171
  # Log tool calls
172
- @executor.info("Executing tool calls: #{JSON.pretty_generate(tool_calls)}")
172
+ @executor.logger.info { "Executing tool calls: #{JsonHandler.pretty_generate!(tool_calls)}" }
173
173
 
174
174
  # Append to session JSON
175
175
  append_to_session_json({
@@ -186,16 +186,16 @@ module ClaudeSwarm
186
186
 
187
187
  begin
188
188
  # Parse arguments
189
- tool_args = tool_args_str.is_a?(String) ? JSON.parse(tool_args_str) : tool_args_str
189
+ tool_args = tool_args_str.is_a?(String) ? JsonHandler.parse!(tool_args_str) : tool_args_str
190
190
 
191
191
  # Log tool execution
192
- @executor.info("Executing tool: #{tool_name} with args: #{JSON.pretty_generate(tool_args)}")
192
+ @executor.logger.info { "Executing tool: #{tool_name} with args: #{JsonHandler.pretty_generate!(tool_args)}" }
193
193
 
194
194
  # Execute tool via MCP
195
195
  result = @mcp_client.call_tool(tool_name, tool_args)
196
196
 
197
197
  # Log result
198
- @executor.info("Tool result for #{tool_name}: #{result}")
198
+ @executor.logger.info { "Tool result for #{tool_name}: #{result}" }
199
199
 
200
200
  # Append to session JSON
201
201
  append_to_session_json({
@@ -214,8 +214,8 @@ module ClaudeSwarm
214
214
  content: result.to_s,
215
215
  }
216
216
  rescue StandardError => e
217
- @executor.error("Tool execution failed for #{tool_name}: #{e.message}")
218
- @executor.error(e.backtrace.join("\n"))
217
+ @executor.logger.error { "Tool execution failed for #{tool_name}: #{e.message}" }
218
+ @executor.logger.error { e.backtrace.join("\n") }
219
219
 
220
220
  # Append error to session JSON
221
221
  append_to_session_json({