claude_swarm 0.1.17 → 0.1.19

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.
@@ -0,0 +1,353 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "fileutils"
5
+ require "json"
6
+ require "pathname"
7
+ require "securerandom"
8
+
9
+ module ClaudeSwarm
10
+ class WorktreeManager
11
+ attr_reader :shared_worktree_name, :created_worktrees
12
+
13
+ def initialize(cli_worktree_option = nil, session_id: nil)
14
+ @cli_worktree_option = cli_worktree_option
15
+ @session_id = session_id
16
+ # Generate a name based on session ID if no option given, empty string, or default "worktree" from Thor
17
+ @shared_worktree_name = if cli_worktree_option.nil? || cli_worktree_option.empty? || cli_worktree_option == "worktree"
18
+ generate_worktree_name
19
+ else
20
+ cli_worktree_option
21
+ end
22
+ @created_worktrees = {} # Maps "repo_root:worktree_name" to worktree_path
23
+ @instance_worktree_configs = {} # Stores per-instance worktree settings
24
+ end
25
+
26
+ def setup_worktrees(instances)
27
+ # First pass: determine worktree configuration for each instance
28
+ instances.each do |instance|
29
+ worktree_config = determine_worktree_config(instance)
30
+ @instance_worktree_configs[instance[:name]] = worktree_config
31
+ end
32
+
33
+ # Second pass: create necessary worktrees
34
+ worktrees_to_create = collect_worktrees_to_create(instances)
35
+ worktrees_to_create.each do |repo_root, worktree_name|
36
+ create_worktree(repo_root, worktree_name)
37
+ end
38
+
39
+ # Third pass: map instance directories to worktree paths
40
+ instances.each do |instance|
41
+ worktree_config = @instance_worktree_configs[instance[:name]]
42
+
43
+ if ENV["CLAUDE_SWARM_DEBUG"]
44
+ puts "Debug [WorktreeManager]: Processing instance #{instance[:name]}"
45
+ puts "Debug [WorktreeManager]: Worktree config: #{worktree_config.inspect}"
46
+ end
47
+
48
+ next if worktree_config[:skip]
49
+
50
+ worktree_name = worktree_config[:name]
51
+ original_dirs = instance[:directories] || [instance[:directory]]
52
+ mapped_dirs = original_dirs.map { |dir| map_to_worktree_path(dir, worktree_name) }
53
+
54
+ if ENV["CLAUDE_SWARM_DEBUG"]
55
+ puts "Debug [WorktreeManager]: Original dirs: #{original_dirs.inspect}"
56
+ puts "Debug [WorktreeManager]: Mapped dirs: #{mapped_dirs.inspect}"
57
+ end
58
+
59
+ if instance[:directories]
60
+ instance[:directories] = mapped_dirs
61
+ # Also update the single directory field for backward compatibility
62
+ instance[:directory] = mapped_dirs.first
63
+ else
64
+ instance[:directory] = mapped_dirs.first
65
+ end
66
+
67
+ puts "Debug [WorktreeManager]: Updated instance[:directory] to: #{instance[:directory]}" if ENV["CLAUDE_SWARM_DEBUG"]
68
+ end
69
+ end
70
+
71
+ def map_to_worktree_path(original_path, worktree_name)
72
+ return original_path unless original_path
73
+
74
+ expanded_path = File.expand_path(original_path)
75
+ repo_root = find_git_root(expanded_path)
76
+
77
+ if ENV["CLAUDE_SWARM_DEBUG"]
78
+ puts "Debug [map_to_worktree_path]: Original path: #{original_path}"
79
+ puts "Debug [map_to_worktree_path]: Expanded path: #{expanded_path}"
80
+ puts "Debug [map_to_worktree_path]: Repo root: #{repo_root}"
81
+ end
82
+
83
+ return original_path unless repo_root
84
+
85
+ # Check if we have a worktree for this repo and name
86
+ worktree_key = "#{repo_root}:#{worktree_name}"
87
+ worktree_path = @created_worktrees[worktree_key]
88
+
89
+ if ENV["CLAUDE_SWARM_DEBUG"]
90
+ puts "Debug [map_to_worktree_path]: Worktree key: #{worktree_key}"
91
+ puts "Debug [map_to_worktree_path]: Worktree path: #{worktree_path}"
92
+ puts "Debug [map_to_worktree_path]: Created worktrees: #{@created_worktrees.inspect}"
93
+ end
94
+
95
+ return original_path unless worktree_path
96
+
97
+ # Calculate relative path from repo root
98
+ relative_path = Pathname.new(expanded_path).relative_path_from(Pathname.new(repo_root)).to_s
99
+
100
+ # Return the equivalent path in the worktree
101
+ result = if relative_path == "."
102
+ worktree_path
103
+ else
104
+ File.join(worktree_path, relative_path)
105
+ end
106
+
107
+ puts "Debug [map_to_worktree_path]: Result: #{result}" if ENV["CLAUDE_SWARM_DEBUG"]
108
+
109
+ result
110
+ end
111
+
112
+ def cleanup_worktrees
113
+ @created_worktrees.each do |worktree_key, worktree_path|
114
+ repo_root = worktree_key.split(":", 2).first
115
+ next unless File.exist?(worktree_path)
116
+
117
+ # Check for uncommitted changes
118
+ if has_uncommitted_changes?(worktree_path)
119
+ puts "⚠️ Warning: Worktree has uncommitted changes, skipping cleanup: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
120
+ next
121
+ end
122
+
123
+ # Check for unpushed commits
124
+ if has_unpushed_commits?(worktree_path)
125
+ puts "⚠️ Warning: Worktree has unpushed commits, skipping cleanup: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
126
+ next
127
+ end
128
+
129
+ puts "Removing worktree: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
130
+
131
+ # Remove the worktree
132
+ output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "remove", worktree_path)
133
+ next if status.success?
134
+
135
+ puts "Warning: Failed to remove worktree: #{output}"
136
+ # Try force remove
137
+ output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "remove", "--force", worktree_path)
138
+ puts "Force remove result: #{output}" unless status.success?
139
+ end
140
+ rescue StandardError => e
141
+ puts "Error during worktree cleanup: #{e.message}"
142
+ end
143
+
144
+ def session_metadata
145
+ {
146
+ enabled: true,
147
+ shared_name: @shared_worktree_name,
148
+ created_paths: @created_worktrees.dup,
149
+ instance_configs: @instance_worktree_configs.dup
150
+ }
151
+ end
152
+
153
+ # Deprecated method for backward compatibility
154
+ def worktree_name
155
+ @shared_worktree_name
156
+ end
157
+
158
+ private
159
+
160
+ def generate_worktree_name
161
+ # Use session ID if available, otherwise generate a random suffix
162
+ if @session_id
163
+ "worktree-#{@session_id}"
164
+ else
165
+ # Fallback to random suffix for tests or when session ID is not available
166
+ random_suffix = SecureRandom.alphanumeric(5).downcase
167
+ "worktree-#{random_suffix}"
168
+ end
169
+ end
170
+
171
+ def determine_worktree_config(instance)
172
+ # Check instance-level worktree setting
173
+ instance_worktree = instance[:worktree]
174
+
175
+ if instance_worktree.nil?
176
+ # No instance-level setting, follow CLI behavior
177
+ if @cli_worktree_option.nil?
178
+ { skip: true }
179
+ else
180
+ { skip: false, name: @shared_worktree_name }
181
+ end
182
+ elsif instance_worktree == false
183
+ # Explicitly disabled for this instance
184
+ { skip: true }
185
+ elsif instance_worktree == true
186
+ # Use shared worktree (either from CLI or auto-generated)
187
+ { skip: false, name: @shared_worktree_name }
188
+ elsif instance_worktree.is_a?(String)
189
+ # Use custom worktree name
190
+ { skip: false, name: instance_worktree }
191
+ else
192
+ raise Error, "Invalid worktree configuration for instance '#{instance[:name]}': #{instance_worktree.inspect}"
193
+ end
194
+ end
195
+
196
+ def collect_worktrees_to_create(instances)
197
+ worktrees_needed = {}
198
+
199
+ instances.each do |instance|
200
+ worktree_config = @instance_worktree_configs[instance[:name]]
201
+ next if worktree_config[:skip]
202
+
203
+ worktree_name = worktree_config[:name]
204
+ directories = instance[:directories] || [instance[:directory]]
205
+
206
+ directories.each do |dir|
207
+ next unless dir
208
+
209
+ expanded_dir = File.expand_path(dir)
210
+ repo_root = find_git_root(expanded_dir)
211
+ next unless repo_root
212
+
213
+ # Track unique repo_root:worktree_name combinations
214
+ worktrees_needed[repo_root] ||= Set.new
215
+ worktrees_needed[repo_root].add(worktree_name)
216
+ end
217
+ end
218
+
219
+ # Convert to array of [repo_root, worktree_name] pairs
220
+ result = []
221
+ worktrees_needed.each do |repo_root, worktree_names|
222
+ worktree_names.each do |worktree_name|
223
+ result << [repo_root, worktree_name]
224
+ end
225
+ end
226
+ result
227
+ end
228
+
229
+ def find_git_root(path)
230
+ current = File.expand_path(path)
231
+
232
+ while current != "/"
233
+ return current if File.exist?(File.join(current, ".git"))
234
+
235
+ current = File.dirname(current)
236
+ end
237
+
238
+ nil
239
+ end
240
+
241
+ def create_worktree(repo_root, worktree_name)
242
+ worktree_key = "#{repo_root}:#{worktree_name}"
243
+ # Create worktrees inside the repository in a .worktrees directory
244
+ worktree_base_dir = File.join(repo_root, ".worktrees")
245
+ worktree_path = File.join(worktree_base_dir, worktree_name)
246
+
247
+ # Check if worktree already exists
248
+ if File.exist?(worktree_path)
249
+ puts "Using existing worktree: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
250
+ @created_worktrees[worktree_key] = worktree_path
251
+ return
252
+ end
253
+
254
+ # Ensure .worktrees directory exists
255
+ FileUtils.mkdir_p(worktree_base_dir)
256
+
257
+ # Create .gitignore inside .worktrees to ignore all contents
258
+ gitignore_path = File.join(worktree_base_dir, ".gitignore")
259
+ File.write(gitignore_path, "# Ignore all worktree contents\n*\n") unless File.exist?(gitignore_path)
260
+
261
+ # Get current branch
262
+ output, status = Open3.capture2e("git", "-C", repo_root, "rev-parse", "--abbrev-ref", "HEAD")
263
+ raise Error, "Failed to get current branch in #{repo_root}: #{output}" unless status.success?
264
+
265
+ current_branch = output.strip
266
+
267
+ # Create worktree with a new branch based on current branch
268
+ branch_name = worktree_name
269
+ puts "Creating worktree: #{worktree_path} with branch: #{branch_name}" unless ENV["CLAUDE_SWARM_PROMPT"]
270
+
271
+ # Create worktree with a new branch
272
+ output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "add", "-b", branch_name, worktree_path, current_branch)
273
+
274
+ # If branch already exists, try without -b flag
275
+ if !status.success? && output.include?("already exists")
276
+ puts "Branch #{branch_name} already exists, using existing branch" unless ENV["CLAUDE_SWARM_PROMPT"]
277
+ output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "add", worktree_path, branch_name)
278
+ end
279
+
280
+ raise Error, "Failed to create worktree: #{output}" unless status.success?
281
+
282
+ @created_worktrees[worktree_key] = worktree_path
283
+ end
284
+
285
+ def has_uncommitted_changes?(worktree_path)
286
+ # Check if there are any uncommitted changes (staged or unstaged)
287
+ output, status = Open3.capture2e("git", "-C", worktree_path, "status", "--porcelain")
288
+ return false unless status.success?
289
+
290
+ # If output is not empty, there are changes
291
+ !output.strip.empty?
292
+ end
293
+
294
+ def has_unpushed_commits?(worktree_path)
295
+ # Get the current branch
296
+ branch_output, branch_status = Open3.capture2e("git", "-C", worktree_path, "rev-parse", "--abbrev-ref", "HEAD")
297
+ return false unless branch_status.success?
298
+
299
+ current_branch = branch_output.strip
300
+
301
+ # Check if the branch has an upstream
302
+ _, upstream_status = Open3.capture2e("git", "-C", worktree_path, "rev-parse", "--abbrev-ref", "#{current_branch}@{upstream}")
303
+
304
+ # If no upstream, check if there are any commits on this branch
305
+ unless upstream_status.success?
306
+ # Get the base branch (usually main or master)
307
+ base_branch = find_base_branch(worktree_path)
308
+
309
+ # If we can't find a base branch or this IS the base branch, check if there are any commits at all
310
+ if base_branch.nil? || current_branch == base_branch
311
+ # Check if this branch has any commits
312
+ commits_output, commits_status = Open3.capture2e("git", "-C", worktree_path, "rev-list", "--count", "HEAD")
313
+ return false unless commits_status.success?
314
+
315
+ # If there's more than 0 commits and no upstream, they're unpushed
316
+ return commits_output.strip.to_i.positive?
317
+ end
318
+
319
+ # Check if this branch has any commits not on the base branch
320
+ commits_output, commits_status = Open3.capture2e("git", "-C", worktree_path, "rev-list", "HEAD", "^#{base_branch}")
321
+ return false unless commits_status.success?
322
+
323
+ # If there are commits, they're unpushed (no upstream set)
324
+ return !commits_output.strip.empty?
325
+ end
326
+
327
+ # Check for unpushed commits
328
+ unpushed_output, unpushed_status = Open3.capture2e("git", "-C", worktree_path, "rev-list", "HEAD", "^#{current_branch}@{upstream}")
329
+ return false unless unpushed_status.success?
330
+
331
+ # If output is not empty, there are unpushed commits
332
+ !unpushed_output.strip.empty?
333
+ end
334
+
335
+ def find_base_branch(repo_path)
336
+ # Try to find the base branch - check for main, master, or the default branch
337
+ %w[main master].each do |branch|
338
+ _, status = Open3.capture2e("git", "-C", repo_path, "rev-parse", "--verify", "refs/heads/#{branch}")
339
+ return branch if status.success?
340
+ end
341
+
342
+ # Try to get the default branch from HEAD
343
+ output, status = Open3.capture2e("git", "-C", repo_path, "symbolic-ref", "refs/remotes/origin/HEAD")
344
+ if status.success?
345
+ # Extract branch name from refs/remotes/origin/main
346
+ branch_match = output.strip.match(%r{refs/remotes/origin/(.+)$})
347
+ return branch_match[1] if branch_match
348
+ end
349
+
350
+ nil
351
+ end
352
+ end
353
+ end
data/llms.txt CHANGED
@@ -57,7 +57,7 @@ A collection of Claude instances (agents) working together. One instance is desi
57
57
  An individual Claude Code agent with:
58
58
  - **description** (required): Role and responsibilities
59
59
  - **directory**: Working directory context
60
- - **model**: Claude model (opus/sonnet/haiku)
60
+ - **model**: Claude model (opus/sonnet/claude-3-5-haiku-20241022)
61
61
  - **connections**: Other instances it can delegate to
62
62
  - **allowed_tools**: Tools this instance can use
63
63
  - **disallowed_tools**: Explicitly denied tools (override allowed)
@@ -79,7 +79,7 @@ swarm:
79
79
  instance_name:
80
80
  description: "Agent role description" # REQUIRED
81
81
  directory: ~/path/to/dir # Working directory
82
- model: opus # opus/sonnet/haiku
82
+ model: opus # opus/sonnet/claude-3-5-haiku-20241022
83
83
  connections: [other1, other2] # Connected instances
84
84
  prompt: "Custom system prompt" # Additional instructions
85
85
  vibe: false # Skip permissions (default: false)
data/single.yml ADDED
@@ -0,0 +1,92 @@
1
+ version: 1
2
+ swarm:
3
+ name: "Claude Swarm Development"
4
+ main: lead_developer
5
+ instances:
6
+ lead_developer:
7
+ description: "Lead developer responsible for developing and maintaining the Claude Swarm gem"
8
+ directory: .
9
+ model: opus
10
+ vibe: true
11
+ connections: [github_expert]
12
+ prompt: |
13
+ You are the lead developer of Claude Swarm, a Ruby gem that orchestrates multiple Claude Code instances as a collaborative AI development team. The gem enables running AI agents with specialized roles, tools, and directory contexts, communicating via MCP (Model Context Protocol) in a tree-like hierarchy.
14
+ Use the github_expert to help you with git and github related tasks.
15
+
16
+ Your responsibilities include:
17
+ - Developing new features and improvements for the Claude Swarm gem
18
+ - Writing clean, maintainable Ruby code following best practices
19
+ - Creating and updating tests using RSpec or similar testing frameworks
20
+ - Maintaining comprehensive documentation in README.md and code comments
21
+ - Managing the gem's dependencies and version compatibility
22
+ - Implementing robust error handling and validation
23
+ - Optimizing performance and resource usage
24
+ - Ensuring the CLI interface is intuitive and user-friendly
25
+ - Debugging issues and fixing bugs reported by users
26
+ - Reviewing and refactoring existing code for better maintainability
27
+
28
+ Key technical areas to focus on:
29
+ - YAML configuration parsing and validation
30
+ - MCP (Model Context Protocol) server implementation
31
+ - Session management and persistence
32
+ - Inter-instance communication mechanisms
33
+ - CLI command handling and option parsing
34
+ - Git worktree integration
35
+ - Cost tracking and monitoring features
36
+ - Process management and cleanup
37
+ - Logging and debugging capabilities
38
+
39
+ When developing features:
40
+ - Consider edge cases and error scenarios
41
+ - Write comprehensive tests for new functionality
42
+ - Update documentation to reflect changes
43
+ - Ensure backward compatibility when possible
44
+ - Follow semantic versioning principles
45
+ - Add helpful error messages and validation
46
+ - Always write tests for new functionality
47
+ - Run linter with `bundle exec rubocop -A`
48
+ - Run tests with `bundle exec rake test`
49
+
50
+ For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
51
+
52
+ Don't hold back. Give it your all. Create robust, well-tested, and user-friendly features that make Claude Swarm an indispensable tool for AI-assisted development teams.
53
+
54
+ github_expert:
55
+ description: "GitHub operations specialist using gh CLI"
56
+ directory: .
57
+ model: sonnet
58
+ vibe: true
59
+ prompt: |
60
+ You are the GitHub operations specialist for the Roast gem project. You handle all GitHub-related tasks using the `gh` command-line tool.
61
+
62
+ Your responsibilities:
63
+ - Create and manage issues: `gh issue create`, `gh issue list`
64
+ - Handle pull requests: `gh pr create`, `gh pr review`, `gh pr merge`
65
+ - Manage releases: `gh release create`
66
+ - Check workflow runs: `gh run list`, `gh run view`
67
+ - Manage repository settings and configurations
68
+ - Handle branch operations and protection rules
69
+
70
+ Common operations you perform:
71
+ 1. Creating feature branches and PRs
72
+ 2. Running and monitoring CI/CD workflows
73
+ 3. Managing issue labels and milestones
74
+ 4. Creating releases with proper changelogs
75
+ 5. Reviewing and merging pull requests
76
+ 6. Setting up GitHub Actions workflows
77
+
78
+ Best practices to follow:
79
+ - Always create feature branches for new work
80
+ - Write clear PR descriptions with context
81
+ - Ensure CI passes before merging
82
+ - Use conventional commit messages
83
+ - Tag releases following semantic versioning
84
+ - Keep issues organized with appropriate labels
85
+
86
+ When working with the team:
87
+ - Create issues for bugs found by test_runner
88
+ - Open PRs for code reviewed by solid_critic
89
+ - Set up CI to run code_quality checks
90
+ - Document Raix integration in wiki/docs
91
+
92
+ For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
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.17
4
+ version: 0.1.19
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda
@@ -67,6 +67,7 @@ files:
67
67
  - example/test-generation.yml
68
68
  - examples/monitoring-demo.yml
69
69
  - examples/multi-directory.yml
70
+ - examples/with-before-commands.yml
70
71
  - exe/claude-swarm
71
72
  - lib/claude_swarm.rb
72
73
  - lib/claude_swarm/claude_code_executor.rb
@@ -83,7 +84,9 @@ files:
83
84
  - lib/claude_swarm/session_path.rb
84
85
  - lib/claude_swarm/task_tool.rb
85
86
  - lib/claude_swarm/version.rb
87
+ - lib/claude_swarm/worktree_manager.rb
86
88
  - llms.txt
89
+ - single.yml
87
90
  homepage: https://github.com/parruda/claude-swarm
88
91
  licenses: []
89
92
  metadata: