claude_swarm 0.1.10 → 0.1.12

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: 5d324ca820897ad2929051a87d06f152df536434522f42cba507a9c6984b019f
4
- data.tar.gz: bd2f9659c374ee277e3f20e4fd339b542887c394730acf5682a90d9c06cfcfb1
3
+ metadata.gz: c468e30d7de839a1b7bb88c2e2ee1c9bfa58b8bb0f0b0375b3495ebf5de340f0
4
+ data.tar.gz: 7234799e8b24cc2b0a691f188a890ee84d0780a77a85723307bdc9f17ad71545
5
5
  SHA512:
6
- metadata.gz: 4ec9b2c70924671846cbfbaf98ddd93869b2e84162c199559b7551ae833bb73ac62950f4bee9dc19f59a138997c4873f6fb8746fa56f5cf1769855e01cdd74e3
7
- data.tar.gz: 64cc15a5ee7844f900eda0097e539046d730e4eb07b1323217a55dee9b965db506b1c3838d15e311291edc133296191a80a92769727cce84361cba6b5a2c7f0a
6
+ metadata.gz: 7ed4d3460c8e686f683513796939827419d254d9b4a168a211d24d89cb18aaeb4cacf42a2dd90d1b88ecbc98ab97f7391b356ed24f0beb05da4bab6c62ce5332
7
+ data.tar.gz: ced867554fa8b43c6ce9f327f791212dcc04a87c378d78e99c87f41776074eba7f2a1bf7691b6b9daaf34fcd7923b057cefe453171d5838f2981ee87d67237f1
data/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## [0.1.12]
2
+ ### Added
3
+ - **Circular dependency detection**: Configuration validation now detects and reports circular dependencies between instances
4
+ - Clear error messages showing the dependency cycle (e.g., "Circular dependency detected: lead -> backend -> lead")
5
+ - Comprehensive test coverage for various circular dependency scenarios
6
+ - **Session management improvements**: Session files are now stored in `~/.claude-swarm/sessions/` organized by project path
7
+ - Added `SessionPath` module to centralize session path management
8
+ - Sessions are now organized by project directory for better multi-project support
9
+ - Added `CLAUDE_SWARM_HOME` environment variable support for custom storage location
10
+ - Log full JSON to `session.log.json` as JSONL
11
+
12
+ ### Changed
13
+ - Session files moved from `./.claude-swarm/sessions/` to `~/.claude-swarm/sessions/[project]/[timestamp]/`
14
+ - Replaced `CLAUDE_SWARM_SESSION_TIMESTAMP` with `CLAUDE_SWARM_SESSION_PATH` environment variable
15
+ - MCP server configurations now use the new centralized session path
16
+
17
+ ### Fixed
18
+ - Fixed circular dependency example in README documentation
19
+
20
+ ## [0.1.11]
21
+ ### Added
22
+ - Main instance debug mode with `claude-swarm --debug`
23
+
1
24
  ## [0.1.10]
2
25
 
3
26
  ### Added
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Claude Swarm
2
2
 
3
- [![Gem Version](https://badge.fury.io/rb/claude_swarm.svg?cache_bust=1)](https://badge.fury.io/rb/claude_swarm)
3
+ [![Gem Version](https://badge.fury.io/rb/claude_swarm.svg?cache_bust=0.1.11)](https://badge.fury.io/rb/claude_swarm)
4
4
  [![CI](https://github.com/parruda/claude-swarm/actions/workflows/ci.yml/badge.svg)](https://github.com/parruda/claude-swarm/actions/workflows/ci.yml)
5
5
 
6
6
  Claude Swarm 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) in a tree-like hierarchy. Define your swarm topology in simple YAML and let Claude instances delegate tasks through connected instances. Perfect for complex projects requiring specialized AI agents for frontend, backend, testing, DevOps, or research tasks.
@@ -48,15 +48,12 @@ swarm:
48
48
  directory: .
49
49
  model: opus
50
50
  connections: [frontend, backend]
51
- allowed_tools: # Tools aren't required if you run it with `--vibe`
52
- - Read
53
- - Edit
54
- - Bash
51
+ vibe: true # Allow all tools for this instance
55
52
  frontend:
56
53
  description: "Frontend specialist handling UI and user experience"
57
54
  directory: ./frontend
58
55
  model: opus
59
- allowed_tools:
56
+ allowed_tools: # Tools aren't required if you run it with `--vibe`
60
57
  - Edit
61
58
  - Write
62
59
  - Bash
@@ -330,7 +327,7 @@ swarm:
330
327
  description: "Backend developer building APIs and services"
331
328
  directory: ./backend
332
329
  model: opus
333
- connections: [architect, database]
330
+ connections: [database]
334
331
  allowed_tools:
335
332
  - Edit
336
333
  - Write
data/claude-swarm.yml ADDED
@@ -0,0 +1,42 @@
1
+ version: 1
2
+ swarm:
3
+ name: "Swarm Name"
4
+ main: lead_developer
5
+ instances:
6
+ lead_developer:
7
+ description: "Lead developer who coordinates the team and makes architectural decisions"
8
+ directory: .
9
+ model: sonnet
10
+ prompt: "You are the lead developer coordinating the team"
11
+ allowed_tools: [Read, Edit, Bash, Write]
12
+ connections: [frontend_dev, backend_dev]
13
+
14
+ # Example instances (uncomment and modify as needed):
15
+
16
+ frontend_dev:
17
+ description: "Frontend developer specializing in React and modern web technologies"
18
+ directory: .
19
+ model: sonnet
20
+ prompt: "You specialize in frontend development with React, TypeScript, and modern web technologies"
21
+ allowed_tools: [Read, Edit, Write, "Bash(npm:*)", "Bash(yarn:*)", "Bash(pnpm:*)"]
22
+
23
+ backend_dev:
24
+ description: "Backend developer focusing on APIs, databases, and server architecture"
25
+ directory: .
26
+ model: sonnet
27
+ prompt: "You specialize in backend development, APIs, databases, and server architecture"
28
+ allowed_tools: [Read, Edit, Write, Bash]
29
+
30
+ # devops_engineer:
31
+ # description: "DevOps engineer managing infrastructure, CI/CD, and deployments"
32
+ # directory: .
33
+ # model: sonnet
34
+ # prompt: "You specialize in infrastructure, CI/CD, containerization, and deployment"
35
+ # allowed_tools: [Read, Edit, Write, "Bash(docker:*)", "Bash(kubectl:*)", "Bash(terraform:*)"]
36
+
37
+ # qa_engineer:
38
+ # description: "QA engineer ensuring quality through comprehensive testing"
39
+ # directory: ./tests
40
+ # model: sonnet
41
+ # prompt: "You specialize in testing, quality assurance, and test automation"
42
+ # allowed_tools: [Read, Edit, Write, Bash]
@@ -4,13 +4,11 @@ require "json"
4
4
  require "open3"
5
5
  require "logger"
6
6
  require "fileutils"
7
+ require_relative "session_path"
7
8
 
8
9
  module ClaudeSwarm
9
10
  class ClaudeCodeExecutor
10
- SWARM_DIR = ".claude-swarm"
11
- SESSIONS_DIR = "sessions"
12
-
13
- attr_reader :session_id, :last_response, :working_directory, :logger, :session_timestamp
11
+ attr_reader :session_id, :last_response, :working_directory, :logger, :session_path
14
12
 
15
13
  def initialize(working_directory: Dir.pwd, model: nil, mcp_config: nil, vibe: false, instance_name: nil, calling_instance: nil)
16
14
  @working_directory = working_directory
@@ -95,17 +93,13 @@ module ClaudeSwarm
95
93
  private
96
94
 
97
95
  def setup_logging
98
- # Use environment variable for session timestamp if available (set by orchestrator)
99
- # Otherwise create a new timestamp
100
- @session_timestamp = ENV["CLAUDE_SWARM_SESSION_TIMESTAMP"] || Time.now.strftime("%Y%m%d_%H%M%S")
101
-
102
- # Ensure the session directory exists
103
- session_dir = File.join(Dir.pwd, SWARM_DIR, SESSIONS_DIR, @session_timestamp)
104
- FileUtils.mkdir_p(session_dir)
96
+ # Use session path from environment (required)
97
+ @session_path = SessionPath.from_env
98
+ SessionPath.ensure_directory(@session_path)
105
99
 
106
100
  # Create logger with session.log filename
107
101
  log_filename = "session.log"
108
- log_path = File.join(session_dir, log_filename)
102
+ log_path = File.join(@session_path, log_filename)
109
103
  @logger = Logger.new(log_path)
110
104
  @logger.level = Logger::INFO
111
105
 
@@ -128,6 +122,8 @@ module ClaudeSwarm
128
122
  end
129
123
 
130
124
  def log_streaming_event(event)
125
+ append_to_session_json(event)
126
+
131
127
  return log_system_message(event) if event["type"] == "system"
132
128
 
133
129
  # Add specific details based on event type
@@ -170,6 +166,32 @@ module ClaudeSwarm
170
166
  @logger.debug("USER: #{JSON.pretty_generate(content)}")
171
167
  end
172
168
 
169
+ def append_to_session_json(event)
170
+ json_filename = "session.log.json"
171
+ json_path = File.join(@session_path, json_filename)
172
+
173
+ # Use file locking to ensure thread-safe writes
174
+ File.open(json_path, File::WRONLY | File::APPEND | File::CREAT) do |file|
175
+ file.flock(File::LOCK_EX)
176
+
177
+ # Create entry with metadata
178
+ entry = {
179
+ instance: @instance_name,
180
+ calling_instance: @calling_instance,
181
+ timestamp: Time.now.iso8601,
182
+ event: event
183
+ }
184
+
185
+ # Write as single line JSON (JSONL format)
186
+ file.puts(entry.to_json)
187
+
188
+ file.flock(File::LOCK_UN)
189
+ end
190
+ rescue StandardError => e
191
+ @logger.error("Failed to append to session JSON: #{e.message}")
192
+ raise
193
+ end
194
+
173
195
  def build_command_array(prompt, options)
174
196
  cmd_array = ["claude"]
175
197
 
@@ -11,7 +11,7 @@ module ClaudeSwarm
11
11
  class ClaudeMcpServer
12
12
  # Class variables to share state with tool classes
13
13
  class << self
14
- attr_accessor :executor, :instance_config, :logger, :session_timestamp, :calling_instance
14
+ attr_accessor :executor, :instance_config, :logger, :session_path, :calling_instance
15
15
  end
16
16
 
17
17
  def initialize(instance_config, calling_instance:)
@@ -30,7 +30,7 @@ module ClaudeSwarm
30
30
  self.class.executor = @executor
31
31
  self.class.instance_config = @instance_config
32
32
  self.class.logger = @executor.logger
33
- self.class.session_timestamp = @executor.session_timestamp
33
+ self.class.session_path = @executor.session_path
34
34
  self.class.calling_instance = @calling_instance
35
35
  end
36
36
 
@@ -22,6 +22,8 @@ module ClaudeSwarm
22
22
  desc: "Prompt to pass to the main Claude instance (non-interactive mode)"
23
23
  method_option :stream_logs, type: :boolean, default: false,
24
24
  desc: "Stream session logs to stdout (only works with -p)"
25
+ method_option :debug, type: :boolean, default: false,
26
+ desc: "Enable debug output"
25
27
  def start(config_file = nil)
26
28
  config_path = config_file || options[:config]
27
29
  unless File.exist?(config_path)
@@ -39,13 +41,12 @@ module ClaudeSwarm
39
41
 
40
42
  begin
41
43
  config = Configuration.new(config_path)
42
- session_timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
43
- generator = McpGenerator.new(config, vibe: options[:vibe], timestamp: session_timestamp)
44
+ generator = McpGenerator.new(config, vibe: options[:vibe])
44
45
  orchestrator = Orchestrator.new(config, generator,
45
46
  vibe: options[:vibe],
46
47
  prompt: options[:prompt],
47
- session_timestamp: session_timestamp,
48
- stream_logs: options[:stream_logs])
48
+ stream_logs: options[:stream_logs],
49
+ debug: options[:debug])
49
50
  orchestrator.start
50
51
  rescue Error => e
51
52
  error e.message
@@ -68,6 +68,7 @@ module ClaudeSwarm
68
68
  @instances[name] = parse_instance(name, config)
69
69
  end
70
70
  validate_connections
71
+ detect_circular_dependencies
71
72
  end
72
73
 
73
74
  def parse_instance(name, config)
@@ -127,6 +128,31 @@ module ClaudeSwarm
127
128
  end
128
129
  end
129
130
 
131
+ def detect_circular_dependencies
132
+ @instances.each_key do |instance_name|
133
+ visited = Set.new
134
+ path = []
135
+ detect_cycle_from(instance_name, visited, path)
136
+ end
137
+ end
138
+
139
+ def detect_cycle_from(instance_name, visited, path)
140
+ return if visited.include?(instance_name)
141
+
142
+ if path.include?(instance_name)
143
+ cycle_start = path.index(instance_name)
144
+ cycle = path[cycle_start..] + [instance_name]
145
+ raise Error, "Circular dependency detected: #{cycle.join(" -> ")}"
146
+ end
147
+
148
+ path.push(instance_name)
149
+ @instances[instance_name][:connections].each do |connection|
150
+ detect_cycle_from(connection, visited, path)
151
+ end
152
+ path.pop
153
+ visited.add(instance_name)
154
+ end
155
+
130
156
  def validate_directories
131
157
  @instances.each do |name, instance|
132
158
  directory = instance[:directory]
@@ -3,16 +3,14 @@
3
3
  require "json"
4
4
  require "fileutils"
5
5
  require "shellwords"
6
+ require_relative "session_path"
6
7
 
7
8
  module ClaudeSwarm
8
9
  class McpGenerator
9
- SWARM_DIR = ".claude-swarm"
10
- SESSIONS_SUBDIR = "sessions"
11
-
12
- def initialize(configuration, vibe: false, timestamp: nil)
10
+ def initialize(configuration, vibe: false)
13
11
  @config = configuration
14
12
  @vibe = vibe
15
- @timestamp = timestamp || Time.now.strftime("%Y%m%d_%H%M%S")
13
+ @session_path = nil # Will be set when needed
16
14
  end
17
15
 
18
16
  def generate_all
@@ -24,24 +22,19 @@ module ClaudeSwarm
24
22
  end
25
23
 
26
24
  def mcp_config_path(instance_name)
27
- File.join(Dir.pwd, SWARM_DIR, SESSIONS_SUBDIR, @timestamp, "#{instance_name}.mcp.json")
25
+ File.join(session_path, "#{instance_name}.mcp.json")
28
26
  end
29
27
 
30
28
  private
31
29
 
32
- def swarm_dir
33
- File.join(Dir.pwd, SWARM_DIR)
30
+ def session_path
31
+ @session_path ||= SessionPath.from_env
34
32
  end
35
33
 
36
34
  def ensure_swarm_directory
37
- FileUtils.mkdir_p(swarm_dir)
38
-
39
- # Create session directory with timestamp
40
- session_dir = File.join(swarm_dir, SESSIONS_SUBDIR, @timestamp)
41
- FileUtils.mkdir_p(session_dir)
42
-
43
- gitignore_path = File.join(swarm_dir, ".gitignore")
44
- File.write(gitignore_path, "*\n") unless File.exist?(gitignore_path)
35
+ # Session directory is already created by orchestrator
36
+ # Just ensure it exists
37
+ SessionPath.ensure_directory(session_path)
45
38
  end
46
39
 
47
40
  def generate_mcp_config(name, instance)
@@ -134,10 +127,7 @@ module ClaudeSwarm
134
127
  {
135
128
  "type" => "stdio",
136
129
  "command" => exe_path,
137
- "args" => args,
138
- "env" => {
139
- "CLAUDE_SWARM_SESSION_TIMESTAMP" => @timestamp
140
- }
130
+ "args" => args
141
131
  }
142
132
  end
143
133
  end
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "shellwords"
4
+ require_relative "session_path"
4
5
 
5
6
  module ClaudeSwarm
6
7
  class Orchestrator
7
- def initialize(configuration, mcp_generator, vibe: false, prompt: nil, session_timestamp: nil, stream_logs: false)
8
+ def initialize(configuration, mcp_generator, vibe: false, prompt: nil, stream_logs: false, debug: false)
8
9
  @config = configuration
9
10
  @generator = mcp_generator
10
11
  @vibe = vibe
11
12
  @prompt = prompt
12
- @session_timestamp = session_timestamp || Time.now.strftime("%Y%m%d_%H%M%S")
13
13
  @stream_logs = stream_logs
14
+ @debug = debug
14
15
  end
15
16
 
16
17
  def start
@@ -20,10 +21,15 @@ module ClaudeSwarm
20
21
  puts
21
22
  end
22
23
 
23
- # Set session timestamp for all instances to share the same session directory
24
- ENV["CLAUDE_SWARM_SESSION_TIMESTAMP"] = @session_timestamp
24
+ # Generate and set session path for all instances
25
+ session_path = SessionPath.generate(working_dir: Dir.pwd)
26
+ SessionPath.ensure_directory(session_path)
27
+
28
+ ENV["CLAUDE_SWARM_SESSION_PATH"] = session_path
29
+ ENV["CLAUDE_SWARM_START_DIR"] = Dir.pwd
30
+
25
31
  unless @prompt
26
- puts "📝 Session files will be saved to: .claude-swarm/sessions/#{@session_timestamp}/"
32
+ puts "📝 Session files will be saved to: #{session_path}/"
27
33
  puts
28
34
  end
29
35
 
@@ -48,7 +54,7 @@ module ClaudeSwarm
48
54
  end
49
55
 
50
56
  command = build_main_command(main_instance)
51
- if ENV["DEBUG"] && !@prompt
57
+ if @debug && !@prompt
52
58
  puts "Running: #{command}"
53
59
  puts
54
60
  end
@@ -73,9 +79,7 @@ module ClaudeSwarm
73
79
 
74
80
  def start_log_streaming
75
81
  Thread.new do
76
- session_log_path = File.join(Dir.pwd, ClaudeSwarm::ClaudeCodeExecutor::SWARM_DIR,
77
- ClaudeSwarm::ClaudeCodeExecutor::SESSIONS_DIR,
78
- @session_timestamp, "session.log")
82
+ session_log_path = File.join(ENV.fetch("CLAUDE_SWARM_SESSION_PATH", nil), "session.log")
79
83
 
80
84
  # Wait for log file to be created
81
85
  sleep 0.1 until File.exist?(session_log_path)
@@ -133,6 +137,8 @@ module ClaudeSwarm
133
137
  parts << instance[:prompt]
134
138
  end
135
139
 
140
+ parts << "--debug" if @debug
141
+
136
142
  mcp_config_path = @generator.mcp_config_path(@config.main_instance)
137
143
  parts << "--mcp-config"
138
144
  parts << mcp_config_path
@@ -5,13 +5,10 @@ require "fast_mcp"
5
5
  require "logger"
6
6
  require "fileutils"
7
7
  require_relative "permission_tool"
8
+ require_relative "session_path"
8
9
 
9
10
  module ClaudeSwarm
10
11
  class PermissionMcpServer
11
- # Directory constants
12
- SWARM_DIR = ".claude-swarm"
13
- SESSIONS_DIR = "sessions"
14
-
15
12
  # Server configuration
16
13
  SERVER_NAME = "claude-swarm-permissions"
17
14
  SERVER_VERSION = "1.0.0"
@@ -60,20 +57,14 @@ module ClaudeSwarm
60
57
  end
61
58
 
62
59
  def setup_logging
63
- session_dir = create_session_directory
64
- @logger = create_logger(session_dir)
60
+ session_path = SessionPath.from_env
61
+ SessionPath.ensure_directory(session_path)
62
+ @logger = create_logger(session_path)
65
63
  @logger.info("Permission MCP server logging initialized")
66
64
  end
67
65
 
68
- def create_session_directory
69
- session_timestamp = ENV["CLAUDE_SWARM_SESSION_TIMESTAMP"] || Time.now.strftime("%Y%m%d_%H%M%S")
70
- session_dir = File.join(Dir.pwd, SWARM_DIR, SESSIONS_DIR, session_timestamp)
71
- FileUtils.mkdir_p(session_dir)
72
- session_dir
73
- end
74
-
75
- def create_logger(session_dir)
76
- log_path = File.join(session_dir, "permissions.log")
66
+ def create_logger(session_path)
67
+ log_path = File.join(session_path, "permissions.log")
77
68
  logger = Logger.new(log_path)
78
69
  logger.level = Logger::DEBUG
79
70
  logger.formatter = log_formatter
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fileutils"
4
+
5
+ module ClaudeSwarm
6
+ module SessionPath
7
+ SESSIONS_DIR = "sessions"
8
+
9
+ class << self
10
+ def swarm_home
11
+ ENV["CLAUDE_SWARM_HOME"] || File.expand_path("~/.claude-swarm")
12
+ end
13
+
14
+ # Convert a directory path to a safe folder name using + as separator
15
+ def project_folder_name(working_dir = Dir.pwd)
16
+ # Don't expand path if it's already expanded (avoids double expansion on Windows)
17
+ path = working_dir.start_with?("/") || working_dir.match?(/^[A-Za-z]:/) ? working_dir : File.expand_path(working_dir)
18
+
19
+ # Handle Windows drive letters (C:\ → C)
20
+ path = path.gsub(/^([A-Za-z]):/, '\1')
21
+
22
+ # Remove leading slash/backslash
23
+ path = path.sub(%r{^[/\\]}, "")
24
+
25
+ # Replace all path separators with +
26
+ path.gsub(%r{[/\\]}, "+")
27
+ end
28
+
29
+ # Generate a full session path for a given directory and timestamp
30
+ def generate(working_dir: Dir.pwd, timestamp: Time.now.strftime("%Y%m%d_%H%M%S"))
31
+ project_name = project_folder_name(working_dir)
32
+ File.join(swarm_home, SESSIONS_DIR, project_name, timestamp)
33
+ end
34
+
35
+ # Ensure the session directory exists
36
+ def ensure_directory(session_path)
37
+ FileUtils.mkdir_p(session_path)
38
+
39
+ # Add .gitignore to swarm home if it doesn't exist
40
+ gitignore_path = File.join(swarm_home, ".gitignore")
41
+ File.write(gitignore_path, "*\n") unless File.exist?(gitignore_path)
42
+ end
43
+
44
+ # Get the session path from environment (required)
45
+ def from_env
46
+ ENV["CLAUDE_SWARM_SESSION_PATH"] or raise "CLAUDE_SWARM_SESSION_PATH not set"
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "0.1.10"
4
+ VERSION = "0.1.12"
5
5
  end
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.1.10
4
+ version: 0.1.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda
@@ -58,6 +58,7 @@ files:
58
58
  - README.md
59
59
  - RELEASING.md
60
60
  - Rakefile
61
+ - claude-swarm.yml
61
62
  - example/claude-swarm.yml
62
63
  - example/microservices-team.yml
63
64
  - example/test-generation.yml
@@ -73,6 +74,7 @@ files:
73
74
  - lib/claude_swarm/permission_tool.rb
74
75
  - lib/claude_swarm/reset_session_tool.rb
75
76
  - lib/claude_swarm/session_info_tool.rb
77
+ - lib/claude_swarm/session_path.rb
76
78
  - lib/claude_swarm/task_tool.rb
77
79
  - lib/claude_swarm/version.rb
78
80
  - sdk-docs.md