swarm_memory 2.1.1 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/claude_swarm/cli.rb +9 -11
- data/lib/claude_swarm/commands/ps.rb +1 -2
- data/lib/claude_swarm/configuration.rb +30 -7
- data/lib/claude_swarm/mcp_generator.rb +4 -10
- data/lib/claude_swarm/orchestrator.rb +43 -44
- data/lib/claude_swarm/system_utils.rb +4 -4
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/claude_swarm.rb +5 -9
- data/lib/swarm_cli/commands/mcp_serve.rb +2 -2
- data/lib/swarm_cli/commands/mcp_tools.rb +3 -3
- data/lib/swarm_cli/config_loader.rb +14 -13
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_cli.rb +2 -0
- data/lib/swarm_memory/adapters/base.rb +4 -4
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +0 -12
- data/lib/swarm_memory/core/storage.rb +66 -6
- data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
- data/lib/swarm_memory/integration/cli_registration.rb +3 -2
- data/lib/swarm_memory/integration/sdk_plugin.rb +24 -4
- data/lib/swarm_memory/optimization/defragmenter.rb +4 -0
- data/lib/swarm_memory/tools/memory_edit.rb +3 -2
- data/lib/swarm_memory/tools/memory_glob.rb +24 -1
- data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
- data/lib/swarm_memory/tools/memory_read.rb +3 -3
- data/lib/swarm_memory/tools/memory_write.rb +2 -2
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +7 -0
- data/lib/swarm_sdk/agent/builder.rb +33 -0
- data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
- data/lib/swarm_sdk/agent/chat/hook_integration.rb +41 -0
- data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
- data/lib/swarm_sdk/agent/chat.rb +199 -52
- data/lib/swarm_sdk/agent/context.rb +6 -2
- data/lib/swarm_sdk/agent/context_manager.rb +6 -0
- data/lib/swarm_sdk/agent/definition.rb +32 -23
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
- data/lib/swarm_sdk/configuration.rb +420 -103
- data/lib/swarm_sdk/events_to_messages.rb +181 -0
- data/lib/swarm_sdk/log_collector.rb +31 -5
- data/lib/swarm_sdk/log_stream.rb +37 -8
- data/lib/swarm_sdk/model_aliases.json +4 -1
- data/lib/swarm_sdk/node/agent_config.rb +39 -9
- data/lib/swarm_sdk/node/builder.rb +158 -42
- data/lib/swarm_sdk/node_context.rb +75 -0
- data/lib/swarm_sdk/node_orchestrator.rb +492 -18
- data/lib/swarm_sdk/plugin.rb +73 -1
- data/lib/swarm_sdk/proc_helpers.rb +53 -0
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
- data/lib/swarm_sdk/providers/openai_with_responses.rb +22 -15
- data/lib/swarm_sdk/restore_result.rb +65 -0
- data/lib/swarm_sdk/result.rb +32 -6
- data/lib/swarm_sdk/snapshot.rb +156 -0
- data/lib/swarm_sdk/snapshot_from_events.rb +386 -0
- data/lib/swarm_sdk/state_restorer.rb +491 -0
- data/lib/swarm_sdk/state_snapshot.rb +369 -0
- data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
- data/lib/swarm_sdk/swarm/builder.rb +208 -11
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
- data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
- data/lib/swarm_sdk/swarm.rb +367 -90
- data/lib/swarm_sdk/swarm_loader.rb +145 -0
- data/lib/swarm_sdk/swarm_registry.rb +136 -0
- data/lib/swarm_sdk/tools/delegate.rb +94 -9
- data/lib/swarm_sdk/tools/read.rb +17 -5
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
- data/lib/swarm_sdk/tools/stores/storage.rb +4 -4
- data/lib/swarm_sdk/tools/think.rb +4 -1
- data/lib/swarm_sdk/tools/todo_write.rb +20 -8
- data/lib/swarm_sdk/utils.rb +18 -0
- data/lib/swarm_sdk/validation_result.rb +33 -0
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk.rb +365 -28
- metadata +17 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8422c40d79fb0c2adcd9e1de9a4cfdf289ced49ef27507969ea797c4187a0ee1
|
|
4
|
+
data.tar.gz: 875f1429c37f2485d32641a7d96c9cae5db226a5bc2fc3e2f8c1443946e021b9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 298c0506a2f486aab1ad29c9f6e4605fe3fbb91fb17f7e3ee913b046f97a9d347312afd686e4bf26d28d6329c8974d421d39ee488e04bf2bc4cde22fea8cdc8c
|
|
7
|
+
data.tar.gz: c472ec8754b18a2a8d0ab1ae1a0dc391b2f4fffc2de670831272938d81e51a6716c81343b174afa8833d256e28a728fdd198e0bb2b6d92e13adc085c786d2abc
|
data/lib/claude_swarm/cli.rb
CHANGED
|
@@ -43,9 +43,8 @@ module ClaudeSwarm
|
|
|
43
43
|
type: :string,
|
|
44
44
|
desc: "Root directory for resolving relative paths (defaults to current directory)"
|
|
45
45
|
def start(config_file = nil)
|
|
46
|
-
#
|
|
47
|
-
root_dir = options[:root_dir] || Dir.pwd
|
|
48
|
-
ENV["CLAUDE_SWARM_ROOT_DIR"] = File.expand_path(root_dir)
|
|
46
|
+
# Determine root directory for this session
|
|
47
|
+
root_dir = File.expand_path(options[:root_dir] || Dir.pwd)
|
|
49
48
|
|
|
50
49
|
# Resolve config path relative to root directory
|
|
51
50
|
config_path = config_file || "claude-swarm.yml"
|
|
@@ -71,7 +70,7 @@ module ClaudeSwarm
|
|
|
71
70
|
end
|
|
72
71
|
|
|
73
72
|
begin
|
|
74
|
-
config = Configuration.new(config_path, base_dir:
|
|
73
|
+
config = Configuration.new(config_path, base_dir: root_dir, options: options)
|
|
75
74
|
generator = McpGenerator.new(config, vibe: options[:vibe])
|
|
76
75
|
orchestrator = Orchestrator.new(
|
|
77
76
|
config,
|
|
@@ -547,24 +546,23 @@ module ClaudeSwarm
|
|
|
547
546
|
exit(1)
|
|
548
547
|
end
|
|
549
548
|
|
|
550
|
-
#
|
|
549
|
+
# Load the original root directory from session
|
|
551
550
|
root_dir_file = File.join(session_path, "root_directory")
|
|
552
|
-
if File.exist?(root_dir_file)
|
|
551
|
+
root_dir = if File.exist?(root_dir_file)
|
|
553
552
|
original_dir = File.read(root_dir_file).strip
|
|
554
553
|
if Dir.exist?(original_dir)
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
say("Changed to original directory: #{original_dir}", :green) unless options[:prompt]
|
|
554
|
+
say("Using original directory: #{original_dir}", :green) unless options[:prompt]
|
|
555
|
+
original_dir
|
|
558
556
|
else
|
|
559
557
|
error("Original directory no longer exists: #{original_dir}")
|
|
560
558
|
exit(1)
|
|
561
559
|
end
|
|
562
560
|
else
|
|
563
561
|
# If no root_directory file, use current directory
|
|
564
|
-
|
|
562
|
+
Dir.pwd
|
|
565
563
|
end
|
|
566
564
|
|
|
567
|
-
config = Configuration.new(config_file, base_dir:
|
|
565
|
+
config = Configuration.new(config_file, base_dir: root_dir)
|
|
568
566
|
|
|
569
567
|
# Load session metadata if it exists to check for worktree info
|
|
570
568
|
session_metadata_file = File.join(session_path, "session_metadata.json")
|
|
@@ -96,9 +96,8 @@ module ClaudeSwarm
|
|
|
96
96
|
main_instance = config.dig("swarm", "main")
|
|
97
97
|
|
|
98
98
|
# Get base directory from session metadata or root_directory file
|
|
99
|
-
base_dir = ClaudeSwarm.root_dir
|
|
100
99
|
root_dir_file = File.join(session_dir, "root_directory")
|
|
101
|
-
base_dir = File.
|
|
100
|
+
base_dir = File.exist?(root_dir_file) ? File.read(root_dir_file).strip : Dir.pwd
|
|
102
101
|
|
|
103
102
|
# Get all directories - handle both string and array formats
|
|
104
103
|
dir_config = config.dig("swarm", "instances", main_instance, "directory")
|
|
@@ -13,13 +13,12 @@ module ClaudeSwarm
|
|
|
13
13
|
ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
|
|
14
14
|
O_SERIES_MODEL_PATTERN = /^(o\d+(\s+(Preview|preview))?(-pro|-mini|-deep-research|-mini-deep-research)?|gpt-5(-mini|-nano)?)$/
|
|
15
15
|
|
|
16
|
-
attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances, :
|
|
16
|
+
attr_reader :config, :config_path, :swarm, :swarm_name, :main_instance, :instances, :base_dir
|
|
17
17
|
|
|
18
18
|
def initialize(config_path, base_dir: nil, options: {})
|
|
19
19
|
@config_path = Pathname.new(config_path).expand_path
|
|
20
20
|
@config_dir = @config_path.dirname
|
|
21
|
-
@base_dir = base_dir || @config_dir
|
|
22
|
-
@root_directory = @base_dir
|
|
21
|
+
@base_dir = base_dir || @config_dir.to_s
|
|
23
22
|
@options = options
|
|
24
23
|
load_and_validate
|
|
25
24
|
end
|
|
@@ -70,19 +69,43 @@ module ClaudeSwarm
|
|
|
70
69
|
validate_directories unless has_before_commands?
|
|
71
70
|
end
|
|
72
71
|
|
|
73
|
-
def interpolate_env_vars!(obj)
|
|
72
|
+
def interpolate_env_vars!(obj, path = [])
|
|
74
73
|
case obj
|
|
75
74
|
when String
|
|
76
|
-
|
|
75
|
+
# Skip interpolation for any values inside MCP configurations
|
|
76
|
+
# Check if we're inside an mcps array element (path like: [..., "instances", <name>, "mcps", <index>, ...])
|
|
77
|
+
if in_mcp_config?(path)
|
|
78
|
+
obj
|
|
79
|
+
else
|
|
80
|
+
interpolate_env_string(obj)
|
|
81
|
+
end
|
|
77
82
|
when Hash
|
|
78
|
-
obj.
|
|
83
|
+
obj.each do |key, value|
|
|
84
|
+
obj[key] = interpolate_env_vars!(value, path + [key])
|
|
85
|
+
end
|
|
86
|
+
obj
|
|
79
87
|
when Array
|
|
80
|
-
obj.map
|
|
88
|
+
obj.map!.with_index { |v, i| interpolate_env_vars!(v, path + [i]) }
|
|
81
89
|
else
|
|
82
90
|
obj
|
|
83
91
|
end
|
|
84
92
|
end
|
|
85
93
|
|
|
94
|
+
def in_mcp_config?(path)
|
|
95
|
+
# Check if we're inside an MCP configuration
|
|
96
|
+
# Pattern: [..., "instances", instance_name, "mcps", index, ...]
|
|
97
|
+
return false if path.size < 4
|
|
98
|
+
|
|
99
|
+
# Find the position of "mcps" in the path
|
|
100
|
+
mcps_index = path.rindex("mcps")
|
|
101
|
+
return false unless mcps_index
|
|
102
|
+
|
|
103
|
+
# Check if this is under instances and followed by an array index
|
|
104
|
+
return false if mcps_index < 2
|
|
105
|
+
|
|
106
|
+
path[mcps_index - 2] == "instances" && path[mcps_index + 1].is_a?(Integer)
|
|
107
|
+
end
|
|
108
|
+
|
|
86
109
|
def interpolate_env_string(str)
|
|
87
110
|
str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
|
|
88
111
|
env_var = Regexp.last_match(1)
|
|
@@ -183,21 +183,17 @@ module ClaudeSwarm
|
|
|
183
183
|
args.push("--claude-session-id", claude_session_id) if claude_session_id
|
|
184
184
|
end
|
|
185
185
|
|
|
186
|
-
# Capture environment variables needed for
|
|
187
|
-
#
|
|
186
|
+
# Capture environment variables needed for running claude-swarm
|
|
187
|
+
# We intentionally exclude Bundler variables to ensure we use the system-installed gem
|
|
188
188
|
required_env = {}
|
|
189
189
|
|
|
190
|
-
#
|
|
191
|
-
ENV.each do |k, v|
|
|
192
|
-
required_env[k] = v if k.start_with?("BUNDLE_")
|
|
193
|
-
end
|
|
194
|
-
|
|
195
|
-
# Claude Swarm-specific variables
|
|
190
|
+
# Claude Swarm-specific variables (always needed)
|
|
196
191
|
ENV.each do |k, v|
|
|
197
192
|
required_env[k] = v if k.start_with?("CLAUDE_SWARM_")
|
|
198
193
|
end
|
|
199
194
|
|
|
200
195
|
# Ruby-specific variables that MCP servers need
|
|
196
|
+
# Exclude RUBYOPT and RUBYLIB to avoid Bundler interference
|
|
201
197
|
[
|
|
202
198
|
"RUBY_ROOT",
|
|
203
199
|
"RUBY_ENGINE",
|
|
@@ -205,8 +201,6 @@ module ClaudeSwarm
|
|
|
205
201
|
"GEM_ROOT",
|
|
206
202
|
"GEM_HOME",
|
|
207
203
|
"GEM_PATH",
|
|
208
|
-
"RUBYOPT",
|
|
209
|
-
"RUBYLIB",
|
|
210
204
|
"PATH",
|
|
211
205
|
].each do |key|
|
|
212
206
|
required_env[key] = ENV[key] if ENV[key]
|
|
@@ -38,7 +38,7 @@ module ClaudeSwarm
|
|
|
38
38
|
@session_log_path = File.join(@session_path, "session.log")
|
|
39
39
|
else
|
|
40
40
|
# Generate new session path
|
|
41
|
-
session_params = { working_dir:
|
|
41
|
+
session_params = { working_dir: @config.base_dir }
|
|
42
42
|
session_params[:session_id] = @provided_session_id if @provided_session_id
|
|
43
43
|
@session_path = SessionPath.generate(**session_params)
|
|
44
44
|
SessionPath.ensure_directory(@session_path)
|
|
@@ -49,7 +49,6 @@ module ClaudeSwarm
|
|
|
49
49
|
|
|
50
50
|
end
|
|
51
51
|
ENV["CLAUDE_SWARM_SESSION_PATH"] = @session_path
|
|
52
|
-
ENV["CLAUDE_SWARM_ROOT_DIR"] = ClaudeSwarm.root_dir
|
|
53
52
|
|
|
54
53
|
# Initialize components that depend on session path
|
|
55
54
|
@process_tracker = ProcessTracker.new(@session_path)
|
|
@@ -235,13 +234,11 @@ module ClaudeSwarm
|
|
|
235
234
|
before_commands_dir = parent_dir
|
|
236
235
|
end
|
|
237
236
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
exit(1)
|
|
244
|
-
end
|
|
237
|
+
success = execute_before_commands?(before_commands, chdir: before_commands_dir)
|
|
238
|
+
unless success
|
|
239
|
+
non_interactive_output { print("❌ Before commands failed. Aborting swarm launch.") }
|
|
240
|
+
cleanup_all
|
|
241
|
+
exit(1)
|
|
245
242
|
end
|
|
246
243
|
|
|
247
244
|
non_interactive_output do
|
|
@@ -262,19 +259,18 @@ module ClaudeSwarm
|
|
|
262
259
|
end
|
|
263
260
|
|
|
264
261
|
# Execute the main instance - this will cascade to other instances via MCP
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
end
|
|
262
|
+
# Execute main Claude instance with unbundled environment to avoid bundler conflicts
|
|
263
|
+
# This ensures the main instance runs in a clean environment without inheriting
|
|
264
|
+
# Claude Swarm's BUNDLE_* environment variables
|
|
265
|
+
main_dir = main_instance[:directory]
|
|
266
|
+
Bundler.with_unbundled_env do
|
|
267
|
+
if @non_interactive_prompt
|
|
268
|
+
stream_to_session_log(*command, chdir: main_dir)
|
|
269
|
+
else
|
|
270
|
+
system_with_pid!(*command, chdir: main_dir) do |pid|
|
|
271
|
+
@process_tracker.track_pid(pid, "claude_#{@config.main_instance}")
|
|
272
|
+
non_interactive_output do
|
|
273
|
+
puts "✓ Claude instance started with PID: #{pid}"
|
|
278
274
|
end
|
|
279
275
|
end
|
|
280
276
|
end
|
|
@@ -306,12 +302,12 @@ module ClaudeSwarm
|
|
|
306
302
|
puts
|
|
307
303
|
end
|
|
308
304
|
|
|
309
|
-
def execute_before_commands?(commands)
|
|
310
|
-
execute_commands(commands, phase: "before", fail_fast: true)
|
|
305
|
+
def execute_before_commands?(commands, chdir:)
|
|
306
|
+
execute_commands(commands, phase: "before", fail_fast: true, chdir: chdir)
|
|
311
307
|
end
|
|
312
308
|
|
|
313
|
-
def execute_after_commands?(commands)
|
|
314
|
-
execute_commands(commands, phase: "after", fail_fast: false)
|
|
309
|
+
def execute_after_commands?(commands, chdir:)
|
|
310
|
+
execute_commands(commands, phase: "after", fail_fast: false, chdir: chdir)
|
|
315
311
|
end
|
|
316
312
|
|
|
317
313
|
def execute_after_commands_once
|
|
@@ -336,16 +332,14 @@ module ClaudeSwarm
|
|
|
336
332
|
after_commands_dir = parent_dir
|
|
337
333
|
end
|
|
338
334
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
end
|
|
335
|
+
non_interactive_output do
|
|
336
|
+
print("⚙️ Executing after commands...")
|
|
337
|
+
end
|
|
343
338
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
end
|
|
339
|
+
success = execute_after_commands?(after_commands, chdir: after_commands_dir)
|
|
340
|
+
unless success
|
|
341
|
+
non_interactive_output do
|
|
342
|
+
puts "⚠️ Some after commands failed"
|
|
349
343
|
end
|
|
350
344
|
end
|
|
351
345
|
end
|
|
@@ -357,7 +351,7 @@ module ClaudeSwarm
|
|
|
357
351
|
|
|
358
352
|
# Save the root directory
|
|
359
353
|
root_dir_file = File.join(session_path, "root_directory")
|
|
360
|
-
File.write(root_dir_file,
|
|
354
|
+
File.write(root_dir_file, @config.base_dir)
|
|
361
355
|
|
|
362
356
|
# Save session metadata
|
|
363
357
|
metadata_file = File.join(session_path, "session_metadata.json")
|
|
@@ -366,7 +360,7 @@ module ClaudeSwarm
|
|
|
366
360
|
|
|
367
361
|
def build_session_metadata
|
|
368
362
|
{
|
|
369
|
-
"root_directory" =>
|
|
363
|
+
"root_directory" => @config.base_dir,
|
|
370
364
|
"timestamp" => Time.now.utc.iso8601,
|
|
371
365
|
"start_time" => @start_time.utc.iso8601,
|
|
372
366
|
"swarm_name" => @config.swarm_name,
|
|
@@ -642,12 +636,12 @@ module ClaudeSwarm
|
|
|
642
636
|
end
|
|
643
637
|
end
|
|
644
638
|
|
|
645
|
-
def stream_to_session_log(*command)
|
|
639
|
+
def stream_to_session_log(*command, chdir:)
|
|
646
640
|
# Setup logger for session logging
|
|
647
641
|
logger = Logger.new(@session_log_path, level: :info, progname: @config.main_instance)
|
|
648
642
|
|
|
649
643
|
# Use Open3.popen2e to capture stdout and stderr merged for formatting
|
|
650
|
-
Open3.popen2e(*command) do |stdin, stdout_and_stderr, wait_thr|
|
|
644
|
+
Open3.popen2e(*command, chdir: chdir) do |stdin, stdout_and_stderr, wait_thr|
|
|
651
645
|
stdin.close
|
|
652
646
|
|
|
653
647
|
# Read and process the merged output
|
|
@@ -819,7 +813,10 @@ module ClaudeSwarm
|
|
|
819
813
|
@logger ||= Logger.new(File.join(@session_path, "session.log"), level: :info, progname: "orchestrator")
|
|
820
814
|
end
|
|
821
815
|
|
|
822
|
-
def execute_commands(commands, phase:, fail_fast:)
|
|
816
|
+
def execute_commands(commands, phase:, fail_fast:, chdir:)
|
|
817
|
+
raise ArgumentError, "chdir parameter is required" if chdir.nil?
|
|
818
|
+
raise ArgumentError, "chdir must be a valid directory: #{chdir}" unless File.directory?(chdir)
|
|
819
|
+
|
|
823
820
|
all_succeeded = true
|
|
824
821
|
|
|
825
822
|
# Setup logger for session logging if we have a session path
|
|
@@ -839,13 +836,15 @@ module ClaudeSwarm
|
|
|
839
836
|
end
|
|
840
837
|
end
|
|
841
838
|
|
|
842
|
-
|
|
843
|
-
|
|
839
|
+
# Use Open3.capture2e with chdir option to execute the command
|
|
840
|
+
# This allows setting the working directory without changing the process directory
|
|
841
|
+
output, status = Open3.capture2e(command, chdir: chdir)
|
|
842
|
+
success = status.success?
|
|
844
843
|
output_separator = "-" * 80
|
|
845
844
|
|
|
846
845
|
logger.info { "Command output:" }
|
|
847
846
|
logger.info { output }
|
|
848
|
-
logger.info { "Exit status: #{
|
|
847
|
+
logger.info { "Exit status: #{status.exitstatus}" }
|
|
849
848
|
logger.info { output_separator }
|
|
850
849
|
|
|
851
850
|
# Show output if in debug mode or if command failed
|
|
@@ -854,7 +853,7 @@ module ClaudeSwarm
|
|
|
854
853
|
output_prefix = phase == "after" ? "After command" : "Command"
|
|
855
854
|
puts "#{output_prefix} #{index + 1} output:"
|
|
856
855
|
puts output
|
|
857
|
-
print("Exit status: #{
|
|
856
|
+
print("Exit status: #{status.exitstatus}")
|
|
858
857
|
end
|
|
859
858
|
end
|
|
860
859
|
|
|
@@ -2,14 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
module ClaudeSwarm
|
|
4
4
|
module SystemUtils
|
|
5
|
-
def system!(*args)
|
|
6
|
-
system(*args)
|
|
5
|
+
def system!(*args, **options)
|
|
6
|
+
system(*args, **options)
|
|
7
7
|
handle_command_failure(last_status, args)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
|
-
def system_with_pid!(*args)
|
|
10
|
+
def system_with_pid!(*args, **options)
|
|
11
11
|
# Spawn the process - by default, inherits the parent's I/O
|
|
12
|
-
pid = Process.spawn(*args)
|
|
12
|
+
pid = Process.spawn(*args, **options)
|
|
13
13
|
|
|
14
14
|
# Yield the PID to the block if given
|
|
15
15
|
yield(pid) if block_given?
|
data/lib/claude_swarm/version.rb
CHANGED
data/lib/claude_swarm.rb
CHANGED
|
@@ -26,25 +26,24 @@ require "fast_mcp"
|
|
|
26
26
|
require "mcp_client"
|
|
27
27
|
require "thor"
|
|
28
28
|
|
|
29
|
+
require_relative "claude_swarm/version"
|
|
29
30
|
# Zeitwerk setup
|
|
30
31
|
require "zeitwerk"
|
|
31
32
|
loader = Zeitwerk::Loader.new
|
|
32
|
-
loader.tag = "
|
|
33
|
-
|
|
33
|
+
loader.tag = File.basename(__FILE__, ".rb")
|
|
34
34
|
loader.ignore("#{__dir__}/claude_swarm/templates")
|
|
35
|
+
loader.push_dir("#{__dir__}/claude_swarm", namespace: ClaudeSwarm)
|
|
36
|
+
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
|
35
37
|
loader.inflector.inflect(
|
|
36
38
|
"cli" => "CLI",
|
|
37
39
|
"openai" => "OpenAI",
|
|
38
40
|
)
|
|
41
|
+
loader.setup
|
|
39
42
|
|
|
40
43
|
module ClaudeSwarm
|
|
41
44
|
class Error < StandardError; end
|
|
42
45
|
|
|
43
46
|
class << self
|
|
44
|
-
def root_dir
|
|
45
|
-
ENV.fetch("CLAUDE_SWARM_ROOT_DIR") { Dir.pwd }
|
|
46
|
-
end
|
|
47
|
-
|
|
48
47
|
def home_dir
|
|
49
48
|
ENV.fetch("CLAUDE_SWARM_HOME") { File.expand_path("~/.claude-swarm") }
|
|
50
49
|
end
|
|
@@ -66,6 +65,3 @@ module ClaudeSwarm
|
|
|
66
65
|
end
|
|
67
66
|
end
|
|
68
67
|
end
|
|
69
|
-
|
|
70
|
-
loader.push_dir("#{__dir__}/claude_swarm", namespace: ClaudeSwarm)
|
|
71
|
-
loader.setup
|
|
@@ -29,7 +29,7 @@ module SwarmCLI
|
|
|
29
29
|
|
|
30
30
|
# Validate the swarm configuration
|
|
31
31
|
begin
|
|
32
|
-
SwarmSDK
|
|
32
|
+
SwarmSDK.load_file(config_path)
|
|
33
33
|
rescue SwarmSDK::ConfigurationError => e
|
|
34
34
|
$stderr.puts "Error: Invalid swarm configuration: #{e.message}"
|
|
35
35
|
exit(1)
|
|
@@ -92,7 +92,7 @@ module SwarmCLI
|
|
|
92
92
|
|
|
93
93
|
define_method(:call) do |task:, description: nil, thinking_budget: nil|
|
|
94
94
|
# Load swarm for each execution (ensures fresh state)
|
|
95
|
-
swarm = SwarmSDK
|
|
95
|
+
swarm = SwarmSDK.load_file(self.class.config_path)
|
|
96
96
|
|
|
97
97
|
# Build prompt with thinking budget if provided
|
|
98
98
|
prompt = task
|
|
@@ -14,9 +14,9 @@ module SwarmCLI
|
|
|
14
14
|
|
|
15
15
|
def initialize(options)
|
|
16
16
|
@options = options
|
|
17
|
-
# Create scratchpad
|
|
18
|
-
|
|
19
|
-
@scratchpad = SwarmSDK::
|
|
17
|
+
# Create volatile scratchpad for MCP server
|
|
18
|
+
# Note: Scratchpad is always volatile - data is not persisted between sessions
|
|
19
|
+
@scratchpad = SwarmSDK::Tools::Stores::ScratchpadStorage.new
|
|
20
20
|
end
|
|
21
21
|
|
|
22
22
|
def execute
|
|
@@ -4,8 +4,8 @@ module SwarmCLI
|
|
|
4
4
|
# ConfigLoader handles loading swarm configurations from both YAML and Ruby DSL files.
|
|
5
5
|
#
|
|
6
6
|
# Supports:
|
|
7
|
-
# - YAML files (.yml, .yaml) - loaded via SwarmSDK
|
|
8
|
-
# - Ruby DSL files (.rb) - executed and expected to return a SwarmSDK::Swarm instance
|
|
7
|
+
# - YAML files (.yml, .yaml) - loaded via SwarmSDK.load_file
|
|
8
|
+
# - Ruby DSL files (.rb) - executed and expected to return a SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance
|
|
9
9
|
#
|
|
10
10
|
# @example Load YAML config
|
|
11
11
|
# swarm = ConfigLoader.load("config.yml")
|
|
@@ -18,11 +18,11 @@ module SwarmCLI
|
|
|
18
18
|
# Load a swarm configuration from file (YAML or Ruby DSL)
|
|
19
19
|
#
|
|
20
20
|
# Detects file type by extension:
|
|
21
|
-
# - .yml, .yaml -> Load as YAML using SwarmSDK
|
|
22
|
-
# - .rb -> Execute as Ruby DSL and expect SwarmSDK::Swarm instance
|
|
21
|
+
# - .yml, .yaml -> Load as YAML using SwarmSDK.load_file
|
|
22
|
+
# - .rb -> Execute as Ruby DSL and expect SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance
|
|
23
23
|
#
|
|
24
24
|
# @param path [String, Pathname] Path to configuration file
|
|
25
|
-
# @return [SwarmSDK::Swarm] Configured swarm instance
|
|
25
|
+
# @return [SwarmSDK::Swarm, SwarmSDK::NodeOrchestrator] Configured swarm or orchestrator instance
|
|
26
26
|
# @raise [SwarmCLI::ConfigurationError] If file not found or invalid format
|
|
27
27
|
def load(path)
|
|
28
28
|
path = Pathname.new(path).expand_path
|
|
@@ -50,7 +50,7 @@ module SwarmCLI
|
|
|
50
50
|
# @param path [Pathname] Path to YAML file
|
|
51
51
|
# @return [SwarmSDK::Swarm] Configured swarm instance
|
|
52
52
|
def load_yaml(path)
|
|
53
|
-
SwarmSDK
|
|
53
|
+
SwarmSDK.load_file(path.to_s)
|
|
54
54
|
rescue SwarmSDK::ConfigurationError => e
|
|
55
55
|
# Re-raise with CLI context
|
|
56
56
|
raise ConfigurationError, "Configuration error in #{path}: #{e.message}"
|
|
@@ -59,12 +59,12 @@ module SwarmCLI
|
|
|
59
59
|
# Load Ruby DSL configuration file
|
|
60
60
|
#
|
|
61
61
|
# Executes the Ruby file in a clean binding and expects it to return
|
|
62
|
-
# a SwarmSDK::Swarm instance. The file should
|
|
63
|
-
# create a Swarm instance directly.
|
|
62
|
+
# a SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance. The file should
|
|
63
|
+
# use SwarmSDK.build or create a Swarm/NodeOrchestrator instance directly.
|
|
64
64
|
#
|
|
65
65
|
# @param path [Pathname] Path to Ruby DSL file
|
|
66
|
-
# @return [SwarmSDK::Swarm] Configured swarm instance
|
|
67
|
-
# @raise [ConfigurationError] If file doesn't return a
|
|
66
|
+
# @return [SwarmSDK::Swarm, SwarmSDK::NodeOrchestrator] Configured swarm or orchestrator instance
|
|
67
|
+
# @raise [ConfigurationError] If file doesn't return a valid instance
|
|
68
68
|
def load_ruby_dsl(path)
|
|
69
69
|
# Read the file content
|
|
70
70
|
content = path.read
|
|
@@ -73,10 +73,11 @@ module SwarmCLI
|
|
|
73
73
|
# This allows the DSL file to use SwarmSDK.build directly
|
|
74
74
|
result = eval(content, binding, path.to_s, 1) # rubocop:disable Security/Eval
|
|
75
75
|
|
|
76
|
-
# Validate result is a Swarm instance
|
|
77
|
-
|
|
76
|
+
# Validate result is a Swarm or NodeOrchestrator instance
|
|
77
|
+
# Both have the same execute(prompt) interface
|
|
78
|
+
unless result.is_a?(SwarmSDK::Swarm) || result.is_a?(SwarmSDK::NodeOrchestrator)
|
|
78
79
|
raise ConfigurationError,
|
|
79
|
-
"Ruby DSL file must return a SwarmSDK::Swarm instance. " \
|
|
80
|
+
"Ruby DSL file must return a SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance. " \
|
|
80
81
|
"Got: #{result.class}. " \
|
|
81
82
|
"Use: SwarmSDK.build { ... } or Swarm.new(...)"
|
|
82
83
|
end
|
data/lib/swarm_cli/version.rb
CHANGED
data/lib/swarm_cli.rb
CHANGED
|
@@ -22,7 +22,9 @@ require_relative "swarm_cli/version"
|
|
|
22
22
|
|
|
23
23
|
require "zeitwerk"
|
|
24
24
|
loader = Zeitwerk::Loader.new
|
|
25
|
+
loader.tag = File.basename(__FILE__, ".rb")
|
|
25
26
|
loader.push_dir("#{__dir__}/swarm_cli", namespace: SwarmCLI)
|
|
27
|
+
loader.inflector = Zeitwerk::GemInflector.new(__FILE__)
|
|
26
28
|
loader.inflector.inflect(
|
|
27
29
|
"cli" => "CLI",
|
|
28
30
|
"ui" => "UI",
|
|
@@ -7,11 +7,11 @@ module SwarmMemory
|
|
|
7
7
|
# Subclasses must implement all public methods to provide
|
|
8
8
|
# different storage backends (filesystem, Redis, SQLite, etc.)
|
|
9
9
|
class Base
|
|
10
|
-
# Maximum size per entry (
|
|
11
|
-
MAX_ENTRY_SIZE =
|
|
10
|
+
# Maximum size per entry (3MB)
|
|
11
|
+
MAX_ENTRY_SIZE = 3_000_000
|
|
12
12
|
|
|
13
|
-
# Maximum total storage size (
|
|
14
|
-
MAX_TOTAL_SIZE =
|
|
13
|
+
# Maximum total storage size (100GB)
|
|
14
|
+
MAX_TOTAL_SIZE = 100_000_000_000
|
|
15
15
|
|
|
16
16
|
# Write content to storage
|
|
17
17
|
#
|
|
@@ -171,12 +171,6 @@ module SwarmMemory
|
|
|
171
171
|
|
|
172
172
|
content = File.read(md_file)
|
|
173
173
|
|
|
174
|
-
# Check if it's a stub (redirect)
|
|
175
|
-
if stub_content?(content)
|
|
176
|
-
target_path = extract_redirect_target(content)
|
|
177
|
-
return read(file_path: target_path) if target_path
|
|
178
|
-
end
|
|
179
|
-
|
|
180
174
|
# Increment hit counter
|
|
181
175
|
increment_hits(file_path)
|
|
182
176
|
|
|
@@ -205,12 +199,6 @@ module SwarmMemory
|
|
|
205
199
|
|
|
206
200
|
content = File.read(md_file)
|
|
207
201
|
|
|
208
|
-
# Follow stub redirect if applicable
|
|
209
|
-
if stub_content?(content)
|
|
210
|
-
target_path = extract_redirect_target(content)
|
|
211
|
-
return read_entry(file_path: target_path) if target_path
|
|
212
|
-
end
|
|
213
|
-
|
|
214
202
|
# Read metadata
|
|
215
203
|
yaml_data = File.exist?(yaml_file) ? YAML.load_file(yaml_file, permitted_classes: [Time, Date, Symbol]) : {}
|
|
216
204
|
|