claude_swarm 0.1.19 → 0.2.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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -63
  3. data/.rubocop_todo.yml +11 -0
  4. data/CHANGELOG.md +110 -0
  5. data/CLAUDE.md +64 -2
  6. data/README.md +190 -28
  7. data/Rakefile +1 -1
  8. data/examples/mixed-provider-swarm.yml +23 -0
  9. data/examples/monitoring-demo.yml +4 -4
  10. data/lib/claude_swarm/claude_code_executor.rb +7 -13
  11. data/lib/claude_swarm/claude_mcp_server.rb +26 -17
  12. data/lib/claude_swarm/cli.rb +384 -265
  13. data/lib/claude_swarm/commands/ps.rb +22 -24
  14. data/lib/claude_swarm/commands/show.rb +45 -63
  15. data/lib/claude_swarm/configuration.rb +137 -8
  16. data/lib/claude_swarm/mcp_generator.rb +39 -15
  17. data/lib/claude_swarm/openai/chat_completion.rb +264 -0
  18. data/lib/claude_swarm/openai/executor.rb +301 -0
  19. data/lib/claude_swarm/openai/responses.rb +338 -0
  20. data/lib/claude_swarm/orchestrator.rb +221 -45
  21. data/lib/claude_swarm/process_tracker.rb +7 -7
  22. data/lib/claude_swarm/session_cost_calculator.rb +93 -0
  23. data/lib/claude_swarm/session_path.rb +3 -5
  24. data/lib/claude_swarm/system_utils.rb +16 -0
  25. data/lib/claude_swarm/templates/generation_prompt.md.erb +230 -0
  26. data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
  27. data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
  28. data/lib/claude_swarm/tools/task_tool.rb +43 -0
  29. data/lib/claude_swarm/version.rb +1 -1
  30. data/lib/claude_swarm/worktree_manager.rb +145 -48
  31. data/lib/claude_swarm.rb +34 -12
  32. data/llms.txt +2 -2
  33. data/single.yml +482 -6
  34. data/team.yml +344 -0
  35. metadata +65 -14
  36. data/claude-swarm.yml +0 -64
  37. data/lib/claude_swarm/reset_session_tool.rb +0 -22
  38. data/lib/claude_swarm/session_info_tool.rb +0 -22
  39. data/lib/claude_swarm/task_tool.rb +0 -39
  40. /data/{example → examples}/claude-swarm.yml +0 -0
  41. /data/{example → examples}/microservices-team.yml +0 -0
  42. /data/{example → examples}/session-restoration-demo.yml +0 -0
  43. /data/{example → examples}/test-generation.yml +0 -0
@@ -0,0 +1,230 @@
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.
2
+
3
+ ## Claude Swarm Overview
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).
5
+
6
+ Key capabilities:
7
+ - Define multiple AI instances with different roles and specializations
8
+ - Set up connections between instances for collaboration
9
+ - Restrict tools based on each instance's responsibilities
10
+ - Run instances in different directories or Git worktrees
11
+ - Support for custom system prompts per instance
12
+ - Choose appropriate models (opus for complex tasks, sonnet for simpler ones)
13
+
14
+ ## Your Task
15
+ 1. Start by asking about the user's project structure and development needs
16
+ 2. Understand what kind of team they need (roles, specializations)
17
+ 3. Suggest an appropriate swarm topology based on their needs
18
+ 4. Help them refine and customize the configuration
19
+ 5. Generate the final claude-swarm.yml content
20
+ 6. When the configuration is complete, save it to: <%= output_file || "a descriptive filename based on the swarm's function" %>
21
+
22
+ ## File Naming Convention
23
+ <% if output_file %>
24
+ The user has specified the output file: <%= output_file %>
25
+ <% else %>
26
+ Since no output file was specified, name the file based on the swarm's function. Examples:
27
+ - web-dev-swarm.yml for full-stack web development teams
28
+ - data-pipeline-swarm.yml for data processing teams
29
+ - microservices-swarm.yml for microservice architectures
30
+ - mobile-app-swarm.yml for mobile development teams
31
+ - ml-research-swarm.yml for machine learning teams
32
+ - devops-swarm.yml for infrastructure and deployment teams
33
+ Use descriptive names that clearly indicate the swarm's purpose.
34
+ <% end %>
35
+
36
+ ## Configuration Structure
37
+ ```yaml
38
+ version: 1
39
+ swarm:
40
+ name: "Descriptive Swarm Name"
41
+ main: main_instance_name
42
+ instances:
43
+ instance_name:
44
+ description: "Clear description of role and responsibilities"
45
+ directory: ./path/to/directory
46
+ model: sonnet # or opus for complex tasks
47
+ allowed_tools: [Read, Edit, Write, Bash]
48
+ connections: [other_instance_names] # Optional
49
+ prompt: |
50
+ Custom system prompt for specialization
51
+
52
+ ```
53
+
54
+ ## Best Practices to Follow
55
+ - Use descriptive, role-based instance names (e.g., frontend_dev, api_architect)
56
+ - Write prompts using multi-line strings to make them more readable.
57
+ - Write clear descriptions explaining each instance's responsibilities
58
+ - Choose opus model for complex architectural or algorithmic tasks and routine development.
59
+ - Choose sonnet model for simpler tasks
60
+ - Set up logical connections (e.g., lead → team members, architect → implementers), but avoid circular dependencies.
61
+ - 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
+ - Select tools based on each instance's actual needs:
63
+ - Read: For code review and analysis roles
64
+ - Edit: For active development roles
65
+ - Write: For creating new files
66
+ - Bash: For running commands, tests, builds
67
+ - MultiEdit: For editing multiple files at once
68
+ - WebFetch: For fetching information from the web
69
+ - WebSearch: For searching the web
70
+ - Use custom prompts to specialize each instance's expertise
71
+ - Organize directories to match project structure
72
+
73
+ ## Interactive Questions to Ask
74
+ - What type of project are you working on?
75
+ - What's your project's directory structure?
76
+ - What are the main technologies/frameworks you're using?
77
+ - What development tasks do you need help with?
78
+ - Do you need specialized roles (testing, DevOps, documentation)?
79
+ - Are there specific areas that need focused attention?
80
+ - Do you have multiple repositories or services to coordinate?
81
+
82
+ <full_readme>
83
+ <%= readme_content %>
84
+ </full_readme>
85
+
86
+ <prompt_best_practices>
87
+ # Claude 4 prompt engineering best practices
88
+
89
+ This guide provides specific prompt engineering techniques for Claude 4 models (Opus 4 and Sonnet 4) to help you achieve optimal results in your applications. These models have been trained for more precise instruction following than previous generations of Claude models.
90
+
91
+ ## General principles
92
+
93
+ ### Be explicit with your instructions
94
+
95
+ Claude 4 models respond well to clear, explicit instructions. Being specific about your desired output can help enhance results. Customers who desire the "above and beyond" behavior from previous Claude models might need to more explicitly request these behaviors with Claude 4.
96
+
97
+ **Less effective:**
98
+
99
+ ```text
100
+ Create an analytics dashboard
101
+ ```
102
+
103
+ **More effective:**
104
+
105
+ ```text
106
+ Create an analytics dashboard. Include as many relevant features and interactions as possible. Go beyond the basics to create a fully-featured implementation.
107
+ ```
108
+
109
+ ### Add context to improve performance
110
+
111
+ Providing context or motivation behind your instructions, such as explaining to Claude why such behavior is important, can help Claude 4 better understand your goals and deliver more targeted responses.
112
+
113
+ **Less effective:**
114
+
115
+ ```text
116
+ NEVER use ellipses
117
+ ```
118
+
119
+ **More effective:**
120
+
121
+ ```text
122
+ Your response will be read aloud by a text-to-speech engine, so never use ellipses since the text-to-speech engine will not know how to pronounce them.
123
+ ```
124
+
125
+ Claude is smart enough to generalize from the explanation.
126
+
127
+ ### Be vigilant with examples & details
128
+
129
+ Claude 4 models pay attention to details and examples as part of instruction following. Ensure that your examples align with the behaviors you want to encourage and minimize behaviors you want to avoid.
130
+
131
+ ## Guidance for specific situations
132
+
133
+ ### Control the format of responses
134
+
135
+ There are a few ways that we have found to be particularly effective in seering output formatting in Claude 4 models:
136
+
137
+ 1. **Tell Claude what to do instead of what not to do**
138
+
139
+ * Instead of: "Do not use markdown in your response"
140
+ * Try: "Your response should be composed of smoothly flowing prose paragraphs."
141
+
142
+ 2. **Use XML format indicators**
143
+
144
+ * Try: "Write the prose sections of your response in <smoothly_flowing_prose_paragraphs> tags."
145
+
146
+ 3. **Match your prompt style to the desired output**
147
+
148
+ The formatting style used in your prompt may influence Claude's response style. If you are still experiencing steerability issues with output formatting, we recommend as best as you can matching your prompt style to your desired output style. For exmaple, removing markdown from your prompt can reduce the volume of markdown in the output.
149
+
150
+ ### Leverage thinking & interleaved thinking capabilities
151
+
152
+ Claude 4 offers thinking capabilities that can be especially helpful for tasks involving reflection after tool use or complex multi-step reasoning. You can guide its initial or interleaved thinking for better results.
153
+
154
+ ```text Example prompt
155
+ After receiving tool results, carefully reflect on their quality and determine optimal next steps before proceeding. Use your thinking to plan and iterate based on this new information, and then take the best next action.
156
+ ```
157
+
158
+ ### Optimize parallel tool calling
159
+
160
+ Claude 4 models excel at parallel tool execution. They have a high success rate in using parallel tool calling without any prompting to do so, but some minor prompting can boost this behavior to ~100% parallel tool use success rate. We have found this prompt to be most effective:
161
+
162
+ ```text Sample prompt for agents
163
+ For maximum efficiency, whenever you need to perform multiple independent operations, invoke all relevant tools simultaneously rather than sequentially.
164
+ ```
165
+
166
+ ### Reduce file creation in agentic coding
167
+
168
+ Claude 4 models may sometimes create new files for testing and iteration purposes, particularly when working with code. This approach allows Claude to use files, especially python scripts, as a 'temporary scratchpad' before saving its final output. Using temporary files can improve outcomes particularly for agentic coding use cases.
169
+
170
+ If you'd prefer to minimize net new file creation, you can instruct Claude to clean up after itself:
171
+
172
+ ```text Sample prompt
173
+ If you create any temporary new files, scripts, or helper files for iteration, clean up these files by removing them at the end of the task.
174
+ ```
175
+
176
+ ### Enhance visual and frontend code generation
177
+
178
+ For frontend code generation, you can steer Claude 4 models to create complex, detailed, and interactive designs by providing explicit encouragement:
179
+
180
+ ```text Sample prompt
181
+ Don't hold back. Give it your all.
182
+ ```
183
+
184
+ You can also improve Claude's frontend performance in specific areas by providing additional modifiers and details on what to focus on:
185
+
186
+ * "Include as many relevant features and interactions as possible"
187
+ * "Add thoughtful details like hover states, transitions, and micro-interactions"
188
+ * "Create an impressive demonstration showcasing web development capabilities"
189
+ * "Apply design principles: hierarchy, contrast, balance, and movement"
190
+
191
+ ### Avoid focusing on passing tests and hard-coding
192
+
193
+ Frontier language models can sometimes focus too heavily on making tests pass at the expense of more general solutions. To prevent this behavior and ensure robust, generalizable solutions:
194
+
195
+ ```text
196
+ Please write a high quality, general purpose solution. Implement a solution that works correctly for all valid inputs, not just the test cases. Do not hard-code values or create solutions that only work for specific test inputs. Instead, implement the actual logic that solves the problem generally.
197
+
198
+ Focus on understanding the problem requirements and implementing the correct algorithm. Tests are there to verify correctness, not to define the solution. Provide a principled implementation that follows best practices and software design principles.
199
+
200
+ If the task is unreasonable or infeasible, or if any of the tests are incorrect, please tell me. The solution should be robust, maintainable, and extendable.
201
+ ```
202
+
203
+ 1. **Be specific about desired behavior**: Consider describing exactly what you'd like to see in the output.
204
+
205
+ 2. **Frame your instructions with modifiers**: Adding modifiers that encourage Claude to increase the quality and detail of its output can help better shape Claude's performance. For example, instead of "Create an analytics dashboard", use "Create an analytics dashboard. Include as many relevant features and interactions as possible. Go beyond the basics to create a fully-featured implementation."
206
+
207
+ 3. **Request specific features explicitly**: Animations and interactive elements should be requested explicitly when desired.
208
+
209
+
210
+ # Be clear, direct, and detailed
211
+
212
+ When interacting with Claude, think of it as a brilliant but very new employee (with amnesia) who needs explicit instructions. Like any new employee, Claude does not have context on your norms, styles, guidelines, or preferred ways of working.
213
+ The more precisely you explain what you want, the better Claude's response will be.
214
+
215
+ **The golden rule of clear prompting:** Show your prompt to a colleague, ideally someone who has minimal context on the task, and ask them to follow the instructions. If they're confused, Claude will likely be too.
216
+
217
+ ## How to be clear, contextual, and specific
218
+
219
+ * **Give Claude contextual information:** Just like you might be able to better perform on a task if you knew more context, Claude will perform better if it has more contextual information. Some examples of contextual information:
220
+ * What the task results will be used for
221
+ * What audience the output is meant for
222
+ * What workflow the task is a part of, and where this task belongs in that workflow
223
+ * The end goal of the task, or what a successful task completion looks like
224
+ * **Be specific about what you want Claude to do:** For example, if you want Claude to output only code and nothing else, say so.
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
+
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
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ module Tools
5
+ class ResetSessionTool < FastMcp::Tool
6
+ tool_name "reset_session"
7
+ description "Reset the Claude session for this agent, starting fresh on the next task"
8
+
9
+ arguments do
10
+ # No arguments needed
11
+ end
12
+
13
+ def call
14
+ executor = ClaudeMcpServer.executor
15
+ executor.reset_session
16
+
17
+ {
18
+ success: true,
19
+ message: "Session has been reset",
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ module Tools
5
+ class SessionInfoTool < FastMcp::Tool
6
+ tool_name "session_info"
7
+ description "Get information about the current Claude session for this agent"
8
+
9
+ arguments do
10
+ # No arguments needed
11
+ end
12
+
13
+ def call
14
+ executor = ClaudeMcpServer.executor
15
+
16
+ {
17
+ has_session: executor.has_session?,
18
+ session_id: executor.session_id,
19
+ working_directory: executor.working_directory,
20
+ }
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeSwarm
4
+ module Tools
5
+ class TaskTool < FastMcp::Tool
6
+ tool_name "task"
7
+ description "Execute a task using Claude Code. There is no description parameter."
8
+ annotations(read_only_hint: true, open_world_hint: false, destructive_hint: false)
9
+
10
+ arguments do
11
+ required(:prompt).filled(:string).description("The task or question for the agent")
12
+ optional(:new_session).filled(:bool).description("Start a new session (default: false)")
13
+ optional(:system_prompt).filled(:string).description("Override the system prompt for this request")
14
+ optional(:description).filled(:string).description("A description for the request")
15
+ end
16
+
17
+ def call(prompt:, new_session: false, system_prompt: nil, description: nil)
18
+ executor = ClaudeMcpServer.executor
19
+ instance_config = ClaudeMcpServer.instance_config
20
+
21
+ options = {
22
+ new_session: new_session,
23
+ system_prompt: system_prompt || instance_config[:prompt],
24
+ description: description,
25
+ }
26
+
27
+ # Add allowed tools from instance config
28
+ options[:allowed_tools] = instance_config[:allowed_tools] if instance_config[:allowed_tools]&.any?
29
+
30
+ # Add disallowed tools from instance config
31
+ options[:disallowed_tools] = instance_config[:disallowed_tools] if instance_config[:disallowed_tools]&.any?
32
+
33
+ # Add connections from instance config
34
+ options[:connections] = instance_config[:connections] if instance_config[:connections]&.any?
35
+
36
+ response = executor.execute(prompt, options)
37
+
38
+ # Return just the result text as expected by MCP
39
+ response["result"]
40
+ end
41
+ end
42
+ end
43
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "0.1.19"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -1,13 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "open3"
4
- require "fileutils"
5
- require "json"
6
- require "pathname"
7
- require "securerandom"
8
-
9
3
  module ClaudeSwarm
10
4
  class WorktreeManager
5
+ include SystemUtils
11
6
  attr_reader :shared_worktree_name, :created_worktrees
12
7
 
13
8
  def initialize(cli_worktree_option = nil, session_id: nil)
@@ -15,10 +10,10 @@ module ClaudeSwarm
15
10
  @session_id = session_id
16
11
  # Generate a name based on session ID if no option given, empty string, or default "worktree" from Thor
17
12
  @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
13
+ generate_worktree_name
14
+ else
15
+ cli_worktree_option
16
+ end
22
17
  @created_worktrees = {} # Maps "repo_root:worktree_name" to worktree_path
23
18
  @instance_worktree_configs = {} # Stores per-instance worktree settings
24
19
  end
@@ -99,10 +94,10 @@ module ClaudeSwarm
99
94
 
100
95
  # Return the equivalent path in the worktree
101
96
  result = if relative_path == "."
102
- worktree_path
103
- else
104
- File.join(worktree_path, relative_path)
105
- end
97
+ worktree_path
98
+ else
99
+ File.join(worktree_path, relative_path)
100
+ end
106
101
 
107
102
  puts "Debug [map_to_worktree_path]: Result: #{result}" if ENV["CLAUDE_SWARM_DEBUG"]
108
103
 
@@ -121,7 +116,9 @@ module ClaudeSwarm
121
116
  end
122
117
 
123
118
  # Check for unpushed commits
124
- if has_unpushed_commits?(worktree_path)
119
+ has_unpushed = has_unpushed_commits?(worktree_path)
120
+
121
+ if has_unpushed
125
122
  puts "⚠️ Warning: Worktree has unpushed commits, skipping cleanup: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
126
123
  next
127
124
  end
@@ -137,16 +134,43 @@ module ClaudeSwarm
137
134
  output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "remove", "--force", worktree_path)
138
135
  puts "Force remove result: #{output}" unless status.success?
139
136
  end
137
+
138
+ # Clean up external worktree directories
139
+ cleanup_external_directories
140
140
  rescue StandardError => e
141
141
  puts "Error during worktree cleanup: #{e.message}"
142
142
  end
143
143
 
144
+ def cleanup_external_directories
145
+ # Remove session-specific worktree directory if it exists and is empty
146
+ return unless @session_id
147
+
148
+ session_worktree_dir = File.join(File.expand_path("~/.claude-swarm/worktrees"), @session_id)
149
+ return unless File.exist?(session_worktree_dir)
150
+
151
+ # Try to remove the directory tree
152
+ begin
153
+ # Remove all empty directories recursively
154
+ Dir.glob(File.join(session_worktree_dir, "**/*"), File::FNM_DOTMATCH).reverse_each do |path|
155
+ next if path.end_with?("/.", "/..")
156
+
157
+ FileUtils.rmdir(path) if File.directory?(path) && Dir.empty?(path)
158
+ end
159
+ # Finally try to remove the session directory itself
160
+ FileUtils.rmdir(session_worktree_dir) if Dir.empty?(session_worktree_dir)
161
+ rescue Errno::ENOTEMPTY
162
+ puts "Note: Session worktree directory not empty, leaving in place: #{session_worktree_dir}" unless ENV["CLAUDE_SWARM_PROMPT"]
163
+ rescue StandardError => e
164
+ puts "Warning: Error cleaning up worktree directories: #{e.message}" unless ENV["CLAUDE_SWARM_PROMPT"]
165
+ end
166
+ end
167
+
144
168
  def session_metadata
145
169
  {
146
170
  enabled: true,
147
171
  shared_name: @shared_worktree_name,
148
172
  created_paths: @created_worktrees.dup,
149
- instance_configs: @instance_worktree_configs.dup
173
+ instance_configs: @instance_worktree_configs.dup,
150
174
  }
151
175
  end
152
176
 
@@ -157,6 +181,35 @@ module ClaudeSwarm
157
181
 
158
182
  private
159
183
 
184
+ def external_worktree_path(repo_root, worktree_name)
185
+ # Get repository name from path
186
+ repo_name = sanitize_repo_name(File.basename(repo_root))
187
+
188
+ # Add a short hash of the full path to handle multiple repos with same name
189
+ path_hash = Digest::SHA256.hexdigest(repo_root)[0..7]
190
+ unique_repo_name = "#{repo_name}-#{path_hash}"
191
+
192
+ # Build external path: ~/.claude-swarm/worktrees/[session_id]/[repo_name-hash]/[worktree_name]
193
+ base_dir = File.expand_path("~/.claude-swarm/worktrees")
194
+
195
+ # Validate base directory is accessible
196
+ begin
197
+ FileUtils.mkdir_p(base_dir)
198
+ rescue Errno::EACCES
199
+ # Fall back to temp directory if home is not writable
200
+ base_dir = File.join(Dir.tmpdir, ".claude-swarm", "worktrees")
201
+ FileUtils.mkdir_p(base_dir)
202
+ end
203
+
204
+ session_dir = @session_id || "default"
205
+ File.join(base_dir, session_dir, unique_repo_name, worktree_name)
206
+ end
207
+
208
+ def sanitize_repo_name(name)
209
+ # Replace problematic characters with underscores
210
+ name.gsub(/[^a-zA-Z0-9._-]/, "_")
211
+ end
212
+
160
213
  def generate_worktree_name
161
214
  # Use session ID if available, otherwise generate a random suffix
162
215
  if @session_id
@@ -240,9 +293,8 @@ module ClaudeSwarm
240
293
 
241
294
  def create_worktree(repo_root, worktree_name)
242
295
  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)
296
+ # Create worktrees in external directory
297
+ worktree_path = external_worktree_path(repo_root, worktree_name)
246
298
 
247
299
  # Check if worktree already exists
248
300
  if File.exist?(worktree_path)
@@ -251,12 +303,16 @@ module ClaudeSwarm
251
303
  return
252
304
  end
253
305
 
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)
306
+ # Ensure parent directory exists with proper error handling
307
+ begin
308
+ FileUtils.mkdir_p(File.dirname(worktree_path))
309
+ rescue Errno::EACCES => e
310
+ raise Error, "Permission denied creating worktree directory: #{e.message}"
311
+ rescue Errno::ENOSPC => e
312
+ raise Error, "Not enough disk space for worktree: #{e.message}"
313
+ rescue StandardError => e
314
+ raise Error, "Failed to create worktree directory: #{e.message}"
315
+ end
260
316
 
261
317
  # Get current branch
262
318
  output, status = Open3.capture2e("git", "-C", repo_root, "rev-parse", "--abbrev-ref", "HEAD")
@@ -277,6 +333,26 @@ module ClaudeSwarm
277
333
  output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "add", worktree_path, branch_name)
278
334
  end
279
335
 
336
+ # If worktree path is already in use, it might be from a previous run
337
+ if !status.success? && output.include?("is already used by worktree")
338
+ puts "Worktree path already in use, checking if it's valid" unless ENV["CLAUDE_SWARM_PROMPT"]
339
+ # Check if the worktree actually exists at that path
340
+ if File.exist?(File.join(worktree_path, ".git"))
341
+ puts "Using existing worktree: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
342
+ @created_worktrees[worktree_key] = worktree_path
343
+ return
344
+ else
345
+ # The worktree is registered but the directory doesn't exist, prune and retry
346
+ puts "Pruning stale worktree references" unless ENV["CLAUDE_SWARM_PROMPT"]
347
+ begin
348
+ system!("git", "-C", repo_root, "worktree", "prune")
349
+ rescue Error
350
+ # Ignore errors when pruning
351
+ end
352
+ output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "add", worktree_path, branch_name)
353
+ end
354
+ end
355
+
280
356
  raise Error, "Failed to create worktree: #{output}" unless status.success?
281
357
 
282
358
  @created_worktrees[worktree_key] = worktree_path
@@ -301,40 +377,61 @@ module ClaudeSwarm
301
377
  # Check if the branch has an upstream
302
378
  _, upstream_status = Open3.capture2e("git", "-C", worktree_path, "rev-parse", "--abbrev-ref", "#{current_branch}@{upstream}")
303
379
 
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)
380
+ # If branch has upstream, check against it
381
+ if upstream_status.success?
382
+ # Check for unpushed commits against upstream
383
+ unpushed_output, unpushed_status = Open3.capture2e("git", "-C", worktree_path, "rev-list", "HEAD", "^#{current_branch}@{upstream}")
384
+ return false unless unpushed_status.success?
308
385
 
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?
386
+ # If output is not empty, there are unpushed commits
387
+ return !unpushed_output.strip.empty?
388
+ end
314
389
 
315
- # If there's more than 0 commits and no upstream, they're unpushed
316
- return commits_output.strip.to_i.positive?
317
- end
390
+ # No upstream - this is likely a new branch created by the worktree
391
+ # The key insight: when git worktree add -b creates a branch, it creates it in BOTH
392
+ # the worktree AND the main repository. So we need to check the reflog in the worktree
393
+ # to see if any NEW commits were made after the worktree was created.
394
+
395
+ # Use reflog to check if any commits were made in this worktree
396
+ reflog_output, reflog_status = Open3.capture2e("git", "-C", worktree_path, "reflog", "--format=%H %gs", current_branch)
318
397
 
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?
398
+ if reflog_status.success? && !reflog_output.strip.empty?
399
+ reflog_lines = reflog_output.strip.split("\n")
322
400
 
323
- # If there are commits, they're unpushed (no upstream set)
324
- return !commits_output.strip.empty?
401
+ # Look for commit entries (not branch creation)
402
+ commit_entries = reflog_lines.select { |line| line.include?(" commit:") || line.include?(" commit (amend):") }
403
+
404
+ # If there are commit entries in the reflog, there are unpushed commits
405
+ return !commit_entries.empty?
325
406
  end
326
407
 
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?
408
+ # As a fallback, assume no unpushed commits
409
+ false
410
+ end
330
411
 
331
- # If output is not empty, there are unpushed commits
332
- !unpushed_output.strip.empty?
412
+ def find_original_repo_for_worktree(worktree_path)
413
+ # Get the git directory for this worktree
414
+ _, git_dir_status = Open3.capture2e("git", "-C", worktree_path, "rev-parse", "--git-dir")
415
+ return unless git_dir_status.success?
416
+
417
+ # Read the gitdir file to find the main repository
418
+ # Worktree .git files contain: gitdir: /path/to/main/repo/.git/worktrees/worktree-name
419
+ if File.file?(File.join(worktree_path, ".git"))
420
+ gitdir_content = File.read(File.join(worktree_path, ".git")).strip
421
+ if gitdir_content =~ /^gitdir: (.+)$/
422
+ git_path = ::Regexp.last_match(1)
423
+ # Extract the main repo path from the worktree git path
424
+ # Format: /path/to/repo/.git/worktrees/worktree-name
425
+ return ::Regexp.last_match(1) if git_path =~ %r{^(.+)/\.git/worktrees/[^/]+$}
426
+ end
427
+ end
428
+
429
+ nil
333
430
  end
334
431
 
335
432
  def find_base_branch(repo_path)
336
433
  # Try to find the base branch - check for main, master, or the default branch
337
- %w[main master].each do |branch|
434
+ ["main", "master"].each do |branch|
338
435
  _, status = Open3.capture2e("git", "-C", repo_path, "rev-parse", "--verify", "refs/heads/#{branch}")
339
436
  return branch if status.success?
340
437
  end
data/lib/claude_swarm.rb CHANGED
@@ -1,17 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "claude_swarm/version"
4
- require_relative "claude_swarm/cli"
5
- require_relative "claude_swarm/configuration"
6
- require_relative "claude_swarm/mcp_generator"
7
- require_relative "claude_swarm/orchestrator"
8
- require_relative "claude_swarm/claude_code_executor"
9
- require_relative "claude_swarm/claude_mcp_server"
10
- require_relative "claude_swarm/session_path"
11
- require_relative "claude_swarm/session_info_tool"
12
- require_relative "claude_swarm/reset_session_tool"
13
- require_relative "claude_swarm/task_tool"
14
- require_relative "claude_swarm/process_tracker"
3
+ # Standard library dependencies
4
+ require "digest"
5
+ require "English"
6
+ require "erb"
7
+ require "fileutils"
8
+ require "io/console"
9
+ require "json"
10
+ require "logger"
11
+ require "open3"
12
+ require "pathname"
13
+ require "pty"
14
+ require "securerandom"
15
+ require "set"
16
+ require "shellwords"
17
+ require "time"
18
+ require "timeout"
19
+ require "tmpdir"
20
+ require "yaml"
21
+
22
+ # External dependencies
23
+ require "fast_mcp_annotations"
24
+ require "mcp_client"
25
+ require "openai"
26
+ require "thor"
27
+
28
+ # Zeitwerk setup
29
+ require "zeitwerk"
30
+ loader = Zeitwerk::Loader.for_gem
31
+ loader.ignore("#{__dir__}/claude_swarm/templates")
32
+ loader.inflector.inflect(
33
+ "cli" => "CLI",
34
+ "openai" => "OpenAI",
35
+ )
36
+ loader.setup
15
37
 
16
38
  module ClaudeSwarm
17
39
  class Error < StandardError; end