claude_swarm 0.1.20 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +9 -66
  3. data/.rubocop_todo.yml +11 -0
  4. data/CHANGELOG.md +93 -0
  5. data/CLAUDE.md +61 -0
  6. data/README.md +172 -15
  7. data/Rakefile +1 -1
  8. data/examples/mixed-provider-swarm.yml +23 -0
  9. data/lib/claude_swarm/claude_code_executor.rb +7 -12
  10. data/lib/claude_swarm/claude_mcp_server.rb +26 -12
  11. data/lib/claude_swarm/cli.rb +293 -165
  12. data/lib/claude_swarm/commands/ps.rb +22 -24
  13. data/lib/claude_swarm/commands/show.rb +45 -63
  14. data/lib/claude_swarm/configuration.rb +137 -8
  15. data/lib/claude_swarm/mcp_generator.rb +39 -14
  16. data/lib/claude_swarm/openai/chat_completion.rb +264 -0
  17. data/lib/claude_swarm/openai/executor.rb +301 -0
  18. data/lib/claude_swarm/openai/responses.rb +338 -0
  19. data/lib/claude_swarm/orchestrator.rb +205 -39
  20. data/lib/claude_swarm/process_tracker.rb +7 -7
  21. data/lib/claude_swarm/session_cost_calculator.rb +93 -0
  22. data/lib/claude_swarm/session_path.rb +3 -5
  23. data/lib/claude_swarm/system_utils.rb +1 -3
  24. data/lib/claude_swarm/tools/reset_session_tool.rb +24 -0
  25. data/lib/claude_swarm/tools/session_info_tool.rb +24 -0
  26. data/lib/claude_swarm/tools/task_tool.rb +43 -0
  27. data/lib/claude_swarm/version.rb +1 -1
  28. data/lib/claude_swarm/worktree_manager.rb +13 -20
  29. data/lib/claude_swarm.rb +23 -10
  30. data/single.yml +482 -6
  31. metadata +50 -16
  32. data/claude-swarm.yml +0 -64
  33. data/lib/claude_swarm/reset_session_tool.rb +0 -22
  34. data/lib/claude_swarm/session_info_tool.rb +0 -22
  35. data/lib/claude_swarm/task_tool.rb +0 -39
  36. /data/{example → examples}/claude-swarm.yml +0 -0
  37. /data/{example → examples}/microservices-team.yml +0 -0
  38. /data/{example → examples}/session-restoration-demo.yml +0 -0
  39. /data/{example → examples}/test-generation.yml +0 -0
@@ -1,107 +1,190 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "thor"
4
- require "json"
5
- require "erb"
6
-
7
3
  module ClaudeSwarm
8
4
  class CLI < Thor
9
5
  include SystemUtils
10
- def self.exit_on_failure?
11
- true
6
+ class << self
7
+ def exit_on_failure?
8
+ true
9
+ end
12
10
  end
13
11
 
14
12
  desc "start [CONFIG_FILE]", "Start a Claude Swarm from configuration file"
15
- method_option :config, aliases: "-c", type: :string, default: "claude-swarm.yml",
16
- desc: "Path to configuration file"
17
- method_option :vibe, type: :boolean, default: false,
18
- desc: "Run with --dangerously-skip-permissions for all instances"
19
- method_option :prompt, aliases: "-p", type: :string,
20
- desc: "Prompt to pass to the main Claude instance (non-interactive mode)"
21
- method_option :stream_logs, type: :boolean, default: false,
22
- desc: "Stream session logs to stdout (only works with -p)"
23
- method_option :debug, type: :boolean, default: false,
24
- desc: "Enable debug output"
25
- method_option :session_id, type: :string,
26
- desc: "Resume a previous session by ID or path"
27
- method_option :worktree, type: :string, aliases: "-w",
28
- desc: "Create instances in Git worktrees with the given name (auto-generated if true)",
29
- banner: "[NAME]"
13
+ method_option :vibe,
14
+ type: :boolean,
15
+ default: false,
16
+ desc: "Run with --dangerously-skip-permissions for all instances"
17
+ method_option :prompt,
18
+ aliases: "-p",
19
+ type: :string,
20
+ desc: "Prompt to pass to the main Claude instance (non-interactive mode)"
21
+ method_option :stream_logs,
22
+ type: :boolean,
23
+ default: false,
24
+ desc: "Stream session logs to stdout (only works with -p)"
25
+ method_option :debug,
26
+ type: :boolean,
27
+ default: false,
28
+ desc: "Enable debug output"
29
+ method_option :worktree,
30
+ type: :string,
31
+ aliases: "-w",
32
+ desc: "Create instances in Git worktrees with the given name (auto-generated if true)",
33
+ banner: "[NAME]"
34
+ method_option :session_id,
35
+ type: :string,
36
+ desc: "Use a specific session ID instead of generating one"
30
37
  def start(config_file = nil)
31
- # Handle session restoration
32
- if options[:session_id]
33
- restore_session(options[:session_id])
34
- return
35
- end
36
-
37
- config_path = config_file || options[:config]
38
+ config_path = config_file || "claude-swarm.yml"
38
39
  unless File.exist?(config_path)
39
- error "Configuration file not found: #{config_path}"
40
- exit 1
40
+ error("Configuration file not found: #{config_path}")
41
+ exit(1)
41
42
  end
42
43
 
43
- say "Starting Claude Swarm from #{config_path}..." unless options[:prompt]
44
+ say("Starting Claude Swarm from #{config_path}...") unless options[:prompt]
44
45
 
45
46
  # Validate stream_logs option
46
47
  if options[:stream_logs] && !options[:prompt]
47
- error "--stream-logs can only be used with -p/--prompt"
48
- exit 1
48
+ error("--stream-logs can only be used with -p/--prompt")
49
+ exit(1)
49
50
  end
50
51
 
51
52
  begin
52
- config = Configuration.new(config_path, base_dir: Dir.pwd)
53
+ config = Configuration.new(config_path, base_dir: Dir.pwd, options: options)
53
54
  generator = McpGenerator.new(config, vibe: options[:vibe])
54
- orchestrator = Orchestrator.new(config, generator,
55
- vibe: options[:vibe],
56
- prompt: options[:prompt],
57
- stream_logs: options[:stream_logs],
58
- debug: options[:debug],
59
- worktree: options[:worktree])
55
+ orchestrator = Orchestrator.new(
56
+ config,
57
+ generator,
58
+ vibe: options[:vibe],
59
+ prompt: options[:prompt],
60
+ stream_logs: options[:stream_logs],
61
+ debug: options[:debug],
62
+ worktree: options[:worktree],
63
+ session_id: options[:session_id],
64
+ )
60
65
  orchestrator.start
61
66
  rescue Error => e
62
- error e.message
63
- exit 1
67
+ error(e.message)
68
+ exit(1)
64
69
  rescue StandardError => e
65
- error "Unexpected error: #{e.message}"
66
- error e.backtrace.join("\n") if options[:verbose]
67
- exit 1
70
+ error("Unexpected error: #{e.message}")
71
+ error(e.backtrace.join("\n")) if options[:verbose]
72
+ exit(1)
68
73
  end
69
74
  end
70
75
 
71
76
  desc "mcp-serve", "Start an MCP server for a Claude instance"
72
- method_option :name, aliases: "-n", type: :string, required: true,
73
- desc: "Instance name"
74
- method_option :directory, aliases: "-d", type: :string, required: true,
75
- desc: "Working directory for the instance"
76
- method_option :directories, type: :array,
77
- desc: "All directories (including main directory) for the instance"
78
- method_option :model, aliases: "-m", type: :string, required: true,
79
- desc: "Claude model to use (e.g., opus, sonnet)"
80
- method_option :prompt, aliases: "-p", type: :string,
81
- desc: "System prompt for the instance"
82
- method_option :description, type: :string,
83
- desc: "Description of the instance's role"
84
- method_option :allowed_tools, aliases: "-t", type: :array,
85
- desc: "Allowed tools for the instance"
86
- method_option :disallowed_tools, type: :array,
87
- desc: "Disallowed tools for the instance"
88
- method_option :connections, type: :array,
89
- desc: "Connections to other instances"
90
- method_option :mcp_config_path, type: :string,
91
- desc: "Path to MCP configuration file"
92
- method_option :debug, type: :boolean, default: false,
93
- desc: "Enable debug output"
94
- method_option :vibe, type: :boolean, default: false,
95
- desc: "Run with --dangerously-skip-permissions"
96
- method_option :calling_instance, type: :string, required: true,
97
- desc: "Name of the instance that launched this MCP server"
98
- method_option :calling_instance_id, type: :string,
99
- desc: "Unique ID of the instance that launched this MCP server"
100
- method_option :instance_id, type: :string,
101
- desc: "Unique ID of this instance"
102
- method_option :claude_session_id, type: :string,
103
- desc: "Claude session ID to resume"
77
+ method_option :name,
78
+ aliases: "-n",
79
+ type: :string,
80
+ required: true,
81
+ desc: "Instance name"
82
+ method_option :directory,
83
+ aliases: "-d",
84
+ type: :string,
85
+ required: true,
86
+ desc: "Working directory for the instance"
87
+ method_option :directories,
88
+ type: :array,
89
+ desc: "All directories (including main directory) for the instance"
90
+ method_option :model,
91
+ aliases: "-m",
92
+ type: :string,
93
+ required: true,
94
+ desc: "Claude model to use (e.g., opus, sonnet)"
95
+ method_option :prompt,
96
+ aliases: "-p",
97
+ type: :string,
98
+ desc: "System prompt for the instance"
99
+ method_option :description,
100
+ type: :string,
101
+ desc: "Description of the instance's role"
102
+ method_option :allowed_tools,
103
+ aliases: "-t",
104
+ type: :array,
105
+ desc: "Allowed tools for the instance"
106
+ method_option :disallowed_tools,
107
+ type: :array,
108
+ desc: "Disallowed tools for the instance"
109
+ method_option :connections,
110
+ type: :array,
111
+ desc: "Connections to other instances"
112
+ method_option :mcp_config_path,
113
+ type: :string,
114
+ desc: "Path to MCP configuration file"
115
+ method_option :debug,
116
+ type: :boolean,
117
+ default: false,
118
+ desc: "Enable debug output"
119
+ method_option :vibe,
120
+ type: :boolean,
121
+ default: false,
122
+ desc: "Run with --dangerously-skip-permissions"
123
+ method_option :calling_instance,
124
+ type: :string,
125
+ required: true,
126
+ desc: "Name of the instance that launched this MCP server"
127
+ method_option :calling_instance_id,
128
+ type: :string,
129
+ desc: "Unique ID of the instance that launched this MCP server"
130
+ method_option :instance_id,
131
+ type: :string,
132
+ desc: "Unique ID of this instance"
133
+ method_option :claude_session_id,
134
+ type: :string,
135
+ desc: "Claude session ID to resume"
136
+ method_option :provider,
137
+ type: :string,
138
+ desc: "Provider to use (claude or openai)"
139
+ method_option :temperature,
140
+ type: :numeric,
141
+ desc: "Temperature for OpenAI models"
142
+ method_option :api_version,
143
+ type: :string,
144
+ desc: "API version for OpenAI (chat_completion or responses)"
145
+ method_option :openai_token_env,
146
+ type: :string,
147
+ desc: "Environment variable name for OpenAI API key"
148
+ method_option :base_url,
149
+ type: :string,
150
+ desc: "Base URL for OpenAI API"
151
+ method_option :reasoning_effort,
152
+ type: :string,
153
+ desc: "Reasoning effort for OpenAI models"
104
154
  def mcp_serve
155
+ # Validate reasoning_effort if provided
156
+ if options[:reasoning_effort]
157
+ # Only validate if provider is openai (or not specified, since it could be set elsewhere)
158
+ if options[:provider] && options[:provider] != "openai"
159
+ error("reasoning_effort is only supported for OpenAI models")
160
+ exit(1)
161
+ end
162
+
163
+ # Validate it's used with an o-series model
164
+ model = options[:model]
165
+ unless model&.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
166
+ error("reasoning_effort is only supported for o-series models (o1, o1 Preview, o1-mini, o1-pro, o3, o3-mini, o3-pro, o3-deep-research, o4-mini, o4-mini-deep-research, etc.)")
167
+ error("Current model: #{model}")
168
+ exit(1)
169
+ end
170
+
171
+ # Validate the value
172
+ unless ClaudeSwarm::Configuration::VALID_REASONING_EFFORTS.include?(options[:reasoning_effort])
173
+ error("reasoning_effort must be 'low', 'medium', or 'high'")
174
+ exit(1)
175
+ end
176
+ end
177
+
178
+ # Validate temperature is not used with o-series models
179
+ if options[:temperature] && options[:provider] == "openai"
180
+ model = options[:model]
181
+ if model&.match?(ClaudeSwarm::Configuration::O_SERIES_MODEL_PATTERN)
182
+ error("temperature parameter is not supported for o-series models (#{model})")
183
+ error("O-series models use deterministic reasoning and don't accept temperature settings")
184
+ exit(1)
185
+ end
186
+ end
187
+
105
188
  instance_config = {
106
189
  name: options[:name],
107
190
  directory: options[:directory],
@@ -115,33 +198,42 @@ module ClaudeSwarm
115
198
  mcp_config_path: options[:mcp_config_path],
116
199
  vibe: options[:vibe] || false,
117
200
  instance_id: options[:instance_id],
118
- claude_session_id: options[:claude_session_id]
201
+ claude_session_id: options[:claude_session_id],
202
+ provider: options[:provider],
203
+ temperature: options[:temperature],
204
+ api_version: options[:api_version],
205
+ openai_token_env: options[:openai_token_env],
206
+ base_url: options[:base_url],
207
+ reasoning_effort: options[:reasoning_effort],
119
208
  }
120
209
 
121
210
  begin
122
211
  server = ClaudeMcpServer.new(
123
212
  instance_config,
124
213
  calling_instance: options[:calling_instance],
125
- calling_instance_id: options[:calling_instance_id]
214
+ calling_instance_id: options[:calling_instance_id],
126
215
  )
127
216
  server.start
128
217
  rescue StandardError => e
129
- error "Error starting MCP server: #{e.message}"
130
- error e.backtrace.join("\n") if options[:debug]
131
- exit 1
218
+ error("Error starting MCP server: #{e.message}")
219
+ error(e.backtrace.join("\n")) if options[:debug]
220
+ exit(1)
132
221
  end
133
222
  end
134
223
 
135
224
  desc "init", "Initialize a new claude-swarm.yml configuration file"
136
- method_option :force, aliases: "-f", type: :boolean, default: false,
137
- desc: "Overwrite existing configuration file"
225
+ method_option :force,
226
+ aliases: "-f",
227
+ type: :boolean,
228
+ default: false,
229
+ desc: "Overwrite existing configuration file"
138
230
  def init
139
231
  config_path = "claude-swarm.yml"
140
232
 
141
233
  if File.exist?(config_path) && !options[:force]
142
- error "Configuration file already exists: #{config_path}"
143
- error "Use --force to overwrite"
144
- exit 1
234
+ error("Configuration file already exists: #{config_path}")
235
+ error("Use --force to overwrite")
236
+ exit(1)
145
237
  end
146
238
 
147
239
  template = <<~YAML
@@ -158,7 +250,8 @@ module ClaudeSwarm
158
250
  description: "Lead developer who coordinates the team and makes architectural decisions"
159
251
  directory: .
160
252
  model: sonnet
161
- prompt: "You are the lead developer coordinating the team"
253
+ prompt: |
254
+ You are the lead developer coordinating the team
162
255
  allowed_tools: [Read, Edit, Bash, Write]
163
256
  # connections: [frontend_dev, backend_dev]
164
257
 
@@ -168,49 +261,59 @@ module ClaudeSwarm
168
261
  # description: "Frontend developer specializing in React and modern web technologies"
169
262
  # directory: ./frontend
170
263
  # model: sonnet
171
- # prompt: "You specialize in frontend development with React, TypeScript, and modern web technologies"
264
+ # prompt: |
265
+ # You specialize in frontend development with React, TypeScript, and modern web technologies
172
266
  # allowed_tools: [Read, Edit, Write, "Bash(npm:*)", "Bash(yarn:*)", "Bash(pnpm:*)"]
173
267
 
174
268
  # backend_dev:
175
- # description: "Backend developer focusing on APIs, databases, and server architecture"
269
+ # description: |
270
+ # Backend developer focusing on APIs, databases, and server architecture
176
271
  # directory: ../other-app/backend
177
272
  # model: sonnet
178
- # prompt: "You specialize in backend development, APIs, databases, and server architecture"
273
+ # prompt: |
274
+ # You specialize in backend development, APIs, databases, and server architecture
179
275
  # allowed_tools: [Read, Edit, Write, Bash]
180
276
 
181
277
  # devops_engineer:
182
278
  # description: "DevOps engineer managing infrastructure, CI/CD, and deployments"
183
279
  # directory: .
184
280
  # model: sonnet
185
- # prompt: "You specialize in infrastructure, CI/CD, containerization, and deployment"
281
+ # prompt: |
282
+ # You specialize in infrastrujcture, CI/CD, containerization, and deployment
186
283
  # allowed_tools: [Read, Edit, Write, "Bash(docker:*)", "Bash(kubectl:*)", "Bash(terraform:*)"]
187
284
 
188
285
  # qa_engineer:
189
286
  # description: "QA engineer ensuring quality through comprehensive testing"
190
287
  # directory: ./tests
191
288
  # model: sonnet
192
- # prompt: "You specialize in testing, quality assurance, and test automation"
289
+ # prompt: |
290
+ # You specialize in testing, quality assurance, and test automation
193
291
  # allowed_tools: [Read, Edit, Write, Bash]
194
292
  YAML
195
293
 
196
294
  File.write(config_path, template)
197
- say "Created #{config_path}", :green
198
- say "Edit this file to configure your swarm, then run 'claude-swarm' to start"
295
+ say("Created #{config_path}", :green)
296
+ say("Edit this file to configure your swarm, then run 'claude-swarm' to start")
199
297
  end
200
298
 
201
299
  desc "generate", "Launch Claude to help generate a swarm configuration interactively"
202
- method_option :output, aliases: "-o", type: :string,
203
- desc: "Output file path for the generated configuration"
204
- method_option :model, aliases: "-m", type: :string, default: "sonnet",
205
- desc: "Claude model to use for generation"
300
+ method_option :output,
301
+ aliases: "-o",
302
+ type: :string,
303
+ desc: "Output file path for the generated configuration"
304
+ method_option :model,
305
+ aliases: "-m",
306
+ type: :string,
307
+ default: "sonnet",
308
+ desc: "Claude model to use for generation"
206
309
  def generate
207
310
  # Check if claude command exists
208
311
  begin
209
312
  system!("command -v claude > /dev/null 2>&1")
210
313
  rescue Error
211
- error "Claude CLI is not installed or not in PATH"
212
- say "To install Claude CLI, visit: https://docs.anthropic.com/en/docs/claude-code"
213
- exit 1
314
+ error("Claude CLI is not installed or not in PATH")
315
+ say("To install Claude CLI, visit: https://docs.anthropic.com/en/docs/claude-code")
316
+ exit(1)
214
317
  end
215
318
 
216
319
  # Read README for context about claude-swarm capabilities
@@ -223,8 +326,9 @@ module ClaudeSwarm
223
326
  # Launch Claude in interactive mode with the initial prompt
224
327
  cmd = [
225
328
  "claude",
226
- "--model", options[:model],
227
- preprompt
329
+ "--model",
330
+ options[:model],
331
+ preprompt,
228
332
  ]
229
333
 
230
334
  # Execute and let the user take over
@@ -233,7 +337,7 @@ module ClaudeSwarm
233
337
 
234
338
  desc "version", "Show Claude Swarm version"
235
339
  def version
236
- say "Claude Swarm #{VERSION}"
340
+ say("Claude Swarm #{VERSION}")
237
341
  end
238
342
 
239
343
  desc "ps", "List running Claude Swarm sessions"
@@ -247,8 +351,11 @@ module ClaudeSwarm
247
351
  end
248
352
 
249
353
  desc "clean", "Remove stale session symlinks and orphaned worktrees"
250
- method_option :days, aliases: "-d", type: :numeric, default: 7,
251
- desc: "Remove sessions older than N days"
354
+ method_option :days,
355
+ aliases: "-d",
356
+ type: :numeric,
357
+ default: 7,
358
+ desc: "Remove sessions older than N days"
252
359
  def clean
253
360
  # Clean stale symlinks
254
361
  cleaned_symlinks = clean_stale_symlinks(options[:days])
@@ -257,49 +364,60 @@ module ClaudeSwarm
257
364
  cleaned_worktrees = clean_orphaned_worktrees(options[:days])
258
365
 
259
366
  if cleaned_symlinks.positive? || cleaned_worktrees.positive?
260
- say "Cleaned #{cleaned_symlinks} stale symlink#{"s" unless cleaned_symlinks == 1}", :green
261
- say "Cleaned #{cleaned_worktrees} orphaned worktree#{"s" unless cleaned_worktrees == 1}", :green
367
+ say("Cleaned #{cleaned_symlinks} stale symlink#{"s" unless cleaned_symlinks == 1}", :green)
368
+ say("Cleaned #{cleaned_worktrees} orphaned worktree#{"s" unless cleaned_worktrees == 1}", :green)
262
369
  else
263
- say "No cleanup needed", :green
370
+ say("No cleanup needed", :green)
264
371
  end
265
372
  end
266
373
 
374
+ desc "restore SESSION_ID", "Restore a previous session by ID"
375
+ def restore(session_id)
376
+ restore_session(session_id)
377
+ end
378
+
267
379
  desc "watch SESSION_ID", "Watch session logs"
268
- method_option :lines, aliases: "-n", type: :numeric, default: 100,
269
- desc: "Number of lines to show initially"
380
+ method_option :lines,
381
+ aliases: "-n",
382
+ type: :numeric,
383
+ default: 100,
384
+ desc: "Number of lines to show initially"
270
385
  def watch(session_id)
271
386
  # Find session path
272
387
  run_symlink = File.join(File.expand_path("~/.claude-swarm/run"), session_id)
273
388
  session_path = if File.symlink?(run_symlink)
274
- File.readlink(run_symlink)
275
- else
276
- # Search in sessions directory
277
- Dir.glob(File.expand_path("~/.claude-swarm/sessions/*/*")).find do |path|
278
- File.basename(path) == session_id
279
- end
280
- end
389
+ File.readlink(run_symlink)
390
+ else
391
+ # Search in sessions directory
392
+ Dir.glob(File.expand_path("~/.claude-swarm/sessions/*/*")).find do |path|
393
+ File.basename(path) == session_id
394
+ end
395
+ end
281
396
 
282
397
  unless session_path && Dir.exist?(session_path)
283
- error "Session not found: #{session_id}"
284
- exit 1
398
+ error("Session not found: #{session_id}")
399
+ exit(1)
285
400
  end
286
401
 
287
402
  log_file = File.join(session_path, "session.log")
288
403
  unless File.exist?(log_file)
289
- error "Log file not found for session: #{session_id}"
290
- exit 1
404
+ error("Log file not found for session: #{session_id}")
405
+ exit(1)
291
406
  end
292
407
 
293
408
  exec("tail", "-f", "-n", options[:lines].to_s, log_file)
294
409
  end
295
410
 
296
411
  desc "list-sessions", "List all available Claude Swarm sessions"
297
- method_option :limit, aliases: "-l", type: :numeric, default: 10,
298
- desc: "Maximum number of sessions to display"
412
+ method_option :limit,
413
+ aliases: "-l",
414
+ type: :numeric,
415
+ default: 10,
416
+ desc: "Maximum number of sessions to display"
299
417
  def list_sessions
300
418
  sessions_dir = File.expand_path("~/.claude-swarm/sessions")
301
419
  unless Dir.exist?(sessions_dir)
302
- say "No sessions found", :yellow
420
+ say("No sessions found", :yellow)
303
421
  return
304
422
  end
305
423
 
@@ -335,7 +453,7 @@ module ClaudeSwarm
335
453
  main_instance: main_instance,
336
454
  instances_count: mcp_files.size,
337
455
  swarm_name: swarm_name,
338
- config_path: config_file
456
+ config_path: config_file,
339
457
  }
340
458
  rescue StandardError
341
459
  # Skip invalid manifests
@@ -343,7 +461,7 @@ module ClaudeSwarm
343
461
  end
344
462
 
345
463
  if sessions.empty?
346
- say "No sessions found", :yellow
464
+ say("No sessions found", :yellow)
347
465
  return
348
466
  end
349
467
 
@@ -352,18 +470,18 @@ module ClaudeSwarm
352
470
  sessions = sessions.first(options[:limit])
353
471
 
354
472
  # Display sessions
355
- say "\nAvailable sessions (newest first):\n", :bold
473
+ say("\nAvailable sessions (newest first):\n", :bold)
356
474
  sessions.each do |session|
357
- say "\n#{session[:project]}/#{session[:id]}", :green
358
- say " Created: #{session[:created_at].strftime("%Y-%m-%d %H:%M:%S")}"
359
- say " Main: #{session[:main_instance]}"
360
- say " Instances: #{session[:instances_count]}"
361
- say " Swarm: #{session[:swarm_name]}"
362
- say " Config: #{session[:config_path]}", :cyan
475
+ say("\n#{session[:project]}/#{session[:id]}", :green)
476
+ say(" Created: #{session[:created_at].strftime("%Y-%m-%d %H:%M:%S")}")
477
+ say(" Main: #{session[:main_instance]}")
478
+ say(" Instances: #{session[:instances_count]}")
479
+ say(" Swarm: #{session[:swarm_name]}")
480
+ say(" Config: #{session[:config_path]}", :cyan)
363
481
  end
364
482
 
365
- say "\nTo resume a session, run:", :bold
366
- say " claude-swarm --session-id <session-id>", :cyan
483
+ say("\nTo resume a session, run:", :bold)
484
+ say(" claude-swarm restore <session-id>", :cyan)
367
485
  end
368
486
 
369
487
  default_task :start
@@ -371,33 +489,33 @@ module ClaudeSwarm
371
489
  private
372
490
 
373
491
  def error(message)
374
- say message, :red
492
+ say(message, :red)
375
493
  end
376
494
 
377
495
  def restore_session(session_id)
378
- say "Restoring session: #{session_id}", :green
496
+ say("Restoring session: #{session_id}", :green)
379
497
 
380
498
  # Find the session path
381
499
  session_path = find_session_path(session_id)
382
500
  unless session_path
383
- error "Session not found: #{session_id}"
384
- exit 1
501
+ error("Session not found: #{session_id}")
502
+ exit(1)
385
503
  end
386
504
 
387
505
  begin
388
506
  # Load session info from instance ID in MCP config
389
507
  mcp_files = Dir.glob(File.join(session_path, "*.mcp.json"))
390
508
  if mcp_files.empty?
391
- error "No MCP configuration files found in session"
392
- exit 1
509
+ error("No MCP configuration files found in session")
510
+ exit(1)
393
511
  end
394
512
 
395
513
  # Load the configuration from the session directory
396
514
  config_file = File.join(session_path, "config.yml")
397
515
 
398
516
  unless File.exist?(config_file)
399
- error "Configuration file not found in session"
400
- exit 1
517
+ error("Configuration file not found in session")
518
+ exit(1)
401
519
  end
402
520
 
403
521
  # Change to the original start directory if it exists
@@ -406,10 +524,10 @@ module ClaudeSwarm
406
524
  original_dir = File.read(start_dir_file).strip
407
525
  if Dir.exist?(original_dir)
408
526
  Dir.chdir(original_dir)
409
- say "Changed to original directory: #{original_dir}", :green unless options[:prompt]
527
+ say("Changed to original directory: #{original_dir}", :green) unless options[:prompt]
410
528
  else
411
- error "Original directory no longer exists: #{original_dir}"
412
- exit 1
529
+ error("Original directory no longer exists: #{original_dir}")
530
+ exit(1)
413
531
  end
414
532
  end
415
533
 
@@ -422,33 +540,34 @@ module ClaudeSwarm
422
540
  metadata = JSON.parse(File.read(session_metadata_file))
423
541
  if metadata["worktree"] && metadata["worktree"]["enabled"]
424
542
  worktree_name = metadata["worktree"]["name"]
425
- say "Restoring with worktree: #{worktree_name}", :green unless options[:prompt]
543
+ say("Restoring with worktree: #{worktree_name}", :green) unless options[:prompt]
426
544
  end
427
545
  end
428
546
 
429
547
  # Create orchestrator with restoration mode
430
548
  generator = McpGenerator.new(config, vibe: options[:vibe], restore_session_path: session_path)
431
- orchestrator = Orchestrator.new(config, generator,
432
- vibe: options[:vibe],
433
- prompt: options[:prompt],
434
- stream_logs: options[:stream_logs],
435
- debug: options[:debug],
436
- restore_session_path: session_path,
437
- worktree: worktree_name)
549
+ orchestrator = Orchestrator.new(
550
+ config,
551
+ generator,
552
+ vibe: options[:vibe],
553
+ prompt: options[:prompt],
554
+ stream_logs: options[:stream_logs],
555
+ debug: options[:debug],
556
+ restore_session_path: session_path,
557
+ worktree: worktree_name,
558
+ session_id: options[:session_id],
559
+ )
438
560
  orchestrator.start
439
561
  rescue StandardError => e
440
- error "Failed to restore session: #{e.message}"
441
- error e.backtrace.join("\n") if options[:debug]
442
- exit 1
562
+ error("Failed to restore session: #{e.message}")
563
+ error(e.backtrace.join("\n")) if options[:debug]
564
+ exit(1)
443
565
  end
444
566
  end
445
567
 
446
568
  def find_session_path(session_id)
447
569
  sessions_dir = File.expand_path("~/.claude-swarm/sessions")
448
570
 
449
- # Check if it's a full path
450
- return session_id if File.exist?(File.join(session_id, "config.yml"))
451
-
452
571
  # Search for the session ID in all projects
453
572
  Dir.glob("#{sessions_dir}/*/#{session_id}").each do |path|
454
573
  config_path = File.join(path, "config.yml")
@@ -518,8 +637,17 @@ module ClaudeSwarm
518
637
  repo_path = repo_git_path.split("/.git/worktrees/").first
519
638
 
520
639
  # Try to remove worktree via git
521
- system!("git", "-C", repo_path, "worktree", "remove", worktree_path, "--force",
522
- out: File::NULL, err: File::NULL)
640
+ system!(
641
+ "git",
642
+ "-C",
643
+ repo_path,
644
+ "worktree",
645
+ "remove",
646
+ worktree_path,
647
+ "--force",
648
+ out: File::NULL,
649
+ err: File::NULL,
650
+ )
523
651
  end
524
652
  end
525
653
 
@@ -532,7 +660,7 @@ module ClaudeSwarm
532
660
  cleaned += 1
533
661
  end
534
662
  rescue StandardError => e
535
- say "Warning: Failed to clean worktree directory #{session_worktree_dir}: #{e.message}", :yellow if options[:debug]
663
+ say("Warning: Failed to clean worktree directory #{session_worktree_dir}: #{e.message}", :yellow) if options[:debug]
536
664
  end
537
665
  end
538
666