swarm_memory 2.1.5 → 2.1.6

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 (182) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. metadata +5 -184
  4. data/lib/claude_swarm/base_executor.rb +0 -133
  5. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  6. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  7. data/lib/claude_swarm/cli.rb +0 -697
  8. data/lib/claude_swarm/commands/ps.rb +0 -215
  9. data/lib/claude_swarm/commands/show.rb +0 -139
  10. data/lib/claude_swarm/configuration.rb +0 -373
  11. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  12. data/lib/claude_swarm/json_handler.rb +0 -91
  13. data/lib/claude_swarm/mcp_generator.rb +0 -230
  14. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  15. data/lib/claude_swarm/openai/executor.rb +0 -256
  16. data/lib/claude_swarm/openai/responses.rb +0 -319
  17. data/lib/claude_swarm/orchestrator.rb +0 -878
  18. data/lib/claude_swarm/process_tracker.rb +0 -78
  19. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  20. data/lib/claude_swarm/session_path.rb +0 -42
  21. data/lib/claude_swarm/settings_generator.rb +0 -77
  22. data/lib/claude_swarm/system_utils.rb +0 -46
  23. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  24. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  25. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  27. data/lib/claude_swarm/version.rb +0 -5
  28. data/lib/claude_swarm/worktree_manager.rb +0 -475
  29. data/lib/claude_swarm/yaml_loader.rb +0 -22
  30. data/lib/claude_swarm.rb +0 -67
  31. data/lib/swarm_cli/cli.rb +0 -201
  32. data/lib/swarm_cli/command_registry.rb +0 -61
  33. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  34. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  35. data/lib/swarm_cli/commands/migrate.rb +0 -55
  36. data/lib/swarm_cli/commands/run.rb +0 -173
  37. data/lib/swarm_cli/config_loader.rb +0 -98
  38. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  39. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  40. data/lib/swarm_cli/interactive_repl.rb +0 -924
  41. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  42. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  43. data/lib/swarm_cli/migrate_options.rb +0 -54
  44. data/lib/swarm_cli/migrator.rb +0 -132
  45. data/lib/swarm_cli/options.rb +0 -151
  46. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  47. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  48. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  49. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  50. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  51. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  52. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  53. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  54. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  55. data/lib/swarm_cli/ui/icons.rb +0 -36
  56. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  57. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  58. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  59. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  60. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  61. data/lib/swarm_cli/version.rb +0 -5
  62. data/lib/swarm_cli.rb +0 -46
  63. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  64. data/lib/swarm_sdk/agent/builder.rb +0 -552
  65. data/lib/swarm_sdk/agent/chat.rb +0 -774
  66. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  67. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  68. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  69. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  70. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  71. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  72. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  73. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  75. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  76. data/lib/swarm_sdk/agent/context.rb +0 -116
  77. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  78. data/lib/swarm_sdk/agent/definition.rb +0 -477
  79. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  80. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  81. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  82. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  83. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  84. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  85. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  86. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  87. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  88. data/lib/swarm_sdk/configuration.rb +0 -135
  89. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  90. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  91. data/lib/swarm_sdk/context_compactor.rb +0 -335
  92. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  93. data/lib/swarm_sdk/context_management/context.rb +0 -328
  94. data/lib/swarm_sdk/defaults.rb +0 -196
  95. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  96. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  97. data/lib/swarm_sdk/hooks/context.rb +0 -197
  98. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  99. data/lib/swarm_sdk/hooks/error.rb +0 -29
  100. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  101. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  102. data/lib/swarm_sdk/hooks/result.rb +0 -150
  103. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  104. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  105. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  106. data/lib/swarm_sdk/log_collector.rb +0 -227
  107. data/lib/swarm_sdk/log_stream.rb +0 -127
  108. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  109. data/lib/swarm_sdk/model_aliases.json +0 -8
  110. data/lib/swarm_sdk/models.json +0 -1
  111. data/lib/swarm_sdk/models.rb +0 -120
  112. data/lib/swarm_sdk/node_context.rb +0 -245
  113. data/lib/swarm_sdk/observer/builder.rb +0 -81
  114. data/lib/swarm_sdk/observer/config.rb +0 -45
  115. data/lib/swarm_sdk/observer/manager.rb +0 -236
  116. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  117. data/lib/swarm_sdk/permissions/config.rb +0 -239
  118. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  119. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  120. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  121. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  122. data/lib/swarm_sdk/plugin.rb +0 -309
  123. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  124. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  125. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  126. data/lib/swarm_sdk/restore_result.rb +0 -65
  127. data/lib/swarm_sdk/result.rb +0 -123
  128. data/lib/swarm_sdk/snapshot.rb +0 -156
  129. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  130. data/lib/swarm_sdk/state_restorer.rb +0 -476
  131. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  132. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  133. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  134. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  135. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  136. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  141. data/lib/swarm_sdk/swarm.rb +0 -717
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/bash.rb +0 -282
  145. data/lib/swarm_sdk/tools/clock.rb +0 -44
  146. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  147. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  148. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  149. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  150. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  151. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  152. data/lib/swarm_sdk/tools/edit.rb +0 -145
  153. data/lib/swarm_sdk/tools/glob.rb +0 -166
  154. data/lib/swarm_sdk/tools/grep.rb +0 -235
  155. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  156. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  157. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  158. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  159. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  160. data/lib/swarm_sdk/tools/read.rb +0 -261
  161. data/lib/swarm_sdk/tools/registry.rb +0 -205
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  165. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  166. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  167. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  168. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  169. data/lib/swarm_sdk/tools/think.rb +0 -98
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/utils.rb +0 -68
  174. data/lib/swarm_sdk/validation_result.rb +0 -33
  175. data/lib/swarm_sdk/version.rb +0 -5
  176. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  177. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  178. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  179. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  180. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  181. data/lib/swarm_sdk/workflow.rb +0 -554
  182. data/lib/swarm_sdk.rb +0 -524
@@ -1,24 +0,0 @@
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
@@ -1,24 +0,0 @@
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
@@ -1,63 +0,0 @@
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
- optional(:thinking_budget).filled(:string).description("Thinking budget: \"think\" < \"think hard\" < \"think harder\" < \"ultrathink\". Each level increases Claude's thinking allocation. Auto-select based on task complexity.")
16
- end
17
-
18
- def call(prompt:, new_session: false, system_prompt: nil, description: nil, thinking_budget: nil)
19
- executor = ClaudeMcpServer.executor
20
- instance_config = ClaudeMcpServer.instance_config
21
-
22
- # Prepend thinking budget to prompt if provided
23
- final_prompt = if thinking_budget
24
- "#{thinking_budget}: #{prompt}"
25
- else
26
- prompt
27
- end
28
-
29
- options = {
30
- new_session: new_session,
31
- system_prompt: system_prompt || instance_config[:prompt],
32
- description: description,
33
- }
34
-
35
- # Add allowed tools from instance config
36
- options[:allowed_tools] = instance_config[:allowed_tools] if instance_config[:allowed_tools]&.any?
37
-
38
- # Add disallowed tools from instance config
39
- options[:disallowed_tools] = instance_config[:disallowed_tools] if instance_config[:disallowed_tools]&.any?
40
-
41
- # Add connections from instance config
42
- options[:connections] = instance_config[:connections] if instance_config[:connections]&.any?
43
-
44
- response = executor.execute(final_prompt, options)
45
-
46
- # Validate the response has a result
47
- unless response.is_a?(Hash) && response.key?("result")
48
- raise "Invalid response from executor: missing 'result' field. Response structure: #{response.keys.join(", ")}"
49
- end
50
-
51
- result = response["result"]
52
-
53
- # Validate the result is not empty
54
- if result.nil? || (result.is_a?(String) && result.strip.empty?)
55
- raise "Agent #{instance_config[:name]} returned an empty response. The task was executed but no content was provided."
56
- end
57
-
58
- # Return just the result text as expected by MCP
59
- result
60
- end
61
- end
62
- end
63
- end
@@ -1,5 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ClaudeSwarm
4
- VERSION = "1.0.10"
5
- end
@@ -1,475 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ClaudeSwarm
4
- class WorktreeManager
5
- include SystemUtils
6
-
7
- attr_reader :shared_worktree_name, :created_worktrees
8
-
9
- def initialize(cli_worktree_option = nil, session_id: nil)
10
- @cli_worktree_option = cli_worktree_option
11
- @session_id = session_id
12
- # Generate a name based on session ID if no option given, empty string, or default "worktree" from Thor
13
- @shared_worktree_name = if cli_worktree_option.nil? || cli_worktree_option.empty? || cli_worktree_option == "worktree"
14
- generate_worktree_name
15
- else
16
- cli_worktree_option
17
- end
18
- @created_worktrees = {} # Maps "repo_root:worktree_name" to worktree_path
19
- @instance_worktree_configs = {} # Stores per-instance worktree settings
20
- @instance_directories = {} # Stores original resolved directories for each instance
21
- @instance_worktree_paths = {} # Stores worktree paths for each instance
22
- end
23
-
24
- def setup_worktrees(instances)
25
- # First pass: determine worktree configuration for each instance
26
- instances.each do |instance|
27
- worktree_config = determine_worktree_config(instance)
28
- @instance_worktree_configs[instance[:name]] = worktree_config
29
- end
30
-
31
- # Second pass: create necessary worktrees
32
- worktrees_to_create = collect_worktrees_to_create(instances)
33
- worktrees_to_create.each do |repo_root, worktree_name|
34
- create_worktree(repo_root, worktree_name)
35
- end
36
-
37
- # Third pass: map instance directories to worktree paths
38
- instances.each do |instance|
39
- worktree_config = @instance_worktree_configs[instance[:name]]
40
-
41
- if ENV["CLAUDE_SWARM_DEBUG"]
42
- puts "Debug [WorktreeManager]: Processing instance #{instance[:name]}"
43
- puts "Debug [WorktreeManager]: Worktree config: #{worktree_config.inspect}"
44
- end
45
-
46
- # Store original directories (resolved)
47
- original_dirs = instance[:directories] || [instance[:directory]]
48
- resolved_dirs = original_dirs.map { |dir| dir ? File.expand_path(dir) : nil }.compact
49
- @instance_directories[instance[:name]] = resolved_dirs
50
-
51
- if worktree_config[:skip]
52
- # No worktree, paths remain the same
53
- @instance_worktree_paths[instance[:name]] = resolved_dirs
54
- next
55
- end
56
-
57
- worktree_name = worktree_config[:name]
58
- mapped_dirs = original_dirs.map { |dir| map_to_worktree_path(dir, worktree_name) }
59
-
60
- # Store the worktree paths
61
- @instance_worktree_paths[instance[:name]] = mapped_dirs
62
-
63
- if ENV["CLAUDE_SWARM_DEBUG"]
64
- puts "Debug [WorktreeManager]: Original dirs: #{original_dirs.inspect}"
65
- puts "Debug [WorktreeManager]: Mapped dirs: #{mapped_dirs.inspect}"
66
- end
67
-
68
- if instance[:directories]
69
- instance[:directories] = mapped_dirs
70
- # Also update the single directory field for backward compatibility
71
- instance[:directory] = mapped_dirs.first
72
- else
73
- instance[:directory] = mapped_dirs.first
74
- end
75
-
76
- puts "Debug [WorktreeManager]: Updated instance[:directory] to: #{instance[:directory]}" if ENV["CLAUDE_SWARM_DEBUG"]
77
- end
78
- end
79
-
80
- def map_to_worktree_path(original_path, worktree_name)
81
- return original_path unless original_path
82
-
83
- expanded_path = File.expand_path(original_path)
84
- repo_root = find_git_root(expanded_path)
85
-
86
- if ENV["CLAUDE_SWARM_DEBUG"]
87
- puts "Debug [map_to_worktree_path]: Original path: #{original_path}"
88
- puts "Debug [map_to_worktree_path]: Expanded path: #{expanded_path}"
89
- puts "Debug [map_to_worktree_path]: Repo root: #{repo_root}"
90
- end
91
-
92
- return original_path unless repo_root
93
-
94
- # Check if we have a worktree for this repo and name
95
- worktree_key = "#{repo_root}:#{worktree_name}"
96
- worktree_path = @created_worktrees[worktree_key]
97
-
98
- if ENV["CLAUDE_SWARM_DEBUG"]
99
- puts "Debug [map_to_worktree_path]: Worktree key: #{worktree_key}"
100
- puts "Debug [map_to_worktree_path]: Worktree path: #{worktree_path}"
101
- puts "Debug [map_to_worktree_path]: Created worktrees: #{@created_worktrees.inspect}"
102
- end
103
-
104
- return original_path unless worktree_path
105
-
106
- # Calculate relative path from repo root
107
- relative_path = Pathname.new(expanded_path).relative_path_from(Pathname.new(repo_root)).to_s
108
-
109
- # Return the equivalent path in the worktree
110
- result = if relative_path == "."
111
- worktree_path
112
- else
113
- File.join(worktree_path, relative_path)
114
- end
115
-
116
- puts "Debug [map_to_worktree_path]: Result: #{result}" if ENV["CLAUDE_SWARM_DEBUG"]
117
-
118
- result
119
- end
120
-
121
- def cleanup_worktrees
122
- @created_worktrees.each do |worktree_key, worktree_path|
123
- repo_root = worktree_key.split(":", 2).first
124
- next unless File.exist?(worktree_path)
125
-
126
- # Check for uncommitted changes
127
- if has_uncommitted_changes?(worktree_path)
128
- puts "⚠️ Warning: Worktree has uncommitted changes, skipping cleanup: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
129
- next
130
- end
131
-
132
- # Check for unpushed commits
133
- has_unpushed = has_unpushed_commits?(worktree_path)
134
-
135
- if has_unpushed
136
- puts "⚠️ Warning: Worktree has unpushed commits, skipping cleanup: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
137
- next
138
- end
139
-
140
- puts "Removing worktree: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
141
-
142
- # Remove the worktree
143
- output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "remove", worktree_path)
144
- next if status.success?
145
-
146
- puts "Warning: Failed to remove worktree: #{output}"
147
- # Try force remove
148
- output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "remove", "--force", worktree_path)
149
- puts "Force remove result: #{output}" unless status.success?
150
- end
151
-
152
- # Clean up external worktree directories
153
- cleanup_external_directories
154
- rescue StandardError => e
155
- puts "Error during worktree cleanup: #{e.message}"
156
- end
157
-
158
- def cleanup_external_directories
159
- # Remove session-specific worktree directory if it exists and is empty
160
- return unless @session_id
161
-
162
- session_worktree_dir = ClaudeSwarm.joined_worktrees_dir(@session_id)
163
- return unless File.exist?(session_worktree_dir)
164
-
165
- # Try to remove the directory tree
166
- begin
167
- # Remove all empty directories recursively
168
- Dir.glob(File.join(session_worktree_dir, "**/*"), File::FNM_DOTMATCH).reverse_each do |path|
169
- next if path.end_with?("/.", "/..")
170
-
171
- FileUtils.rmdir(path) if File.directory?(path) && Dir.empty?(path)
172
- end
173
- # Finally try to remove the session directory itself
174
- FileUtils.rmdir(session_worktree_dir) if Dir.empty?(session_worktree_dir)
175
- rescue Errno::ENOTEMPTY
176
- puts "Note: Session worktree directory not empty, leaving in place: #{session_worktree_dir}" unless ENV["CLAUDE_SWARM_PROMPT"]
177
- rescue StandardError => e
178
- puts "Warning: Error cleaning up worktree directories: #{e.message}" unless ENV["CLAUDE_SWARM_PROMPT"]
179
- end
180
- end
181
-
182
- def session_metadata
183
- # Build instance details with resolved paths and worktree mappings
184
- instance_details = {}
185
-
186
- @instance_worktree_configs.each do |instance_name, worktree_config|
187
- instance_details[instance_name] = {
188
- worktree_config: worktree_config,
189
- directories: @instance_directories[instance_name] || {},
190
- worktree_paths: @instance_worktree_paths[instance_name] || {},
191
- }
192
- end
193
-
194
- {
195
- enabled: true,
196
- shared_name: @shared_worktree_name,
197
- created_paths: @created_worktrees.dup,
198
- instance_configs: instance_details,
199
- }
200
- end
201
-
202
- # Deprecated method for backward compatibility
203
- def worktree_name
204
- @shared_worktree_name
205
- end
206
-
207
- private
208
-
209
- def external_worktree_path(repo_root, worktree_name)
210
- # Get repository name from path
211
- repo_name = sanitize_repo_name(File.basename(repo_root))
212
-
213
- # Add a short hash of the full path to handle multiple repos with same name
214
- path_hash = Digest::SHA256.hexdigest(repo_root)[0..7]
215
- unique_repo_name = "#{repo_name}-#{path_hash}"
216
-
217
- # Build external path: ~/.claude-swarm/worktrees/[session_id]/[repo_name-hash]/[worktree_name]
218
- base_dir = ClaudeSwarm.joined_worktrees_dir
219
-
220
- # Validate base directory is accessible
221
- begin
222
- FileUtils.mkdir_p(base_dir)
223
- rescue Errno::EACCES
224
- # Fall back to temp directory if home is not writable
225
- base_dir = File.join(Dir.tmpdir, ".claude-swarm", "worktrees")
226
- FileUtils.mkdir_p(base_dir)
227
- end
228
-
229
- session_dir = @session_id || "default"
230
- File.join(base_dir, session_dir, unique_repo_name, worktree_name)
231
- end
232
-
233
- def sanitize_repo_name(name)
234
- # Replace problematic characters with underscores
235
- name.gsub(/[^a-zA-Z0-9._-]/, "_")
236
- end
237
-
238
- def generate_worktree_name
239
- # Use session ID if available, otherwise generate a random suffix
240
- if @session_id
241
- "worktree-#{@session_id}"
242
- else
243
- # Fallback to random suffix for tests or when session ID is not available
244
- random_suffix = SecureRandom.alphanumeric(5).downcase
245
- "worktree-#{random_suffix}"
246
- end
247
- end
248
-
249
- def determine_worktree_config(instance)
250
- # Check instance-level worktree setting
251
- instance_worktree = instance[:worktree]
252
-
253
- if instance_worktree.nil?
254
- # No instance-level setting, follow CLI behavior
255
- if @cli_worktree_option.nil?
256
- { skip: true }
257
- else
258
- { skip: false, name: @shared_worktree_name }
259
- end
260
- elsif instance_worktree == false
261
- # Explicitly disabled for this instance
262
- { skip: true }
263
- elsif instance_worktree == true
264
- # Use shared worktree (either from CLI or auto-generated)
265
- { skip: false, name: @shared_worktree_name }
266
- elsif instance_worktree.is_a?(String)
267
- # Use custom worktree name
268
- { skip: false, name: instance_worktree }
269
- else
270
- raise Error, "Invalid worktree configuration for instance '#{instance[:name]}': #{instance_worktree.inspect}"
271
- end
272
- end
273
-
274
- def collect_worktrees_to_create(instances)
275
- worktrees_needed = {}
276
-
277
- instances.each do |instance|
278
- worktree_config = @instance_worktree_configs[instance[:name]]
279
- next if worktree_config[:skip]
280
-
281
- worktree_name = worktree_config[:name]
282
- directories = instance[:directories] || [instance[:directory]]
283
-
284
- directories.each do |dir|
285
- next unless dir
286
-
287
- expanded_dir = File.expand_path(dir)
288
- repo_root = find_git_root(expanded_dir)
289
- next unless repo_root
290
-
291
- # Track unique repo_root:worktree_name combinations
292
- worktrees_needed[repo_root] ||= Set.new
293
- worktrees_needed[repo_root].add(worktree_name)
294
- end
295
- end
296
-
297
- # Convert to array of [repo_root, worktree_name] pairs
298
- result = []
299
- worktrees_needed.each do |repo_root, worktree_names|
300
- worktree_names.each do |worktree_name|
301
- result << [repo_root, worktree_name]
302
- end
303
- end
304
- result
305
- end
306
-
307
- def find_git_root(path)
308
- current = File.expand_path(path)
309
-
310
- while current != "/"
311
- return current if File.exist?(File.join(current, ".git"))
312
-
313
- current = File.dirname(current)
314
- end
315
-
316
- nil
317
- end
318
-
319
- def create_worktree(repo_root, worktree_name)
320
- worktree_key = "#{repo_root}:#{worktree_name}"
321
- # Create worktrees in external directory
322
- worktree_path = external_worktree_path(repo_root, worktree_name)
323
-
324
- # Check if worktree already exists
325
- if File.exist?(worktree_path)
326
- puts "Using existing worktree: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
327
- @created_worktrees[worktree_key] = worktree_path
328
- return
329
- end
330
-
331
- # Ensure parent directory exists with proper error handling
332
- begin
333
- FileUtils.mkdir_p(File.dirname(worktree_path))
334
- rescue Errno::EACCES => e
335
- raise Error, "Permission denied creating worktree directory: #{e.message}"
336
- rescue Errno::ENOSPC => e
337
- raise Error, "Not enough disk space for worktree: #{e.message}"
338
- rescue StandardError => e
339
- raise Error, "Failed to create worktree directory: #{e.message}"
340
- end
341
-
342
- # Get current branch
343
- output, status = Open3.capture2e("git", "-C", repo_root, "rev-parse", "--abbrev-ref", "HEAD")
344
- raise Error, "Failed to get current branch in #{repo_root}: #{output}" unless status.success?
345
-
346
- current_branch = output.strip
347
-
348
- # Create worktree with a new branch based on current branch
349
- branch_name = worktree_name
350
- puts "Creating worktree: #{worktree_path} with branch: #{branch_name}" unless ENV["CLAUDE_SWARM_PROMPT"]
351
-
352
- # Create worktree with a new branch
353
- output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "add", "-b", branch_name, worktree_path, current_branch)
354
-
355
- # If branch already exists, try without -b flag
356
- if !status.success? && output.include?("already exists")
357
- puts "Branch #{branch_name} already exists, using existing branch" unless ENV["CLAUDE_SWARM_PROMPT"]
358
- output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "add", worktree_path, branch_name)
359
- end
360
-
361
- # If worktree path is already in use, it might be from a previous run
362
- if !status.success? && output.include?("is already used by worktree")
363
- puts "Worktree path already in use, checking if it's valid" unless ENV["CLAUDE_SWARM_PROMPT"]
364
- # Check if the worktree actually exists at that path
365
- if File.exist?(File.join(worktree_path, ".git"))
366
- puts "Using existing worktree: #{worktree_path}" unless ENV["CLAUDE_SWARM_PROMPT"]
367
- @created_worktrees[worktree_key] = worktree_path
368
- return
369
- else
370
- # The worktree is registered but the directory doesn't exist, prune and retry
371
- puts "Pruning stale worktree references" unless ENV["CLAUDE_SWARM_PROMPT"]
372
- begin
373
- system!("git", "-C", repo_root, "worktree", "prune")
374
- rescue Error
375
- # Ignore errors when pruning
376
- end
377
- output, status = Open3.capture2e("git", "-C", repo_root, "worktree", "add", worktree_path, branch_name)
378
- end
379
- end
380
-
381
- raise Error, "Failed to create worktree: #{output}" unless status.success?
382
-
383
- @created_worktrees[worktree_key] = worktree_path
384
- end
385
-
386
- def has_uncommitted_changes?(worktree_path)
387
- # Check if there are any uncommitted changes (staged or unstaged)
388
- output, status = Open3.capture2e("git", "-C", worktree_path, "status", "--porcelain")
389
- return false unless status.success?
390
-
391
- # If output is not empty, there are changes
392
- !output.strip.empty?
393
- end
394
-
395
- def has_unpushed_commits?(worktree_path)
396
- # Get the current branch
397
- branch_output, branch_status = Open3.capture2e("git", "-C", worktree_path, "rev-parse", "--abbrev-ref", "HEAD")
398
- return false unless branch_status.success?
399
-
400
- current_branch = branch_output.strip
401
-
402
- # Check if the branch has an upstream
403
- _, upstream_status = Open3.capture2e("git", "-C", worktree_path, "rev-parse", "--abbrev-ref", "#{current_branch}@{upstream}")
404
-
405
- # If branch has upstream, check against it
406
- if upstream_status.success?
407
- # Check for unpushed commits against upstream
408
- unpushed_output, unpushed_status = Open3.capture2e("git", "-C", worktree_path, "rev-list", "HEAD", "^#{current_branch}@{upstream}")
409
- return false unless unpushed_status.success?
410
-
411
- # If output is not empty, there are unpushed commits
412
- return !unpushed_output.strip.empty?
413
- end
414
-
415
- # No upstream - this is likely a new branch created by the worktree
416
- # The key insight: when git worktree add -b creates a branch, it creates it in BOTH
417
- # the worktree AND the main repository. So we need to check the reflog in the worktree
418
- # to see if any NEW commits were made after the worktree was created.
419
-
420
- # Use reflog to check if any commits were made in this worktree
421
- reflog_output, reflog_status = Open3.capture2e("git", "-C", worktree_path, "reflog", "--format=%H %gs", current_branch)
422
-
423
- if reflog_status.success? && !reflog_output.strip.empty?
424
- reflog_lines = reflog_output.strip.split("\n")
425
-
426
- # Look for commit entries (not branch creation)
427
- commit_entries = reflog_lines.select { |line| line.include?(" commit:") || line.include?(" commit (amend):") }
428
-
429
- # If there are commit entries in the reflog, there are unpushed commits
430
- return !commit_entries.empty?
431
- end
432
-
433
- # As a fallback, assume no unpushed commits
434
- false
435
- end
436
-
437
- def find_original_repo_for_worktree(worktree_path)
438
- # Get the git directory for this worktree
439
- _, git_dir_status = Open3.capture2e("git", "-C", worktree_path, "rev-parse", "--git-dir")
440
- return unless git_dir_status.success?
441
-
442
- # Read the gitdir file to find the main repository
443
- # Worktree .git files contain: gitdir: /path/to/main/repo/.git/worktrees/worktree-name
444
- if File.file?(File.join(worktree_path, ".git"))
445
- gitdir_content = File.read(File.join(worktree_path, ".git")).strip
446
- if gitdir_content =~ /^gitdir: (.+)$/
447
- git_path = ::Regexp.last_match(1)
448
- # Extract the main repo path from the worktree git path
449
- # Format: /path/to/repo/.git/worktrees/worktree-name
450
- return ::Regexp.last_match(1) if git_path =~ %r{^(.+)/\.git/worktrees/[^/]+$}
451
- end
452
- end
453
-
454
- nil
455
- end
456
-
457
- def find_base_branch(repo_path)
458
- # Try to find the base branch - check for main, master, or the default branch
459
- ["main", "master"].each do |branch|
460
- _, status = Open3.capture2e("git", "-C", repo_path, "rev-parse", "--verify", "refs/heads/#{branch}")
461
- return branch if status.success?
462
- end
463
-
464
- # Try to get the default branch from HEAD
465
- output, status = Open3.capture2e("git", "-C", repo_path, "symbolic-ref", "refs/remotes/origin/HEAD")
466
- if status.success?
467
- # Extract branch name from refs/remotes/origin/main
468
- branch_match = output.strip.match(%r{refs/remotes/origin/(.+)$})
469
- return branch_match[1] if branch_match
470
- end
471
-
472
- nil
473
- end
474
- end
475
- end
@@ -1,22 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ClaudeSwarm
4
- # Provides consistent YAML loading across the application
5
- module YamlLoader
6
- class << self
7
- # Load a YAML configuration file (enables aliases for configuration flexibility)
8
- # @param file_path [String] Path to the configuration file
9
- # @return [Hash] The loaded configuration
10
- # @raise [ClaudeSwarm::Error] Re-raises with a more descriptive error message
11
- def load_config_file(file_path)
12
- YAML.load_file(file_path, aliases: true)
13
- rescue Errno::ENOENT
14
- raise ClaudeSwarm::Error, "Configuration file not found: #{file_path}"
15
- rescue Psych::SyntaxError => e
16
- raise ClaudeSwarm::Error, "Invalid YAML syntax in #{file_path}: #{e.message}"
17
- rescue Psych::BadAlias => e
18
- raise ClaudeSwarm::Error, "Invalid YAML alias in #{file_path}: #{e.message}"
19
- end
20
- end
21
- end
22
- end
data/lib/claude_swarm.rb DELETED
@@ -1,67 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Standard library dependencies
4
- require "bundler"
5
- require "digest"
6
- require "English"
7
- require "erb"
8
- require "fileutils"
9
- require "io/console"
10
- require "json"
11
- require "logger"
12
- require "open3"
13
- require "pathname"
14
- require "pty"
15
- require "securerandom"
16
- require "set"
17
- require "shellwords"
18
- require "time"
19
- require "timeout"
20
- require "tmpdir"
21
- require "yaml"
22
-
23
- # External dependencies
24
- require "claude_sdk"
25
- require "fast_mcp"
26
- require "mcp_client"
27
- require "thor"
28
-
29
- require_relative "claude_swarm/version"
30
- # Zeitwerk setup
31
- require "zeitwerk"
32
- loader = Zeitwerk::Loader.new
33
- loader.tag = File.basename(__FILE__, ".rb")
34
- loader.ignore("#{__dir__}/claude_swarm/templates")
35
- loader.push_dir("#{__dir__}/claude_swarm", namespace: ClaudeSwarm)
36
- loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
37
- loader.inflector.inflect(
38
- "cli" => "CLI",
39
- "openai" => "OpenAI",
40
- )
41
- loader.setup
42
-
43
- module ClaudeSwarm
44
- class Error < StandardError; end
45
-
46
- class << self
47
- def home_dir
48
- ENV.fetch("CLAUDE_SWARM_HOME") { File.expand_path("~/.claude-swarm") }
49
- end
50
-
51
- def joined_home_dir(*strings)
52
- File.join(home_dir, *strings)
53
- end
54
-
55
- def joined_run_dir(*strings)
56
- joined_home_dir("run", *strings)
57
- end
58
-
59
- def joined_sessions_dir(*strings)
60
- joined_home_dir("sessions", *strings)
61
- end
62
-
63
- def joined_worktrees_dir(*strings)
64
- joined_home_dir("worktrees", *strings)
65
- end
66
- end
67
- end