swarm_memory 2.1.4 → 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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_memory/version.rb +1 -1
  3. data/lib/swarm_memory.rb +7 -2
  4. metadata +6 -185
  5. data/lib/claude_swarm/base_executor.rb +0 -133
  6. data/lib/claude_swarm/claude_code_executor.rb +0 -349
  7. data/lib/claude_swarm/claude_mcp_server.rb +0 -78
  8. data/lib/claude_swarm/cli.rb +0 -697
  9. data/lib/claude_swarm/commands/ps.rb +0 -215
  10. data/lib/claude_swarm/commands/show.rb +0 -139
  11. data/lib/claude_swarm/configuration.rb +0 -373
  12. data/lib/claude_swarm/hooks/session_start_hook.rb +0 -42
  13. data/lib/claude_swarm/json_handler.rb +0 -91
  14. data/lib/claude_swarm/mcp_generator.rb +0 -243
  15. data/lib/claude_swarm/openai/chat_completion.rb +0 -256
  16. data/lib/claude_swarm/openai/executor.rb +0 -256
  17. data/lib/claude_swarm/openai/responses.rb +0 -319
  18. data/lib/claude_swarm/orchestrator.rb +0 -878
  19. data/lib/claude_swarm/process_tracker.rb +0 -78
  20. data/lib/claude_swarm/session_cost_calculator.rb +0 -209
  21. data/lib/claude_swarm/session_path.rb +0 -42
  22. data/lib/claude_swarm/settings_generator.rb +0 -77
  23. data/lib/claude_swarm/system_utils.rb +0 -46
  24. data/lib/claude_swarm/templates/generation_prompt.md.erb +0 -230
  25. data/lib/claude_swarm/tools/reset_session_tool.rb +0 -24
  26. data/lib/claude_swarm/tools/session_info_tool.rb +0 -24
  27. data/lib/claude_swarm/tools/task_tool.rb +0 -63
  28. data/lib/claude_swarm/version.rb +0 -5
  29. data/lib/claude_swarm/worktree_manager.rb +0 -475
  30. data/lib/claude_swarm/yaml_loader.rb +0 -22
  31. data/lib/claude_swarm.rb +0 -67
  32. data/lib/swarm_cli/cli.rb +0 -201
  33. data/lib/swarm_cli/command_registry.rb +0 -61
  34. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  35. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  36. data/lib/swarm_cli/commands/migrate.rb +0 -55
  37. data/lib/swarm_cli/commands/run.rb +0 -173
  38. data/lib/swarm_cli/config_loader.rb +0 -98
  39. data/lib/swarm_cli/formatters/human_formatter.rb +0 -781
  40. data/lib/swarm_cli/formatters/json_formatter.rb +0 -51
  41. data/lib/swarm_cli/interactive_repl.rb +0 -924
  42. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  43. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  44. data/lib/swarm_cli/migrate_options.rb +0 -54
  45. data/lib/swarm_cli/migrator.rb +0 -132
  46. data/lib/swarm_cli/options.rb +0 -151
  47. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  48. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  49. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  50. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  51. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  52. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  53. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  54. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  55. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  56. data/lib/swarm_cli/ui/icons.rb +0 -36
  57. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  58. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  59. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  60. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  61. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  62. data/lib/swarm_cli/version.rb +0 -5
  63. data/lib/swarm_cli.rb +0 -46
  64. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -127
  65. data/lib/swarm_sdk/agent/builder.rb +0 -552
  66. data/lib/swarm_sdk/agent/chat.rb +0 -774
  67. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -268
  68. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  69. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  70. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -78
  71. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -233
  72. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  73. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  74. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -136
  75. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  76. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -98
  77. data/lib/swarm_sdk/agent/context.rb +0 -116
  78. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  79. data/lib/swarm_sdk/agent/definition.rb +0 -477
  80. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -182
  81. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  82. data/lib/swarm_sdk/builders/base_builder.rb +0 -409
  83. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  84. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  85. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  86. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  87. data/lib/swarm_sdk/configuration/parser.rb +0 -353
  88. data/lib/swarm_sdk/configuration/translator.rb +0 -255
  89. data/lib/swarm_sdk/configuration.rb +0 -135
  90. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  91. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -106
  92. data/lib/swarm_sdk/context_compactor.rb +0 -335
  93. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  94. data/lib/swarm_sdk/context_management/context.rb +0 -328
  95. data/lib/swarm_sdk/defaults.rb +0 -196
  96. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  97. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  98. data/lib/swarm_sdk/hooks/context.rb +0 -197
  99. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  100. data/lib/swarm_sdk/hooks/error.rb +0 -29
  101. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  102. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  103. data/lib/swarm_sdk/hooks/result.rb +0 -150
  104. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -255
  105. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  106. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  107. data/lib/swarm_sdk/log_collector.rb +0 -227
  108. data/lib/swarm_sdk/log_stream.rb +0 -127
  109. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  110. data/lib/swarm_sdk/model_aliases.json +0 -8
  111. data/lib/swarm_sdk/models.json +0 -1
  112. data/lib/swarm_sdk/models.rb +0 -120
  113. data/lib/swarm_sdk/node_context.rb +0 -245
  114. data/lib/swarm_sdk/observer/builder.rb +0 -81
  115. data/lib/swarm_sdk/observer/config.rb +0 -45
  116. data/lib/swarm_sdk/observer/manager.rb +0 -236
  117. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  118. data/lib/swarm_sdk/permissions/config.rb +0 -239
  119. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  120. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  121. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  122. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  123. data/lib/swarm_sdk/plugin.rb +0 -309
  124. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  125. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  126. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  127. data/lib/swarm_sdk/restore_result.rb +0 -65
  128. data/lib/swarm_sdk/result.rb +0 -123
  129. data/lib/swarm_sdk/snapshot.rb +0 -156
  130. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  131. data/lib/swarm_sdk/state_restorer.rb +0 -476
  132. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  133. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -683
  134. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -167
  135. data/lib/swarm_sdk/swarm/builder.rb +0 -249
  136. data/lib/swarm_sdk/swarm/executor.rb +0 -213
  137. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -150
  138. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -340
  139. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -154
  140. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  141. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -358
  142. data/lib/swarm_sdk/swarm.rb +0 -717
  143. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  144. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  145. data/lib/swarm_sdk/tools/bash.rb +0 -282
  146. data/lib/swarm_sdk/tools/clock.rb +0 -44
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -267
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -163
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  160. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  161. data/lib/swarm_sdk/tools/read.rb +0 -261
  162. data/lib/swarm_sdk/tools/registry.rb +0 -205
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  166. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  167. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -272
  168. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  169. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  170. data/lib/swarm_sdk/tools/think.rb +0 -98
  171. data/lib/swarm_sdk/tools/todo_write.rb +0 -235
  172. data/lib/swarm_sdk/tools/web_fetch.rb +0 -262
  173. data/lib/swarm_sdk/tools/write.rb +0 -112
  174. data/lib/swarm_sdk/utils.rb +0 -68
  175. data/lib/swarm_sdk/validation_result.rb +0 -33
  176. data/lib/swarm_sdk/version.rb +0 -5
  177. data/lib/swarm_sdk/workflow/agent_config.rb +0 -79
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -143
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -555
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -249
  182. data/lib/swarm_sdk/workflow.rb +0 -554
  183. data/lib/swarm_sdk.rb +0 -524
  184. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -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.9"
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